package iclient import ( "errors" "fmt" "maps" "os" "path/filepath" "github.com/lxc/incus/v7/shared/util" ) // ConfigRemoteTLS holds inline TLS material for a remote. type ConfigRemoteTLS struct { Certificate string `json:"certificate"` Key string `json:"key"` CA string `json:"ca"` } // ConfigRemote holds the details for communicating with one Incus daemon. type ConfigRemote struct { Addrs []string `yaml:"-"` LastWorkingAddr string `yaml:"last_working_address,omitempty"` AuthType string `yaml:"auth_type,omitempty"` KeepAlive int `yaml:"keepalive,omitempty"` Protocol string `yaml:"protocol,omitempty"` CredHelper string `yaml:"credentials_helper,omitempty"` Public bool `yaml:"public"` Global bool `yaml:"-"` Static bool `yaml:"-"` TLS *ConfigRemoteTLS `yaml:"-"` } // UnmarshalYAML reads `addr` as a comma-separated list into Addrs. func (r *ConfigRemote) UnmarshalYAML(unmarshal func(any) error) error { type R ConfigRemote tmp := struct { *R `yaml:",inline"` Addrs string `yaml:"addr"` }{ R: (*R)(r), } err := unmarshal(&tmp) if err != nil { return err } r.Addrs = util.SplitNTrimSpace(tmp.Addrs, ",", -1, true) if r.Addrs == nil { return errors.New("remotes must have at least one address") } return nil } // rollingAddrs returns the addresses starting at the last known working one. func (r ConfigRemote) rollingAddrs() []string { if len(r.Addrs) == 0 { return nil } start := 0 if r.LastWorkingAddr != "" { for i, addr := range r.Addrs { if addr == r.LastWorkingAddr { start = i break } } } out := make([]string, 0, len(r.Addrs)) for i := range r.Addrs { out = append(out, r.Addrs[(start+i)%len(r.Addrs)]) } return out } // Config is the parsed Incus CLI configuration. type Config struct { DefaultRemote string `yaml:"default-remote"` Remotes map[string]ConfigRemote `yaml:"remotes"` // ConfigDir is where the configuration was read from; the certificate // paths hang off it. ConfigDir string `yaml:"-"` // UserAgent is sent with every request. UserAgent string `yaml:"-"` // creds memoises what each remote's credentials helper answered, so a run // resolving one registry for a dozen images spawns the helper once. Every // remote gets an entry up front, so this map is only ever read. creds map[string]*credEntry } // ConfigLocalRemote is the unix socket remote, which cannot be removed. var ConfigLocalRemote = ConfigRemote{ Addrs: []string{"unix://"}, Static: true, Public: false, Protocol: "incus", } // ConfigImagesRemote is the community image server. var ConfigImagesRemote = ConfigRemote{ Addrs: []string{"https://images.linuxcontainers.org"}, Public: true, Protocol: "simplestreams", } // ConfigDefaultRemotes is the set used when there is no configuration file. var ConfigDefaultRemotes = map[string]ConfigRemote{ "images": ConfigImagesRemote, "local": ConfigLocalRemote, } // ConfigDefaultConfig returns the configuration used when no file exists. func ConfigDefaultConfig() *Config { c := &Config{ Remotes: maps.Clone(ConfigDefaultRemotes), DefaultRemote: "local", } c.prefillCreds() return c } // prefillCreds gives every remote its credential entry, so resolving one later // only reads the map and needs no lock of its own. func (c *Config) prefillCreds() { c.creds = make(map[string]*credEntry, len(c.Remotes)) for name := range c.Remotes { c.creds[name] = &credEntry{} } } // configGlobalPath joins the system-wide configuration directory. func configGlobalPath(paths ...string) string { dir := "/etc/incus" v, ok := os.LookupEnv("INCUS_GLOBAL_CONF") if ok { dir = v } return filepath.Join(append([]string{dir}, paths...)...) } // Path joins the configuration directory with the given elements. func (c *Config) Path(paths ...string) string { return filepath.Join(append([]string{c.ConfigDir}, paths...)...) } // ServerCertPath returns where the remote's pinned server certificate lives. func (c *Config) ServerCertPath(remote string) string { if c.Remotes[remote].Global { return configGlobalPath("servercerts", fmt.Sprintf("%s.crt", remote)) } return c.Path("servercerts", fmt.Sprintf("%s.crt", remote)) } // clientCertPaths returns the certificate, key and CA paths for a remote, // preferring a per-remote set over the shared one. func (c *Config) clientCertPaths(remote string) (cert, key, ca string) { perRemote := c.Path("clientcerts", fmt.Sprintf("%s.crt", remote)) if util.PathExists(perRemote) { return perRemote, c.Path("clientcerts", fmt.Sprintf("%s.key", remote)), c.Path("clientcerts", fmt.Sprintf("%s.ca", remote)) } return c.Path("client.crt"), c.Path("client.key"), c.Path("client.ca") } // readIfPresent returns the file's contents, or "" when it does not exist. func readIfPresent(path string) (string, error) { if !util.PathExists(path) { return "", nil } content, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("reading %s: %w", path, err) } return string(content), nil }