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

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package oci
     5  
     6  import (
     7  	stdcontext "context"
     8  	"fmt"
     9  	"net"
    10  	"os"
    11  	"strings"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/jsonschema"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/schema"
    18  	ociIdentity "github.com/oracle/oci-go-sdk/v65/identity"
    19  	"gopkg.in/ini.v1"
    20  	"gopkg.in/juju/environschema.v1"
    21  
    22  	"github.com/juju/juju/cloud"
    23  	"github.com/juju/juju/core/instance"
    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/oci/common"
    29  )
    30  
    31  var logger = loggo.GetLogger("juju.provider.oci")
    32  
    33  // EnvironProvider type implements environs.EnvironProvider interface
    34  type EnvironProvider struct{}
    35  
    36  type environConfig struct {
    37  	*config.Config
    38  	attrs map[string]interface{}
    39  }
    40  
    41  var _ config.ConfigSchemaSource = (*EnvironProvider)(nil)
    42  var _ environs.ProviderSchema = (*EnvironProvider)(nil)
    43  
    44  var configSchema = environschema.Fields{
    45  	"compartment-id": {
    46  		Description: "The OCID of the compartment in which juju has access to create resources.",
    47  		Type:        environschema.Tstring,
    48  	},
    49  	"address-space": {
    50  		Description: "The CIDR block to use when creating default subnets. The subnet must have at least a /16 size.",
    51  		Type:        environschema.Tstring,
    52  	},
    53  }
    54  
    55  var configDefaults = schema.Defaults{
    56  	"compartment-id": "",
    57  	"address-space":  DefaultAddressSpace,
    58  }
    59  
    60  var configFields = func() schema.Fields {
    61  	fs, _, err := configSchema.ValidationSchema()
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  	return fs
    66  }()
    67  
    68  // credentialSection holds the keys present in one section of the OCI
    69  // config file, as created by the OCI command line. This is only used
    70  // during credential detection
    71  type credentialSection struct {
    72  	User        string
    73  	Tenancy     string
    74  	KeyFile     string
    75  	PassPhrase  string
    76  	Fingerprint string
    77  }
    78  
    79  var credentialSchema = map[cloud.AuthType]cloud.CredentialSchema{
    80  	cloud.HTTPSigAuthType: {
    81  		{
    82  			"user", cloud.CredentialAttr{
    83  				Description: "Username OCID",
    84  			},
    85  		},
    86  		{
    87  			"tenancy", cloud.CredentialAttr{
    88  				Description: "Tenancy OCID",
    89  			},
    90  		},
    91  		{
    92  			"key", cloud.CredentialAttr{
    93  				Description: "PEM encoded private key",
    94  			},
    95  		},
    96  		{
    97  			"pass-phrase", cloud.CredentialAttr{
    98  				Description: "Passphrase used to unlock the key",
    99  				Hidden:      true,
   100  			},
   101  		},
   102  		{
   103  			"fingerprint", cloud.CredentialAttr{
   104  				Description: "Private key fingerprint",
   105  			},
   106  		},
   107  		// Deprecated, but still supported for backward compatibility
   108  		{
   109  			"region", cloud.CredentialAttr{
   110  				Description: "DEPRECATED: Region to log into",
   111  			},
   112  		},
   113  	},
   114  }
   115  
   116  func (p EnvironProvider) newConfig(cfg *config.Config) (*environConfig, error) {
   117  	if cfg == nil {
   118  		return nil, errors.New("cannot set config on uninitialized env")
   119  	}
   120  
   121  	valid, err := p.Validate(cfg, nil)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	return &environConfig{valid, valid.UnknownAttrs()}, nil
   126  }
   127  
   128  func (c *environConfig) compartmentID() *string {
   129  	compartmentID := c.attrs["compartment-id"].(string)
   130  	if compartmentID == "" {
   131  		return nil
   132  	}
   133  	return &compartmentID
   134  }
   135  
   136  func (c *environConfig) addressSpace() *string {
   137  	addressSpace := c.attrs["address-space"].(string)
   138  	if addressSpace == "" {
   139  		addressSpace = DefaultAddressSpace
   140  	}
   141  	return &addressSpace
   142  }
   143  
   144  // Schema implements environs.ProviderSchema
   145  func (o *EnvironProvider) Schema() environschema.Fields {
   146  	fields, err := config.Schema(configSchema)
   147  	if err != nil {
   148  		panic(err)
   149  	}
   150  	return fields
   151  }
   152  
   153  // ConfigSchema implements config.ConfigSchemaSource
   154  func (o *EnvironProvider) ConfigSchema() schema.Fields {
   155  	return configFields
   156  }
   157  
   158  // ConfigDefaults implements config.ConfigSchemaSource
   159  func (o *EnvironProvider) ConfigDefaults() schema.Defaults {
   160  	return configDefaults
   161  }
   162  
   163  // Version implements environs.EnvironProvider.
   164  func (e EnvironProvider) Version() int {
   165  	return 1
   166  }
   167  
   168  // CloudSchema implements environs.EnvironProvider.
   169  func (e EnvironProvider) CloudSchema() *jsonschema.Schema {
   170  	return nil
   171  }
   172  
   173  // Ping implements environs.EnvironProvider.
   174  func (e *EnvironProvider) Ping(ctx context.ProviderCallContext, endpoint string) error {
   175  	return errors.NotImplementedf("Ping")
   176  }
   177  
   178  func validateCloudSpec(c environscloudspec.CloudSpec) error {
   179  	if err := c.Validate(); err != nil {
   180  		return errors.Trace(err)
   181  	}
   182  	if c.Credential == nil {
   183  		return errors.NotValidf("missing credential")
   184  	}
   185  	if authType := c.Credential.AuthType(); authType != cloud.HTTPSigAuthType {
   186  		return errors.NotSupportedf("%q auth-type", authType)
   187  	}
   188  	return nil
   189  }
   190  
   191  // PrepareConfig implements environs.EnvironProvider.
   192  func (e EnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) {
   193  	if err := validateCloudSpec(args.Cloud); err != nil {
   194  		return nil, errors.Annotate(err, "validating cloud spec")
   195  	}
   196  	// TODO(gsamfira): Set default block storage backend
   197  	return args.Config, nil
   198  }
   199  
   200  // Open implements environs.EnvironProvider.
   201  func (e *EnvironProvider) Open(_ stdcontext.Context, params environs.OpenParams) (environs.Environ, error) {
   202  	logger.Infof("opening model %q", params.Config.Name())
   203  
   204  	if err := validateCloudSpec(params.Cloud); err != nil {
   205  		return nil, errors.Trace(err)
   206  	}
   207  
   208  	creds := params.Cloud.Credential.Attributes()
   209  	providerConfig := common.JujuConfigProvider{
   210  		Key:         []byte(creds["key"]),
   211  		Fingerprint: creds["fingerprint"],
   212  		Passphrase:  creds["pass-phrase"],
   213  		Tenancy:     creds["tenancy"],
   214  		User:        creds["user"],
   215  		OCIRegion:   params.Cloud.Region,
   216  	}
   217  	// We don't support setting a default region in the credentials anymore. Because, such approach conflicts with the
   218  	// way we handle regions in Juju.
   219  	if creds["region"] != "" {
   220  		logger.Warningf("Setting a default region in Oracle Cloud credentials is not supported.")
   221  	}
   222  	err := providerConfig.Validate()
   223  	if err != nil {
   224  		return nil, errors.Trace(err)
   225  	}
   226  	compute, err := common.NewComputeClient(providerConfig)
   227  	if err != nil {
   228  		return nil, errors.Trace(err)
   229  	}
   230  
   231  	networking, err := common.NewNetworkClient(providerConfig)
   232  	if err != nil {
   233  		return nil, errors.Trace(err)
   234  	}
   235  
   236  	storage, err := common.NewStorageClient(providerConfig)
   237  	if err != nil {
   238  		return nil, errors.Trace(err)
   239  	}
   240  
   241  	identity, err := ociIdentity.NewIdentityClientWithConfigurationProvider(providerConfig)
   242  	if err != nil {
   243  		return nil, errors.Trace(err)
   244  	}
   245  
   246  	env := &Environ{
   247  		Compute:    compute,
   248  		Networking: networking,
   249  		Storage:    storage,
   250  		Firewall:   networking,
   251  		Identity:   identity,
   252  		ociConfig:  providerConfig,
   253  		clock:      clock.WallClock,
   254  		p:          e,
   255  	}
   256  
   257  	if err := env.SetConfig(params.Config); err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	env.namespace, err = instance.NewNamespace(env.Config().UUID())
   262  	if err != nil {
   263  		return nil, errors.Trace(err)
   264  	}
   265  
   266  	cfg := env.ecfg()
   267  	if cfg.compartmentID() == nil {
   268  		return nil, errors.New("compartment-id may not be empty")
   269  	}
   270  
   271  	addressSpace := cfg.addressSpace()
   272  	if _, ipNET, err := net.ParseCIDR(*addressSpace); err == nil {
   273  		size, _ := ipNET.Mask.Size()
   274  		if size > 16 {
   275  			return nil, errors.Errorf("configured subnet (%q) is not large enough. Please use a prefix length in the range /8 to /16. Current prefix length is /%d", *addressSpace, size)
   276  		}
   277  	} else {
   278  		return nil, errors.Trace(err)
   279  	}
   280  
   281  	return env, nil
   282  }
   283  
   284  // CredentialSchemas implements environs.ProviderCredentials.
   285  func (e EnvironProvider) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema {
   286  	return credentialSchema
   287  }
   288  
   289  // DetectCredentials implements environs.ProviderCredentials.
   290  // Configuration options for the OCI SDK are detailed here:
   291  // https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm
   292  func (e EnvironProvider) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) {
   293  	result := cloud.CloudCredential{
   294  		AuthCredentials: make(map[string]cloud.Credential),
   295  	}
   296  	cfg_file, err := ociConfigFile()
   297  	if err != nil {
   298  		if os.IsNotExist(errors.Cause(err)) {
   299  			return &result, nil
   300  		}
   301  		return nil, errors.Trace(err)
   302  	}
   303  
   304  	cfg, err := ini.LooseLoad(cfg_file)
   305  	if err != nil {
   306  		return nil, errors.Trace(err)
   307  	}
   308  	cfg.NameMapper = ini.TitleUnderscore
   309  
   310  	for _, val := range cfg.SectionStrings() {
   311  		values := new(credentialSection)
   312  		if err := cfg.Section(val).MapTo(values); err != nil {
   313  			logger.Warningf("invalid value in section %s: %s", val, err)
   314  			continue
   315  		}
   316  		missingFields := []string{}
   317  		if values.User == "" {
   318  			missingFields = append(missingFields, "user")
   319  		}
   320  
   321  		if values.Tenancy == "" {
   322  			missingFields = append(missingFields, "tenancy")
   323  		}
   324  
   325  		if values.KeyFile == "" {
   326  			missingFields = append(missingFields, "key_file")
   327  		}
   328  
   329  		if values.Fingerprint == "" {
   330  			missingFields = append(missingFields, "fingerprint")
   331  		}
   332  
   333  		if len(missingFields) > 0 {
   334  			logger.Warningf("missing required field(s) in section %s: %s", val, strings.Join(missingFields, ", "))
   335  			continue
   336  		}
   337  
   338  		pemFileContent, err := os.ReadFile(values.KeyFile)
   339  		if err != nil {
   340  			return nil, errors.Trace(err)
   341  		}
   342  		if err := common.ValidateKey(pemFileContent, values.PassPhrase); err != nil {
   343  			logger.Warningf("failed to decrypt PEM %s using the configured pass phrase", values.KeyFile)
   344  			continue
   345  		}
   346  
   347  		httpSigCreds := cloud.NewCredential(
   348  			cloud.HTTPSigAuthType,
   349  			map[string]string{
   350  				"user":        values.User,
   351  				"tenancy":     values.Tenancy,
   352  				"key":         string(pemFileContent),
   353  				"pass-phrase": values.PassPhrase,
   354  				"fingerprint": values.Fingerprint,
   355  			},
   356  		)
   357  		httpSigCreds.Label = fmt.Sprintf("OCI credential %q", val)
   358  		result.AuthCredentials[val] = httpSigCreds
   359  	}
   360  	if len(result.AuthCredentials) == 0 {
   361  		return nil, errors.NotFoundf("OCI credentials")
   362  	}
   363  	return &result, nil
   364  }
   365  
   366  // FinalizeCredential implements environs.ProviderCredentials.
   367  func (e EnvironProvider) FinalizeCredential(
   368  	ctx environs.FinalizeCredentialContext,
   369  	params environs.FinalizeCredentialParams) (*cloud.Credential, error) {
   370  
   371  	return &params.Credential, nil
   372  }
   373  
   374  // Validate implements config.Validator.
   375  func (e EnvironProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   376  	if err := config.Validate(cfg, old); err != nil {
   377  		return nil, err
   378  	}
   379  	newAttrs, err := cfg.ValidateUnknownAttrs(
   380  		configFields, configDefaults,
   381  	)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	return cfg.Apply(newAttrs)
   387  }