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