package config
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/opencontainers/go-digest"
"github.com/openeuler/Conch/internal/netstack"
"github.com/openeuler/Conch/internal/runtimeapi"
"github.com/openeuler/Conch/pkg/ulog"
"gopkg.in/yaml.v3"
)
var WorkDir = defaultWorkDir
const (
defaultWorkDir = "/var/run/conch"
defaultStateDir = "/var/lib/conch"
)
type Config struct {
App AppConfig `yaml:"app"`
Log LogConfig `yaml:"log"`
Server ServerConfig `yaml:"server"`
Network NetworkConfig `yaml:"network"`
Sandbox SandboxConfig `yaml:"sandbox"`
Volume VolumeConfig `yaml:"volume"`
}
type AppConfig struct {
Name string `yaml:"name"`
}
type LogConfig struct {
Level string `yaml:"level"`
Output string `yaml:"output"`
}
type ServerConfig struct {
WorkDir string `yaml:"work_dir"`
StateDir string `yaml:"state_dir"`
}
type NetworkConfig struct {
WarmPoolSize int `yaml:"warm_pool_size"`
CNI CNIConfig `yaml:"cni"`
}
type CNIConfig = netstack.CNIManagerConfig
type VMMBinaryConfig struct {
Binary string `yaml:"binary"`
}
const (
DefaultSandboxBackend = "stratovirt"
defaultVolumeBackend = "virtiofs"
)
type SandboxConfig struct {
VsockSignalRetry time.Duration `yaml:"vsock_signal_retry"`
VsockSignalTimeout time.Duration `yaml:"vsock_signal_timeout"`
RequestTimeout time.Duration `yaml:"request_timeout"`
Backend string `yaml:"backend"`
DefaultSpec SandboxSpec `yaml:"default_spec"`
CloudHypervisor *VMMBinaryConfig `yaml:"cloud_hypervisor"`
Stratovirt *VMMBinaryConfig `yaml:"stratovirt"`
}
type SandboxSpec struct {
TemplateName string `yaml:"template_name"`
TemplateID string `yaml:"template_id"`
VCPUNum int64 `yaml:"vcpu_num"`
VCPUMax int64 `yaml:"vcpu_max"`
RamMB int64 `yaml:"ram_mb"`
}
func (c SandboxConfig) BinaryPaths() map[string]string {
paths := make(map[string]string, 2)
if c.CloudHypervisor != nil {
paths["cloud-hypervisor"] = c.CloudHypervisor.Binary
}
if c.Stratovirt != nil {
paths["stratovirt"] = c.Stratovirt.Binary
}
return paths
}
type VolumeConfig struct {
MaxMounts int `yaml:"max_mounts"`
Backend string `yaml:"backend"`
Virtiofs VolumeVirtiofsConfig `yaml:"virtiofs"`
}
type VolumeVirtiofsConfig struct {
Binary string `yaml:"binary"`
}
func DefaultConfig() *Config {
return &Config{
App: AppConfig{
Name: "conch",
},
Log: LogConfig{
Level: "debug",
Output: "stdout",
},
Server: ServerConfig{
WorkDir: defaultWorkDir,
StateDir: defaultStateDir,
},
Network: NetworkConfig{
WarmPoolSize: netstack.DefaultWarmPoolSize,
CNI: CNIConfig{
PluginBinDirs: []string{netstack.DefaultCNIPluginBinDir},
PluginConfDir: netstack.DefaultCNIPluginConfDir,
CacheDir: defaultStateDir + "/cni",
},
},
Sandbox: SandboxConfig{
VsockSignalRetry: 10 * time.Millisecond,
VsockSignalTimeout: 60 * time.Second,
RequestTimeout: 60 * time.Second,
Backend: DefaultSandboxBackend,
DefaultSpec: SandboxSpec{
VCPUNum: 2,
VCPUMax: 2,
RamMB: 2048,
},
Stratovirt: &VMMBinaryConfig{Binary: "/usr/bin/stratovirt"},
},
Volume: VolumeConfig{
MaxMounts: 10,
Backend: defaultVolumeBackend,
Virtiofs: VolumeVirtiofsConfig{
Binary: "virtiofsd",
},
},
}
}
func LoadConfig(configPath string) (*Config, error) {
if configPath == "" {
return DefaultConfig(), nil
}
if absPath, err := filepath.Abs(configPath); err == nil {
configPath = absPath
}
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
fileInfo, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("failed to inspect config file permissions: %w", err)
}
if fileInfo.Mode().Perm()&0o037 != 0 {
return nil, fmt.Errorf(
"config file %q has insecure permissions %04o",
configPath,
fileInfo.Mode().Perm(),
)
}
var cfg Config
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
defaultCfg := DefaultConfig()
if cfg.App.Name == "" {
cfg.App.Name = defaultCfg.App.Name
}
if cfg.Log.Level == "" {
cfg.Log.Level = defaultCfg.Log.Level
}
if cfg.Log.Output == "" {
cfg.Log.Output = defaultCfg.Log.Output
}
if cfg.Server.WorkDir == "" {
cfg.Server.WorkDir = defaultCfg.Server.WorkDir
}
if cfg.Server.StateDir == "" {
cfg.Server.StateDir = defaultCfg.Server.StateDir
}
if cfg.Network.WarmPoolSize == 0 {
cfg.Network.WarmPoolSize = defaultCfg.Network.WarmPoolSize
}
if len(cfg.Network.CNI.PluginBinDirs) == 0 {
cfg.Network.CNI.PluginBinDirs = defaultCfg.Network.CNI.PluginBinDirs
}
cfg.Network.CNI.CacheDir = filepath.Join(cfg.Server.StateDir, "cni")
if cfg.Sandbox.VsockSignalRetry == 0 {
cfg.Sandbox.VsockSignalRetry = defaultCfg.Sandbox.VsockSignalRetry
}
if cfg.Sandbox.VsockSignalTimeout == 0 {
cfg.Sandbox.VsockSignalTimeout = defaultCfg.Sandbox.VsockSignalTimeout
}
if cfg.Sandbox.RequestTimeout == 0 {
cfg.Sandbox.RequestTimeout = defaultCfg.Sandbox.RequestTimeout
}
if cfg.Sandbox.Backend == "" {
cfg.Sandbox.Backend = defaultCfg.Sandbox.Backend
}
if cfg.Sandbox.Stratovirt == nil {
cfg.Sandbox.Stratovirt = defaultCfg.Sandbox.Stratovirt
}
if cfg.Sandbox.DefaultSpec.VCPUNum == 0 {
cfg.Sandbox.DefaultSpec.VCPUNum = defaultCfg.Sandbox.DefaultSpec.VCPUNum
}
if cfg.Sandbox.DefaultSpec.VCPUMax == 0 {
cfg.Sandbox.DefaultSpec.VCPUMax = defaultCfg.Sandbox.DefaultSpec.VCPUMax
}
if cfg.Sandbox.DefaultSpec.RamMB == 0 {
cfg.Sandbox.DefaultSpec.RamMB = defaultCfg.Sandbox.DefaultSpec.RamMB
}
if cfg.Volume.MaxMounts == 0 {
cfg.Volume.MaxMounts = defaultCfg.Volume.MaxMounts
}
if cfg.Volume.Backend == "" {
cfg.Volume.Backend = defaultCfg.Volume.Backend
}
if cfg.Volume.Virtiofs.Binary == "" {
cfg.Volume.Virtiofs.Binary = defaultCfg.Volume.Virtiofs.Binary
}
if err := validateConfig(&cfg); err != nil {
return nil, err
}
if cfg.Server.WorkDir != "" {
WorkDir = cfg.Server.WorkDir
}
return &cfg, nil
}
func validateConfig(cfg *Config) error {
if !filepath.IsAbs(cfg.Server.WorkDir) {
return fmt.Errorf("invalid server.work_dir=%q: must be an absolute path", cfg.Server.WorkDir)
}
if !filepath.IsAbs(cfg.Server.StateDir) {
return fmt.Errorf("invalid server.state_dir=%q: must be an absolute path", cfg.Server.StateDir)
}
if cfg.Network.WarmPoolSize < 0 {
return fmt.Errorf("invalid network.warm_pool_size=%d: must be greater than or equal to 0", cfg.Network.WarmPoolSize)
}
if cfg.Volume.MaxMounts < 0 {
return fmt.Errorf("invalid volume.max_mounts=%d: must be greater than or equal to 0", cfg.Volume.MaxMounts)
}
templateName := strings.TrimSpace(cfg.Sandbox.DefaultSpec.TemplateName)
templateID := strings.TrimSpace(cfg.Sandbox.DefaultSpec.TemplateID)
if templateName != "" && templateID != "" {
return fmt.Errorf("sandbox.default_spec.template_name and sandbox.default_spec.template_id are mutually exclusive")
}
if templateID != "" {
parsed, err := digest.Parse(templateID)
if err != nil {
return fmt.Errorf("invalid sandbox.default_spec.template_id %q: %w", templateID, err)
}
templateID = parsed.String()
}
cfg.Sandbox.DefaultSpec.TemplateName = templateName
cfg.Sandbox.DefaultSpec.TemplateID = templateID
backend := strings.TrimSpace(cfg.Volume.Backend)
if backend != "" && backend != defaultVolumeBackend {
return fmt.Errorf("invalid volume.backend=%q: only %q is supported", cfg.Volume.Backend, defaultVolumeBackend)
}
if cfg.Sandbox.DefaultSpec.VCPUNum < 1 ||
cfg.Sandbox.DefaultSpec.VCPUMax < cfg.Sandbox.DefaultSpec.VCPUNum ||
cfg.Sandbox.DefaultSpec.VCPUNum > runtimeapi.SandboxMaxVCPU ||
cfg.Sandbox.DefaultSpec.VCPUMax > runtimeapi.SandboxMaxVCPU {
return fmt.Errorf("invalid sandbox.default_spec CPU configuration")
}
if cfg.Sandbox.DefaultSpec.RamMB < 1 || cfg.Sandbox.DefaultSpec.RamMB > runtimeapi.SandboxMaxRAMMB {
return fmt.Errorf("invalid sandbox.default_spec.ram_mb=%d: must be between 1 and %d", cfg.Sandbox.DefaultSpec.RamMB, runtimeapi.SandboxMaxRAMMB)
}
if err := validateVMMBinaryConfig("cloud_hypervisor", cfg.Sandbox.CloudHypervisor); err != nil {
return err
}
if err := validateVMMBinaryConfig("stratovirt", cfg.Sandbox.Stratovirt); err != nil {
return err
}
vmmBinaries := cfg.Sandbox.BinaryPaths()
if _, ok := vmmBinaries[cfg.Sandbox.Backend]; !ok {
return fmt.Errorf("sandbox.backend %q is not configured", cfg.Sandbox.Backend)
}
return nil
}
func validateVMMBinaryConfig(name string, cfg *VMMBinaryConfig) error {
if cfg == nil {
return nil
}
binary := strings.TrimSpace(cfg.Binary)
if binary == "" {
return fmt.Errorf("vmm.%s.binary is required when vmm.%s is configured", name, name)
}
if !filepath.IsAbs(binary) {
return fmt.Errorf("invalid vmm.%s.binary=%q: must be an absolute path", name, cfg.Binary)
}
cfg.Binary = filepath.Clean(binary)
return nil
}
func (c *Config) GetLogConfig() (ulog.Config, error) {
level, err := parseLogLevel(c.Log.Level)
if err != nil {
return ulog.Config{}, fmt.Errorf("invalid log level: %w", err)
}
var stdout bool
var outputPath string
switch c.Log.Output {
case "stdout":
stdout = true
outputPath = ""
case "file":
stdout = false
outputPath = "/var/log/conchd/"
case "both":
stdout = true
outputPath = "/var/log/conchd/"
default:
return ulog.Config{}, fmt.Errorf("invalid log output mode: %s (must be stdout, file, or both)", c.Log.Output)
}
return ulog.Config{
Level: level,
OutputPath: outputPath,
Stdout: stdout,
}, nil
}
func (c *Config) GetServerUnixSocket() string {
if c == nil {
return ""
}
return filepath.Join(c.Server.WorkDir, "conchd.sock")
}
func (c *Config) PIDFilePath() string { return filepath.Join(c.Server.WorkDir, "conchd.pid") }
func (c *Config) ContainerdRootDir() string { return filepath.Join(c.Server.StateDir, "containerd") }
func (c *Config) ContainerdStateDir() string { return filepath.Join(c.Server.WorkDir, "containerd") }
func (c *Config) VirtiofsRuntimeDir() string { return filepath.Join(c.Server.WorkDir, "sandboxes") }
func parseLogLevel(level string) (ulog.LogLevel, error) {
switch level {
case "debug":
return ulog.DebugLevel, nil
case "info":
return ulog.InfoLevel, nil
case "warn":
return ulog.WarnLevel, nil
case "error":
return ulog.ErrorLevel, nil
case "fatal":
return ulog.FatalLevel, nil
default:
return 0, fmt.Errorf("unknown log level: %s", level)
}
}
func FindConfigFile() string {
locations := []string{
"/etc/conch/config.yaml",
"config/config.yaml",
}
for _, loc := range locations {
if _, err := os.Stat(loc); err == nil {
absPath, _ := filepath.Abs(loc)
return absPath
}
}
return ""
}