github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/lxd/provider.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	stdcontext "context"
     8  	"net/http"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	jujuhttp "github.com/juju/http/v2"
    16  	"github.com/juju/jsonschema"
    17  	"github.com/juju/schema"
    18  	"gopkg.in/juju/environschema.v1"
    19  	"gopkg.in/yaml.v2"
    20  
    21  	"github.com/juju/juju/cloud"
    22  	"github.com/juju/juju/container/lxd"
    23  	corelogger "github.com/juju/juju/core/logger"
    24  	"github.com/juju/juju/environs"
    25  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/environs/context"
    28  	"github.com/juju/juju/provider/lxd/lxdnames"
    29  )
    30  
    31  // LXCConfigReader reads files required for the LXC configuration.
    32  type LXCConfigReader interface {
    33  	// ReadConfig takes a path and returns a LXCConfig.
    34  	// Returns an error if there is an error with the location of the config
    35  	// file, or there was an error parsing the file.
    36  	ReadConfig(path string) (LXCConfig, error)
    37  
    38  	// ReadCert takes a path and returns a raw certificate, there is no
    39  	// validation of the certificate.
    40  	// Returns an error if there is an error with the location of the
    41  	// certificate.
    42  	ReadCert(path string) ([]byte, error)
    43  }
    44  
    45  // LXCConfig represents a configuration setup of a LXC configuration file.
    46  // The LXCConfig expects the configuration file to be in a yaml representation.
    47  type LXCConfig struct {
    48  	DefaultRemote string                     `yaml:"local"`
    49  	Remotes       map[string]LXCRemoteConfig `yaml:"remotes"`
    50  }
    51  
    52  // LXCRemoteConfig defines a the remote servers of a LXC configuration.
    53  type LXCRemoteConfig struct {
    54  	Addr     string `yaml:"addr"`
    55  	Public   bool   `yaml:"public"`
    56  	Protocol string `yaml:"protocol"`
    57  	AuthType string `yaml:"auth_type"`
    58  }
    59  
    60  type environProvider struct {
    61  	environs.ProviderCredentials
    62  	environs.RequestFinalizeCredential
    63  	environs.ProviderCredentialsRegister
    64  	serverFactory   ServerFactory
    65  	lxcConfigReader LXCConfigReader
    66  	Clock           clock.Clock
    67  }
    68  
    69  var cloudSchema = &jsonschema.Schema{
    70  	Type:     []jsonschema.Type{jsonschema.ObjectType},
    71  	Required: []string{cloud.EndpointKey, cloud.AuthTypesKey},
    72  	Order:    []string{cloud.EndpointKey, cloud.AuthTypesKey, cloud.RegionsKey},
    73  	Properties: map[string]*jsonschema.Schema{
    74  		cloud.EndpointKey: {
    75  			Singular: "the API endpoint url for the remote LXD server",
    76  			Type:     []jsonschema.Type{jsonschema.StringType},
    77  			Format:   jsonschema.FormatURI,
    78  		},
    79  		cloud.AuthTypesKey: {
    80  			Singular:    "auth type",
    81  			Plural:      "auth types",
    82  			Type:        []jsonschema.Type{jsonschema.ArrayType},
    83  			UniqueItems: jsonschema.Bool(true),
    84  			Default:     string(cloud.CertificateAuthType),
    85  			Items: &jsonschema.ItemSpec{
    86  				Schemas: []*jsonschema.Schema{{
    87  					Type: []jsonschema.Type{jsonschema.StringType},
    88  					Enum: []interface{}{
    89  						string(cloud.CertificateAuthType),
    90  					},
    91  				}},
    92  			},
    93  		},
    94  		cloud.RegionsKey: {
    95  			Type:     []jsonschema.Type{jsonschema.ObjectType},
    96  			Singular: "region",
    97  			Plural:   "regions",
    98  			Default:  "default",
    99  			AdditionalProperties: &jsonschema.Schema{
   100  				Type:          []jsonschema.Type{jsonschema.ObjectType},
   101  				Required:      []string{cloud.EndpointKey},
   102  				MaxProperties: jsonschema.Int(1),
   103  				Properties: map[string]*jsonschema.Schema{
   104  					cloud.EndpointKey: {
   105  						Singular:      "the API endpoint url for the region",
   106  						Type:          []jsonschema.Type{jsonschema.StringType},
   107  						Format:        jsonschema.FormatURI,
   108  						Default:       "",
   109  						PromptDefault: "use cloud api url",
   110  					},
   111  				},
   112  			},
   113  		},
   114  	},
   115  }
   116  
   117  // NewProvider returns a new LXD EnvironProvider.
   118  func NewProvider() environs.CloudEnvironProvider {
   119  	configReader := lxcConfigReader{}
   120  	factory := NewServerFactory(NewHTTPClientFunc(func() *http.Client {
   121  		return jujuhttp.NewClient(
   122  			jujuhttp.WithLogger(logger.ChildWithLabels("http", corelogger.HTTP)),
   123  		).Client()
   124  	}))
   125  
   126  	credentials := environProviderCredentials{
   127  		certReadWriter:  certificateReadWriter{},
   128  		certGenerator:   certificateGenerator{},
   129  		serverFactory:   factory,
   130  		lxcConfigReader: configReader,
   131  	}
   132  	return &environProvider{
   133  		ProviderCredentials:         credentials,
   134  		RequestFinalizeCredential:   credentials,
   135  		ProviderCredentialsRegister: credentials,
   136  		serverFactory:               factory,
   137  		lxcConfigReader:             configReader,
   138  	}
   139  }
   140  
   141  // Version is part of the EnvironProvider interface.
   142  func (*environProvider) Version() int {
   143  	return 0
   144  }
   145  
   146  // Open implements environs.EnvironProvider.
   147  func (p *environProvider) Open(_ stdcontext.Context, args environs.OpenParams) (environs.Environ, error) {
   148  	if err := p.validateCloudSpec(args.Cloud); err != nil {
   149  		return nil, errors.Annotate(err, "validating cloud spec")
   150  	}
   151  	env, err := newEnviron(
   152  		p,
   153  		args.Cloud,
   154  		args.Config,
   155  	)
   156  	return env, errors.Trace(err)
   157  }
   158  
   159  // CloudSchema returns the schema used to validate input for add-cloud.
   160  func (p *environProvider) CloudSchema() *jsonschema.Schema {
   161  	return cloudSchema
   162  }
   163  
   164  // Ping tests the connection to the cloud, to verify the endpoint is valid.
   165  func (p *environProvider) Ping(ctx context.ProviderCallContext, endpoint string) error {
   166  	// if the endpoint is empty, then don't ping, as we can assume we're using
   167  	// local lxd
   168  	if endpoint == "" {
   169  		return nil
   170  	}
   171  
   172  	// Ensure the Port on the Host, if we get an error it is reasonable to
   173  	// assume that the address in the spec is invalid.
   174  	lxdEndpoint, err := lxd.EnsureHostPort(endpoint)
   175  	if err != nil {
   176  		return errors.Trace(err)
   177  	}
   178  
   179  	// Connect to the remote server anonymously so we can just verify it exists
   180  	// as we're not sure that the certificates are loaded in time for when the
   181  	// ping occurs i.e. interactive add-cloud
   182  	_, err = lxd.ConnectRemote(lxd.NewInsecureServerSpec(lxdEndpoint))
   183  	if err != nil {
   184  		return errors.Annotatef(err, "no lxd server running at %s", lxdEndpoint)
   185  	}
   186  	return nil
   187  }
   188  
   189  // PrepareConfig implements environs.EnvironProvider.
   190  func (p *environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) {
   191  	if err := p.validateCloudSpec(args.Cloud); err != nil {
   192  		return nil, errors.Annotate(err, "validating cloud spec")
   193  	}
   194  	// Set the default filesystem-storage source.
   195  	attrs := make(map[string]interface{})
   196  	if _, ok := args.Config.StorageDefaultFilesystemSource(); !ok {
   197  		attrs[config.StorageDefaultFilesystemSourceKey] = lxdStorageProviderType
   198  	}
   199  	if len(attrs) == 0 {
   200  		return args.Config, nil
   201  	}
   202  	cfg, err := args.Config.Apply(attrs)
   203  	return cfg, errors.Trace(err)
   204  }
   205  
   206  // Validate implements environs.EnvironProvider.
   207  func (*environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   208  	if _, err := newValidConfig(cfg); err != nil {
   209  		return nil, errors.Annotate(err, "invalid base config")
   210  	}
   211  	return cfg, nil
   212  }
   213  
   214  // DetectClouds implements environs.CloudDetector.
   215  func (p *environProvider) DetectClouds() ([]cloud.Cloud, error) {
   216  	usedNames := map[string]bool{lxdnames.ProviderType: true, lxdnames.DefaultCloud: true}
   217  	clouds := []cloud.Cloud{localhostCloud}
   218  	for _, dir := range configDirs() {
   219  		configPath := filepath.Join(dir, "config.yml")
   220  		config, err := p.lxcConfigReader.ReadConfig(configPath)
   221  		if err != nil {
   222  			if !strings.HasSuffix(err.Error(), "permission denied") {
   223  				logger.Warningf("unable to read/parse LXC config file (%s): %s", configPath, err)
   224  			}
   225  		}
   226  		for name, remote := range config.Remotes {
   227  			if remote.Protocol != lxdnames.ProviderType {
   228  				continue
   229  			}
   230  			if usedNames[name] {
   231  				logger.Warningf("ignoring ambigious cloud %s", name)
   232  				continue
   233  			}
   234  			usedNames[name] = true
   235  			clouds = append(clouds, cloud.Cloud{
   236  				Name:        name,
   237  				Type:        lxdnames.ProviderType,
   238  				Endpoint:    remote.Addr,
   239  				Description: "LXD Cluster",
   240  				AuthTypes: []cloud.AuthType{
   241  					cloud.CertificateAuthType,
   242  				},
   243  				Regions: []cloud.Region{{
   244  					Name:     lxdnames.DefaultRemoteRegion,
   245  					Endpoint: remote.Addr,
   246  				}},
   247  			})
   248  		}
   249  	}
   250  	return clouds, nil
   251  }
   252  
   253  // DetectCloud implements environs.CloudDetector.
   254  func (p *environProvider) DetectCloud(name string) (cloud.Cloud, error) {
   255  	clouds, err := p.DetectClouds()
   256  	if err != nil {
   257  		return cloud.Cloud{}, errors.Trace(err)
   258  	}
   259  	match := name
   260  	if match == lxdnames.DefaultCloudAltName {
   261  		match = lxdnames.DefaultCloud
   262  	}
   263  	for _, cloud := range clouds {
   264  		if cloud.Name == match {
   265  			return cloud, nil
   266  		}
   267  	}
   268  	return cloud.Cloud{}, errors.NotFoundf("cloud %s", name)
   269  }
   270  
   271  // FinalizeCloud is part of the environs.CloudFinalizer interface.
   272  func (p *environProvider) FinalizeCloud(
   273  	ctx environs.FinalizeCloudContext,
   274  	in cloud.Cloud,
   275  ) (cloud.Cloud, error) {
   276  	var endpoint string
   277  	resolveEndpoint := func(name string, ep *string) error {
   278  		// If the name doesn't equal "localhost" then we shouldn't resolve
   279  		// the end point, instead we should just accept what we already have.
   280  		if !lxdnames.IsDefaultCloud(name) || *ep != "" {
   281  			return nil
   282  		}
   283  		if endpoint == "" {
   284  			// The cloud endpoint is empty, which means
   285  			// that we should connect to the local LXD.
   286  			hostAddress, err := getLocalHostAddress(ctx, p.serverFactory)
   287  			if err != nil {
   288  				return errors.Trace(err)
   289  			}
   290  			endpoint = hostAddress
   291  		}
   292  		*ep = endpoint
   293  		return nil
   294  	}
   295  
   296  	// If any of the endpoints point to localhost, go through and backfill the
   297  	// cloud spec with local host addresses.
   298  	if err := resolveEndpoint(in.Name, &in.Endpoint); err != nil {
   299  		return cloud.Cloud{}, errors.Trace(err)
   300  	}
   301  	for i, k := range in.Regions {
   302  		if err := resolveEndpoint(k.Name, &in.Regions[i].Endpoint); err != nil {
   303  			return cloud.Cloud{}, errors.Trace(err)
   304  		}
   305  	}
   306  	// If the provider type is not named localhost and there is no region, set the
   307  	// region to be a default region
   308  	if !lxdnames.IsDefaultCloud(in.Name) && len(in.Regions) == 0 {
   309  		in.Regions = append(in.Regions, cloud.Region{
   310  			Name:     lxdnames.DefaultRemoteRegion,
   311  			Endpoint: in.Endpoint,
   312  		})
   313  	}
   314  	return in, nil
   315  }
   316  
   317  func getLocalHostAddress(ctx environs.FinalizeCloudContext, serverFactory ServerFactory) (string, error) {
   318  	svr, err := serverFactory.LocalServer()
   319  	if err != nil {
   320  		return "", errors.Trace(err)
   321  	}
   322  
   323  	bridgeName := svr.LocalBridgeName()
   324  	hostAddress, err := serverFactory.LocalServerAddress()
   325  	if err != nil {
   326  		return "", errors.Trace(err)
   327  	}
   328  	ctx.Verbosef(
   329  		"Resolved LXD host address on bridge %s: %s",
   330  		bridgeName, hostAddress,
   331  	)
   332  	return hostAddress, nil
   333  }
   334  
   335  // localhostCloud is the predefined "localhost" LXD cloud. We leave the
   336  // endpoints empty to indicate that LXD is on the local host. When the
   337  // cloud is finalized (see FinalizeCloud), we resolve the bridge address
   338  // of the LXD host, and use that as the endpoint.
   339  var localhostCloud = cloud.Cloud{
   340  	Name: lxdnames.DefaultCloud,
   341  	Type: lxdnames.ProviderType,
   342  	AuthTypes: []cloud.AuthType{
   343  		cloud.CertificateAuthType,
   344  	},
   345  	Endpoint: "",
   346  	Regions: []cloud.Region{{
   347  		Name:     lxdnames.DefaultLocalRegion,
   348  		Endpoint: "",
   349  	}},
   350  	Description: cloud.DefaultCloudDescription(lxdnames.ProviderType),
   351  }
   352  
   353  // DetectRegions implements environs.CloudRegionDetector.
   354  func (*environProvider) DetectRegions() ([]cloud.Region, error) {
   355  	// For now we just return a hard-coded "localhost" region,
   356  	// i.e. the local LXD daemon. We may later want to detect
   357  	// locally-configured remotes.
   358  	return []cloud.Region{{Name: lxdnames.DefaultLocalRegion}}, nil
   359  }
   360  
   361  // Schema returns the configuration schema for an environment.
   362  func (*environProvider) Schema() environschema.Fields {
   363  	fields, err := config.Schema(configSchema)
   364  	if err != nil {
   365  		panic(err)
   366  	}
   367  	return fields
   368  }
   369  
   370  func (p *environProvider) validateCloudSpec(spec environscloudspec.CloudSpec) error {
   371  	if err := spec.Validate(); err != nil {
   372  		return errors.Trace(err)
   373  	}
   374  	if spec.Credential == nil {
   375  		return errors.NotValidf("missing credential")
   376  	}
   377  
   378  	// Always validate the spec.Endpoint, to ensure that it's valid.
   379  	if _, err := endpointURL(spec.Endpoint); err != nil {
   380  		return errors.Trace(err)
   381  	}
   382  	switch authType := spec.Credential.AuthType(); authType {
   383  	case cloud.CertificateAuthType:
   384  		if spec.Credential == nil {
   385  			return errors.NotFoundf("credentials")
   386  		}
   387  		if _, _, ok := getCertificates(*spec.Credential); !ok {
   388  			return errors.NotValidf("certificate credentials")
   389  		}
   390  	default:
   391  		return errors.NotSupportedf("%q auth-type", authType)
   392  	}
   393  	return nil
   394  }
   395  
   396  // ConfigSchema returns extra config attributes specific
   397  // to this provider only.
   398  func (p *environProvider) ConfigSchema() schema.Fields {
   399  	return configFields
   400  }
   401  
   402  // ConfigDefaults returns the default values for the
   403  // provider specific config attributes.
   404  func (p *environProvider) ConfigDefaults() schema.Defaults {
   405  	return configDefaults
   406  }
   407  
   408  // lxcConfigReader is the default implementation for reading files from disk.
   409  type lxcConfigReader struct{}
   410  
   411  func (lxcConfigReader) ReadConfig(path string) (LXCConfig, error) {
   412  	configFile, err := os.ReadFile(path)
   413  	if err != nil {
   414  		if cause := errors.Cause(err); os.IsNotExist(cause) {
   415  			return LXCConfig{}, nil
   416  		}
   417  		return LXCConfig{}, errors.Trace(err)
   418  	}
   419  
   420  	var config LXCConfig
   421  	if err := yaml.Unmarshal(configFile, &config); err != nil {
   422  		return LXCConfig{}, errors.Trace(err)
   423  	}
   424  
   425  	return config, nil
   426  }
   427  
   428  func (lxcConfigReader) ReadCert(path string) ([]byte, error) {
   429  	certFile, err := os.ReadFile(path)
   430  	return certFile, errors.Trace(err)
   431  }