github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/provider/gce/config.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gce
     5  
     6  import (
     7  	"os"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/schema"
    11  
    12  	"github.com/juju/juju/environs/config"
    13  	"github.com/juju/juju/provider/gce/google"
    14  )
    15  
    16  // TODO(ericsnow) While not strictly config-related, we could use some
    17  // mechanism by which we can validate the values we've hard-coded in
    18  // this provider match up with the external authoritative sources. One
    19  // example of this is the data stored in instancetypes.go. Similarly
    20  // we should also ensure the cloud-images metadata is correct and
    21  // up-to-date, though that is more the responsibility of that team.
    22  // Regardless, it may be useful to include a tool somewhere in juju
    23  // that we can use to validate this provider's potentially out-of-date
    24  // data.
    25  
    26  // The GCE-specific config keys.
    27  const (
    28  	cfgAuthFile      = "auth-file"
    29  	cfgPrivateKey    = "private-key"
    30  	cfgClientID      = "client-id"
    31  	cfgClientEmail   = "client-email"
    32  	cfgRegion        = "region"
    33  	cfgProjectID     = "project-id"
    34  	cfgImageEndpoint = "image-endpoint"
    35  )
    36  
    37  // boilerplateConfig will be shown in help output, so please keep it up to
    38  // date when you change environment configuration below.
    39  var boilerplateConfig = `
    40  gce:
    41    type: gce
    42  
    43    # Google Auth Info
    44    # The GCE provider uses OAuth to authenticate. This requires that
    45    # you set it up and get the relevant credentials. For more information
    46    # see https://cloud.google.com/compute/docs/api/how-tos/authorization.
    47    # The key information can be downloaded as a JSON file, or copied, from:
    48    #   https://console.developers.google.com/project/<projet>/apiui/credential
    49    # Either set the path to the downloaded JSON file here:
    50    auth-file:
    51  
    52    # ...or set the individual fields for the credentials. Either way, all
    53    # three of these are required and have specific meaning to GCE.
    54    # private-key:
    55    # client-email:
    56    # client-id:
    57  
    58    # Google instance info
    59    # To provision instances and perform related operations, the provider
    60    # will need to know which GCE project to use and into which region to
    61    # provision. While the region has a default, the project ID is
    62    # required. For information on the project ID, see
    63    # https://cloud.google.com/compute/docs/projects and regarding regions
    64    # see https://cloud.google.com/compute/docs/zones.
    65    project-id:
    66    # region: us-central1
    67  
    68    # The GCE provider uses pre-built images when provisioning instances.
    69    # You can customize the location in which to find them with the
    70    # image-endpoint setting. The default value is the a location within
    71    # GCE, so it will give you the best speed when bootstrapping or adding
    72    # machines. For more information on the image cache see
    73    # https://cloud-images.ubuntu.com/.
    74    # image-endpoint: https://www.googleapis.com
    75  `[1:]
    76  
    77  // configFields is the spec for each GCE config value's type.
    78  var configFields = schema.Fields{
    79  	cfgAuthFile:      schema.String(),
    80  	cfgPrivateKey:    schema.String(),
    81  	cfgClientID:      schema.String(),
    82  	cfgClientEmail:   schema.String(),
    83  	cfgRegion:        schema.String(),
    84  	cfgProjectID:     schema.String(),
    85  	cfgImageEndpoint: schema.String(),
    86  }
    87  
    88  // TODO(ericsnow) Do we need custom defaults for "image-metadata-url" or
    89  // "agent-metadata-url"? The defaults are the official ones (e.g.
    90  // cloud-images).
    91  
    92  var configDefaults = schema.Defaults{
    93  	cfgAuthFile: "",
    94  	// See http://cloud-images.ubuntu.com/releases/streams/v1/com.ubuntu.cloud:released:gce.json
    95  	cfgImageEndpoint: "https://www.googleapis.com",
    96  	cfgRegion:        "us-central1",
    97  }
    98  
    99  var configSecretFields = []string{
   100  	cfgPrivateKey,
   101  }
   102  
   103  var configImmutableFields = []string{
   104  	cfgAuthFile,
   105  	cfgPrivateKey,
   106  	cfgClientID,
   107  	cfgClientEmail,
   108  	cfgRegion,
   109  	cfgProjectID,
   110  	cfgImageEndpoint,
   111  }
   112  
   113  var configAuthFields = []string{
   114  	cfgPrivateKey,
   115  	cfgClientID,
   116  	cfgClientEmail,
   117  }
   118  
   119  // osEnvFields is the mapping from GCE env vars to config keys.
   120  var osEnvFields = map[string]string{
   121  	google.OSEnvPrivateKey:    cfgPrivateKey,
   122  	google.OSEnvClientID:      cfgClientID,
   123  	google.OSEnvClientEmail:   cfgClientEmail,
   124  	google.OSEnvRegion:        cfgRegion,
   125  	google.OSEnvProjectID:     cfgProjectID,
   126  	google.OSEnvImageEndpoint: cfgImageEndpoint,
   127  }
   128  
   129  // handleInvalidField converts a google.InvalidConfigValue into a new
   130  // error, translating a {provider/gce/google}.OSEnvVar* value into a
   131  // GCE config key in the new error.
   132  func handleInvalidField(err error) error {
   133  	vErr := err.(*google.InvalidConfigValue)
   134  	if strValue, ok := vErr.Value.(string); ok && strValue == "" {
   135  		key := osEnvFields[vErr.Key]
   136  		return errors.Errorf("%s: must not be empty", key)
   137  	}
   138  	return err
   139  }
   140  
   141  type environConfig struct {
   142  	*config.Config
   143  	attrs       map[string]interface{}
   144  	credentials *google.Credentials
   145  }
   146  
   147  // newConfig builds a new environConfig from the provided Config and
   148  // returns it.
   149  func newConfig(cfg *config.Config) *environConfig {
   150  	return &environConfig{
   151  		Config: cfg,
   152  		attrs:  cfg.UnknownAttrs(),
   153  	}
   154  }
   155  
   156  // newValidConfig builds a new environConfig from the provided Config
   157  // and returns it. This includes applying the provided defaults
   158  // values, if any. The resulting config values are validated.
   159  func newValidConfig(cfg *config.Config, defaults map[string]interface{}) (*environConfig, error) {
   160  	credentials, err := parseCredentials(cfg)
   161  	if err != nil {
   162  		return nil, errors.Trace(err)
   163  	}
   164  	handled, err := applyCredentials(cfg, credentials)
   165  	if err != nil {
   166  		return nil, errors.Trace(err)
   167  	}
   168  	cfg = handled
   169  
   170  	// Ensure that the provided config is valid.
   171  	if err := config.Validate(cfg, nil); err != nil {
   172  		return nil, errors.Trace(err)
   173  	}
   174  
   175  	// Apply the defaults and coerce/validate the custom config attrs.
   176  	validated, err := cfg.ValidateUnknownAttrs(configFields, defaults)
   177  	if err != nil {
   178  		return nil, errors.Trace(err)
   179  	}
   180  	validCfg, err := cfg.Apply(validated)
   181  	if err != nil {
   182  		return nil, errors.Trace(err)
   183  	}
   184  
   185  	// Build the config.
   186  	ecfg := newConfig(validCfg)
   187  	ecfg.credentials = credentials
   188  
   189  	// Do final validation.
   190  	if err := ecfg.validate(); err != nil {
   191  		return nil, errors.Trace(err)
   192  	}
   193  
   194  	return ecfg, nil
   195  }
   196  
   197  func (c *environConfig) authFile() string {
   198  	if c.attrs[cfgAuthFile] == nil {
   199  		return ""
   200  	}
   201  	return c.attrs[cfgAuthFile].(string)
   202  }
   203  
   204  func (c *environConfig) privateKey() string {
   205  	return c.attrs[cfgPrivateKey].(string)
   206  }
   207  
   208  func (c *environConfig) clientID() string {
   209  	return c.attrs[cfgClientID].(string)
   210  }
   211  
   212  func (c *environConfig) clientEmail() string {
   213  	return c.attrs[cfgClientEmail].(string)
   214  }
   215  
   216  func (c *environConfig) region() string {
   217  	return c.attrs[cfgRegion].(string)
   218  }
   219  
   220  func (c *environConfig) projectID() string {
   221  	return c.attrs[cfgProjectID].(string)
   222  }
   223  
   224  // imageEndpoint identifies where the provider should look for
   225  // cloud images (i.e. for simplestreams).
   226  func (c *environConfig) imageEndpoint() string {
   227  	return c.attrs[cfgImageEndpoint].(string)
   228  }
   229  
   230  // auth build a new Credentials based on the config and returns it.
   231  func (c *environConfig) auth() *google.Credentials {
   232  	if c.credentials == nil {
   233  		c.credentials = &google.Credentials{
   234  			ClientID:    c.clientID(),
   235  			ClientEmail: c.clientEmail(),
   236  			PrivateKey:  []byte(c.privateKey()),
   237  		}
   238  	}
   239  	return c.credentials
   240  }
   241  
   242  // newConnection build a ConnectionConfig based on the config and returns it.
   243  func (c *environConfig) newConnection() google.ConnectionConfig {
   244  	return google.ConnectionConfig{
   245  		Region:    c.region(),
   246  		ProjectID: c.projectID(),
   247  	}
   248  }
   249  
   250  // secret gathers the "secret" config values and returns them.
   251  func (c *environConfig) secret() map[string]string {
   252  	secretAttrs := make(map[string]string, len(configSecretFields))
   253  	for _, key := range configSecretFields {
   254  		secretAttrs[key] = c.attrs[key].(string)
   255  	}
   256  	return secretAttrs
   257  }
   258  
   259  // validate checks GCE-specific config values.
   260  func (c environConfig) validate() error {
   261  	// All fields must be populated, even with just the default.
   262  	for field := range configFields {
   263  		if dflt, ok := configDefaults[field]; ok && dflt == "" {
   264  			continue
   265  		}
   266  		if c.attrs[field].(string) == "" {
   267  			return errors.Errorf("%s: must not be empty", field)
   268  		}
   269  	}
   270  
   271  	// Check sanity of GCE fields.
   272  	if err := c.auth().Validate(); err != nil {
   273  		return errors.Trace(handleInvalidField(err))
   274  	}
   275  	if err := c.newConnection().Validate(); err != nil {
   276  		return errors.Trace(handleInvalidField(err))
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  // update applies changes from the provided config to the env config.
   283  // Changes to any immutable attributes result in an error.
   284  func (c *environConfig) update(cfg *config.Config) error {
   285  	// Validate the updates. newValidConfig does not modify the "known"
   286  	// config attributes so it is safe to call Validate here first.
   287  	if err := config.Validate(cfg, c.Config); err != nil {
   288  		return errors.Trace(err)
   289  	}
   290  
   291  	updates, err := newValidConfig(cfg, configDefaults)
   292  	if err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  
   296  	// Check that no immutable fields have changed.
   297  	attrs := updates.UnknownAttrs()
   298  	for _, field := range configImmutableFields {
   299  		if attrs[field] != c.attrs[field] {
   300  			return errors.Errorf("%s: cannot change from %v to %v", field, c.attrs[field], attrs[field])
   301  		}
   302  	}
   303  
   304  	// Apply the updates.
   305  	c.Config = cfg
   306  	c.attrs = cfg.UnknownAttrs()
   307  	return nil
   308  }
   309  
   310  // parseCredentials extracts the OAuth2 info from the config from the
   311  // individual fields (falling back on the JSON file).
   312  func parseCredentials(cfg *config.Config) (*google.Credentials, error) {
   313  	attrs := cfg.UnknownAttrs()
   314  
   315  	// Try the auth fields first.
   316  	values := make(map[string]string)
   317  	for _, field := range configAuthFields {
   318  		if existing, ok := attrs[field].(string); ok && existing != "" {
   319  			for key, candidate := range osEnvFields {
   320  				if field == candidate {
   321  					values[key] = existing
   322  					break
   323  				}
   324  			}
   325  		}
   326  	}
   327  	if len(values) > 0 {
   328  		creds, err := google.NewCredentials(values)
   329  		if err != nil {
   330  			return nil, errors.Trace(err)
   331  		}
   332  		return creds, nil
   333  	}
   334  
   335  	// Fall back to the auth file.
   336  	filename, ok := attrs[cfgAuthFile].(string)
   337  	if !ok || filename == "" {
   338  		// The missing credentials will be caught later.
   339  		return nil, nil
   340  	}
   341  	authFile, err := os.Open(filename)
   342  	if err != nil {
   343  		return nil, errors.Trace(err)
   344  	}
   345  	defer authFile.Close()
   346  	creds, err := google.ParseJSONKey(authFile)
   347  	if err != nil {
   348  		return nil, errors.Trace(err)
   349  	}
   350  	return creds, nil
   351  }
   352  
   353  func applyCredentials(cfg *config.Config, creds *google.Credentials) (*config.Config, error) {
   354  	updates := make(map[string]interface{})
   355  	for k, v := range creds.Values() {
   356  		if v == "" {
   357  			continue
   358  		}
   359  		if field, ok := osEnvFields[k]; ok {
   360  			for _, authField := range configAuthFields {
   361  				if field == authField {
   362  					updates[field] = v
   363  					break
   364  				}
   365  			}
   366  		}
   367  	}
   368  	updated, err := cfg.Apply(updates)
   369  	if err != nil {
   370  		return nil, errors.Trace(err)
   371  	}
   372  	return updated, nil
   373  }