mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
248 lines
5.7 KiB
Go
248 lines
5.7 KiB
Go
package cliconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"go.yaml.in/yaml/v4"
|
|
|
|
"github.com/lxc/incus/v7/shared/api"
|
|
"github.com/lxc/incus/v7/shared/logger"
|
|
"github.com/lxc/incus/v7/shared/util"
|
|
)
|
|
|
|
func getConfigDir() (string, error) {
|
|
// Honor an explicit override.
|
|
if os.Getenv("INCUS_CONF") != "" {
|
|
return os.Getenv("INCUS_CONF"), nil
|
|
}
|
|
|
|
// Use the platform-specific user configuration directory.
|
|
// (~/.config on Linux, ~/Library/Application Support on macOS, %AppData% on Windows)
|
|
baseDir, err := os.UserConfigDir()
|
|
if err != nil || baseDir == "" {
|
|
// Fall back to the current user's home directory.
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if usr.HomeDir == "" {
|
|
return "", nil
|
|
}
|
|
|
|
baseDir = filepath.Join(usr.HomeDir, ".config")
|
|
}
|
|
|
|
return filepath.Join(baseDir, "incus"), nil
|
|
}
|
|
|
|
// migrateConfigDir moves the configuration from the legacy ~/.config/incus
|
|
// location to the platform-specific directory on first use.
|
|
func migrateConfigDir(configDir string) {
|
|
// Nothing to migrate if the target already exists or an override is set.
|
|
if util.PathExists(configDir) || os.Getenv("INCUS_CONF") != "" {
|
|
return
|
|
}
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || home == "" {
|
|
return
|
|
}
|
|
|
|
legacyDir := filepath.Join(home, ".config", "incus")
|
|
if legacyDir == configDir || !util.PathExists(legacyDir) {
|
|
return
|
|
}
|
|
|
|
// Make sure the parent of the target exists, then move the data over.
|
|
err = os.MkdirAll(filepath.Dir(configDir), 0o700)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = os.Rename(legacyDir, configDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// On macOS, leave a symlink at the legacy location as ~/.config is
|
|
// commonly used by CLI tools and users may expect to find it there.
|
|
if runtime.GOOS == "darwin" {
|
|
_ = os.Symlink(configDir, legacyDir)
|
|
}
|
|
}
|
|
|
|
func getConfigPaths() (string, string, error) {
|
|
configDir, err := getConfigDir()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if configDir == "" {
|
|
return "", "", nil
|
|
}
|
|
|
|
// Migrate any configuration from the legacy location.
|
|
migrateConfigDir(configDir)
|
|
|
|
configPath := os.ExpandEnv(filepath.Join(configDir, "config.yml"))
|
|
|
|
return configPath, filepath.Dir(configPath), nil
|
|
}
|
|
|
|
// LoadConfig reads the configuration from the config path; if the path does
|
|
// not exist, it returns a default configuration; if the given path is empty
|
|
// it tries to determine automatically the configuration file to load.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
configDir := filepath.Dir(path)
|
|
if path == "" {
|
|
var err error
|
|
path, configDir, err = getConfigPaths()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if path == "" || !util.PathExists(path) {
|
|
return NewConfig(configDir, true), nil
|
|
}
|
|
|
|
// Open the config file
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to read the configuration file: %w", err)
|
|
}
|
|
|
|
// Decode the YAML document
|
|
c := NewConfig(configDir, false)
|
|
err = yaml.Load(content, c)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to decode the configuration: %w", err)
|
|
}
|
|
|
|
for k, r := range c.Remotes {
|
|
if !r.Public && r.AuthType == "" {
|
|
r.AuthType = api.AuthenticationMethodTLS
|
|
c.Remotes[k] = r
|
|
}
|
|
}
|
|
|
|
// Apply the global (system-wide) remotes
|
|
globalConf := NewConfig("", false)
|
|
content, err = os.ReadFile(globalConf.GlobalConfigPath("config.yml"))
|
|
if err == nil {
|
|
err = yaml.Load(content, globalConf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to decode the configuration: %w", err)
|
|
}
|
|
|
|
for k, r := range globalConf.Remotes {
|
|
_, ok := c.Remotes[k]
|
|
if !ok {
|
|
r.Global = true
|
|
c.Remotes[k] = r
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set default values
|
|
if c.Remotes == nil {
|
|
c.Remotes = make(map[string]Remote)
|
|
}
|
|
|
|
// Apply the static remotes
|
|
for k, v := range StaticRemotes {
|
|
if c.Remotes[k].Project != "" {
|
|
v.Project = c.Remotes[k].Project
|
|
}
|
|
|
|
c.Remotes[k] = v
|
|
}
|
|
|
|
// Fill in defaults.
|
|
for k, r := range c.Remotes {
|
|
if r.Protocol == "" {
|
|
r.Protocol = "incus"
|
|
c.Remotes[k] = r
|
|
}
|
|
}
|
|
|
|
if c.DefaultRemote == "" {
|
|
c.DefaultRemote = DefaultConfig().DefaultRemote
|
|
}
|
|
|
|
// Keep track of the on-disk default remote.
|
|
c.fileDefaultRemote = c.DefaultRemote
|
|
|
|
// If the environment specifies a remote this takes priority over what
|
|
// is defined in the configuration
|
|
envDefaultRemote := os.Getenv("INCUS_REMOTE")
|
|
if len(envDefaultRemote) > 0 {
|
|
c.DefaultRemote = envDefaultRemote
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// SaveConfig writes the provided configuration to the config file.
|
|
func (c *Config) SaveConfig(path string) error {
|
|
// Create a new copy for the config file
|
|
conf := Config{}
|
|
err := util.DeepCopy(c, &conf)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to copy the configuration: %w", err)
|
|
}
|
|
|
|
// Remove the global remotes
|
|
for k, v := range c.Remotes {
|
|
if v.Global {
|
|
delete(conf.Remotes, k)
|
|
}
|
|
}
|
|
|
|
// Don't persist a default remote that came from the environment.
|
|
envDefaultRemote := os.Getenv("INCUS_REMOTE")
|
|
if envDefaultRemote != "" && conf.DefaultRemote == envDefaultRemote && c.fileDefaultRemote != "" {
|
|
_, ok := c.Remotes[c.fileDefaultRemote]
|
|
if ok {
|
|
conf.DefaultRemote = c.fileDefaultRemote
|
|
}
|
|
}
|
|
|
|
defaultRemote := DefaultConfig().DefaultRemote
|
|
|
|
// Remove the static remotes
|
|
for k := range StaticRemotes {
|
|
if k == defaultRemote {
|
|
continue
|
|
}
|
|
|
|
delete(conf.Remotes, k)
|
|
}
|
|
|
|
// Create the config file (or truncate an existing one)
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to create the configuration file: %w", err)
|
|
}
|
|
|
|
defer logger.WarnOnError(f.Close, "Failed to close file")
|
|
|
|
// Write the new config
|
|
data, err := yaml.Dump(&conf, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to marshal the configuration: %w", err)
|
|
}
|
|
|
|
_, err = f.Write(data)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to write the configuration: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|