package config import ( "encoding/json" "errors" "fmt" "net" "os" "runtime" "slices" "strings" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/canonical/lxd/client" "github.com/canonical/lxd/lxc/cookiejar" "github.com/canonical/lxd/shared" "github.com/canonical/lxd/shared/api" ) // Remote holds details for communication with a remote daemon. type Remote struct { Addr string `yaml:"addr"` AuthType string `yaml:"auth_type,omitempty"` Project string `yaml:"project,omitempty"` Protocol string `yaml:"protocol,omitempty"` Public bool `yaml:"public"` Global bool `yaml:"-"` Static bool `yaml:"-"` } // ParseRemote splits remote and object. func (c *Config) ParseRemote(raw string) (remoteName string, resourceName string, err error) { remote, object, found := strings.Cut(raw, ":") if !found { return c.DefaultRemote, raw, nil } _, ok := c.Remotes[remote] if !ok { // Attempt to play nice with snapshots containing ":" if shared.IsSnapshot(raw) && shared.IsSnapshot(remote) { return c.DefaultRemote, raw, nil } return "", "", fmt.Errorf("The remote \"%s\" does not exist", remote) } return remote, object, nil } // GetInstanceServer returns a lxd.InstanceServer for the remote with the given name. func (c *Config) GetInstanceServer(name string) (lxd.InstanceServer, error) { return c.GetInstanceServerWithConnectionArgs(name, nil) } // GetInstanceServerWithConnectionArgs returns a lxd.InstanceServer for the remote with the given name. Any // populated fields of the given connection arguments override the default connection arguments for the remote. func (c *Config) GetInstanceServerWithConnectionArgs(name string, inArgs *lxd.ConnectionArgs) (lxd.InstanceServer, error) { remote, err := c.getPrivateRemoteByName(name) if err != nil { return nil, err } // Get connection arguments args, err := c.getConnectionArgs(name) if err != nil { return nil, err } if inArgs != nil { args = mergeConnectionArgs(*args, *inArgs) } return c.connectRemote(*remote, args) } // mergeConnectionArgs returns a copy of baseArgs where each field is overwritten with fields from additionalArgs (if // non-zero). This is useful for when the CLI needs the base connection arguments for a remote, but also needs to set // a transport wrapper for extracting headers, or to skip calling GET /1.0 on each API call. func mergeConnectionArgs(baseArgs lxd.ConnectionArgs, additionalArgs lxd.ConnectionArgs) *lxd.ConnectionArgs { args := baseArgs if additionalArgs.TLSServerCert != "" { args.TLSServerCert = additionalArgs.TLSServerCert } if additionalArgs.TLSClientCert != "" { args.TLSClientCert = additionalArgs.TLSClientCert } if additionalArgs.TLSClientKey != "" { args.TLSClientKey = additionalArgs.TLSClientKey } if additionalArgs.TLSCA != "" { args.TLSCA = additionalArgs.TLSCA } if additionalArgs.UserAgent != "" { args.UserAgent = additionalArgs.UserAgent } if additionalArgs.AuthType != "" { args.AuthType = additionalArgs.AuthType } if additionalArgs.Proxy != nil { args.Proxy = additionalArgs.Proxy } if additionalArgs.HTTPClient != nil { args.HTTPClient = additionalArgs.HTTPClient } if additionalArgs.TransportWrapper != nil { args.TransportWrapper = additionalArgs.TransportWrapper } if additionalArgs.InsecureSkipVerify { args.InsecureSkipVerify = additionalArgs.InsecureSkipVerify } if additionalArgs.CookieJar != nil { args.CookieJar = additionalArgs.CookieJar } if additionalArgs.OIDCTokens != nil { args.OIDCTokens = additionalArgs.OIDCTokens } if additionalArgs.SkipGetServer { args.SkipGetServer = additionalArgs.SkipGetServer } if additionalArgs.CachePath != "" { args.CachePath = additionalArgs.CachePath } if additionalArgs.CacheExpiry != 0 { args.CacheExpiry = additionalArgs.CacheExpiry } return &args } // getPrivateRemoteByName returns the Remote with the given name and ensures that the remote is not public. func (c *Config) getPrivateRemoteByName(name string) (*Remote, error) { remote, err := c.getPublicRemoteByName(name) if err != nil { return nil, err } // Check the remote is private. if remote.Public || remote.Protocol == "simplestreams" { return nil, errors.New("The remote is not a private LXD server") } return remote, nil } // getPublicRemoteByName returns the Remote with the given name. func (c *Config) getPublicRemoteByName(name string) (*Remote, error) { // Handle "local" on non-Linux if name == "local" && runtime.GOOS != "linux" { return nil, ErrNotLinux } // Get the remote remote, ok := c.Remotes[name] if !ok { return nil, fmt.Errorf("The remote \"%s\" does not exist", name) } return &remote, nil } // connectRemote returns a lxd.InstanceServer for the given Remote and configures it with the given lxd.ConnectionArgs. func (c *Config) connectRemote(remote Remote, args *lxd.ConnectionArgs) (lxd.InstanceServer, error) { // Unix socket after, ok := strings.CutPrefix(remote.Addr, "unix:") if ok { d, err := lxd.ConnectLXDUnix(strings.TrimPrefix(after, "//"), args) if err != nil { netErr, ok := errors.AsType[*net.OpError](err) if ok { if errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("LXD unix socket %q not found: Please check LXD is running", netErr.Addr) } if errors.Is(err, os.ErrPermission) { return nil, fmt.Errorf("LXD unix socket %q not accessible: permission denied", netErr.Addr) } return nil, fmt.Errorf("LXD unix socket %q not accessible: %w", netErr.Addr, err) } return nil, fmt.Errorf("LXD unix socket not accessible: %w", err) } if remote.Project != "" && remote.Project != "default" { d = d.UseProject(remote.Project) } if c.ProjectOverride != "" { d = d.UseProject(c.ProjectOverride) } return d, nil } // HTTPS // If bearer token is provided, we don't need TLS client certificate and key. // However, do not advertise that bearer token can be configured. // The support for bearer token in LXC is purely for testing purposes. if !slices.Contains([]string{api.AuthenticationMethodOIDC}, remote.AuthType) && (args.TLSClientCert == "" || args.TLSClientKey == "") && args.BearerToken == "" { return nil, errors.New("Missing TLS client certificate and key") } d, err := lxd.ConnectLXD(remote.Addr, args) if err != nil { return nil, err } if remote.Project != "" && remote.Project != "default" { d = d.UseProject(remote.Project) } if c.ProjectOverride != "" { d = d.UseProject(c.ProjectOverride) } return d, nil } // GetImageServer returns a ImageServer struct for the remote. func (c *Config) GetImageServer(name string) (lxd.ImageServer, error) { remote, err := c.getPublicRemoteByName(name) if err != nil { return nil, err } // Get connection arguments args, err := c.getConnectionArgs(name) if err != nil { return nil, err } // Unix socket after, ok := strings.CutPrefix(remote.Addr, "unix:") if ok { d, err := lxd.ConnectLXDUnix(strings.TrimPrefix(after, "//"), args) if err != nil { return nil, err } if remote.Project != "" && remote.Project != "default" { d = d.UseProject(remote.Project) } if c.ProjectOverride != "" { d = d.UseProject(c.ProjectOverride) } return d, nil } // HTTPs (simplestreams) if remote.Protocol == "simplestreams" { d, err := lxd.ConnectSimpleStreams(remote.Addr, args) if err != nil { return nil, err } return d, nil } // HTTPs (public LXD) if remote.Public { d, err := lxd.ConnectPublicLXD(remote.Addr, args) if err != nil { return nil, err } return d, nil } // HTTPs (private LXD) d, err := lxd.ConnectLXD(remote.Addr, args) if err != nil { return nil, err } if remote.Project != "" && remote.Project != "default" { d = d.UseProject(remote.Project) } if c.ProjectOverride != "" { d = d.UseProject(c.ProjectOverride) } return d, nil } // getConnectionArgs retrieves the connection arguments for the specified remote. // It constructs the necessary connection arguments based on the remote's configuration, including authentication type, // authentication interactors, cookie jar, OIDC tokens, TLS certificates, and client key. // The function returns the connection arguments or an error if any configuration is missing or encounters a problem. func (c *Config) getConnectionArgs(name string) (*lxd.ConnectionArgs, error) { remote := c.Remotes[name] args := lxd.ConnectionArgs{ UserAgent: c.UserAgent, AuthType: remote.AuthType, } if args.AuthType == api.AuthenticationMethodOIDC { if c.oidcTokens == nil { c.oidcTokens = map[string]*oidc.Tokens[*oidc.IDTokenClaims]{} } tokenPath := c.OIDCTokenPath(name) if c.oidcTokens[name] == nil { content, err := os.ReadFile(tokenPath) if err == nil { var tokens oidc.Tokens[*oidc.IDTokenClaims] err = json.Unmarshal(content, &tokens) if err != nil { return nil, err } c.oidcTokens[name] = &tokens } else if os.IsNotExist(err) { c.oidcTokens[name] = &oidc.Tokens[*oidc.IDTokenClaims]{} } else { return nil, err } } args.OIDCTokens = c.oidcTokens[name] if c.cookieJars == nil || c.cookieJars[name] == nil { err := os.MkdirAll(c.ConfigPath("jars"), 0700) if err != nil { return nil, err } _, err = os.Stat(c.CookiesPath(name)) if err != nil && os.IsNotExist(err) { // Migrate legacy cookies file if it exists. err = shared.FileCopy(c.ConfigPath("cookies"), c.CookiesPath(name)) if err != nil && !os.IsNotExist(err) { return nil, err } } else if err != nil { return nil, err } jar, err := cookiejar.Open(c.CookiesPath(name), remote.Addr) if err != nil { return nil, err } if c.cookieJars == nil { c.cookieJars = map[string]*cookiejar.Jar{} } c.cookieJars[name] = jar } args.CookieJar = c.cookieJars[name] } // Stop here if no TLS involved if strings.HasPrefix(remote.Addr, "unix:") { return &args, nil } // Server certificate content, err := os.ReadFile(c.ServerCertPath(name)) if err == nil { args.TLSServerCert = string(content) } else if !os.IsNotExist(err) { return nil, err } // Stop here if no client certificate involved if remote.Protocol == "simplestreams" || slices.Contains([]string{api.AuthenticationMethodOIDC}, remote.AuthType) { return &args, nil } // Check for LXD_AUTH_BEARER_TOKEN environment variable // Stop here if bearer token is used. It takes precedence over the TLS client certificate and key. token, ok := os.LookupEnv("LXD_AUTH_BEARER_TOKEN") if ok && token != "" { args.BearerToken = token return &args, nil } // Client certificate content, err = os.ReadFile(c.ConfigPath("client.crt")) if err == nil { args.TLSClientCert = string(content) } else if !os.IsNotExist(err) { return nil, err } // Client CA content, err = os.ReadFile(c.ConfigPath("client.ca")) if err == nil { args.TLSCA = string(content) } else if !os.IsNotExist(err) { return nil, err } // Client key content, err = os.ReadFile(c.ConfigPath("client.key")) if err == nil { args.TLSClientKey = string(content) } else if !os.IsNotExist(err) { return nil, err } return &args, nil }