incus-mirror/shared/cliconfig/file.go
Stéphane Graber f3007efb9e
incus: Don't persist INCUS_REMOTE as the default remote
Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
2026-07-17 17:03:58 -04:00

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
}