github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/azure/config.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure
     5  
     6  import (
     7  	"net/url"
     8  	"strings"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure"
    11  	"github.com/Azure/azure-sdk-for-go/arm/storage"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/names"
    14  	"github.com/juju/schema"
    15  
    16  	"github.com/juju/juju/environs/config"
    17  )
    18  
    19  const (
    20  	configAttrAppId              = "application-id"
    21  	configAttrSubscriptionId     = "subscription-id"
    22  	configAttrTenantId           = "tenant-id"
    23  	configAttrAppPassword        = "application-password"
    24  	configAttrLocation           = "location"
    25  	configAttrEndpoint           = "endpoint"
    26  	configAttrStorageEndpoint    = "storage-endpoint"
    27  	configAttrStorageAccountType = "storage-account-type"
    28  
    29  	// The below bits are internal book-keeping things, rather than
    30  	// configuration. Config is just what we have to work with.
    31  
    32  	// configAttrStorageAccount is the name of the storage account. We
    33  	// can't just use a well-defined name for the storage acocunt because
    34  	// storage account names must be globally unique; each storage account
    35  	// has an associated public DNS entry.
    36  	configAttrStorageAccount = "storage-account"
    37  
    38  	// configAttrStorageAccountKey is the primary key for the storage
    39  	// account.
    40  	configAttrStorageAccountKey = "storage-account-key"
    41  
    42  	// resourceNameLengthMax is the maximum length of resource
    43  	// names in Azure.
    44  	resourceNameLengthMax = 80
    45  )
    46  
    47  var configFields = schema.Fields{
    48  	configAttrLocation:           schema.String(),
    49  	configAttrEndpoint:           schema.String(),
    50  	configAttrStorageEndpoint:    schema.String(),
    51  	configAttrAppId:              schema.String(),
    52  	configAttrSubscriptionId:     schema.String(),
    53  	configAttrTenantId:           schema.String(),
    54  	configAttrAppPassword:        schema.String(),
    55  	configAttrStorageAccount:     schema.String(),
    56  	configAttrStorageAccountKey:  schema.String(),
    57  	configAttrStorageAccountType: schema.String(),
    58  }
    59  
    60  var configDefaults = schema.Defaults{
    61  	configAttrStorageAccount:     schema.Omit,
    62  	configAttrStorageAccountKey:  schema.Omit,
    63  	configAttrStorageAccountType: string(storage.StandardLRS),
    64  }
    65  
    66  var requiredConfigAttributes = []string{
    67  	configAttrAppId,
    68  	configAttrAppPassword,
    69  	configAttrSubscriptionId,
    70  	configAttrTenantId,
    71  	configAttrLocation,
    72  	configAttrEndpoint,
    73  	configAttrStorageEndpoint,
    74  }
    75  
    76  var immutableConfigAttributes = []string{
    77  	configAttrSubscriptionId,
    78  	configAttrTenantId,
    79  	configAttrStorageAccount,
    80  	configAttrStorageAccountType,
    81  }
    82  
    83  var internalConfigAttributes = []string{
    84  	configAttrStorageAccount,
    85  	configAttrStorageAccountKey,
    86  }
    87  
    88  type azureModelConfig struct {
    89  	*config.Config
    90  	token              *azure.ServicePrincipalToken
    91  	subscriptionId     string
    92  	location           string // canonicalized
    93  	endpoint           string
    94  	storageEndpoint    string
    95  	storageAccount     string
    96  	storageAccountKey  string
    97  	storageAccountType storage.AccountType
    98  }
    99  
   100  var knownStorageAccountTypes = []string{
   101  	"Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Standard_ZRS", "Premium_LRS",
   102  }
   103  
   104  // Validate ensures that the provided configuration is valid for this
   105  // provider, and that changes between the old (if provided) and new
   106  // configurations are valid.
   107  func (*azureEnvironProvider) Validate(newCfg, oldCfg *config.Config) (*config.Config, error) {
   108  	_, err := validateConfig(newCfg, oldCfg)
   109  	if err != nil {
   110  		return nil, errors.Trace(err)
   111  	}
   112  	return newCfg, nil
   113  }
   114  
   115  func validateConfig(newCfg, oldCfg *config.Config) (*azureModelConfig, error) {
   116  	err := config.Validate(newCfg, oldCfg)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	validated, err := newCfg.ValidateUnknownAttrs(configFields, configDefaults)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	// Ensure required configuration is provided.
   127  	for _, key := range requiredConfigAttributes {
   128  		if value, ok := validated[key].(string); !ok || value == "" {
   129  			return nil, errors.Errorf("%q config not specified", key)
   130  		}
   131  	}
   132  	if oldCfg != nil {
   133  		// Ensure immutable configuration isn't changed.
   134  		oldUnknownAttrs := oldCfg.UnknownAttrs()
   135  		for _, key := range immutableConfigAttributes {
   136  			oldValue, hadValue := oldUnknownAttrs[key].(string)
   137  			if hadValue {
   138  				newValue, haveValue := validated[key].(string)
   139  				if !haveValue {
   140  					return nil, errors.Errorf(
   141  						"cannot remove immutable %q config", key,
   142  					)
   143  				}
   144  				if newValue != oldValue {
   145  					return nil, errors.Errorf(
   146  						"cannot change immutable %q config (%v -> %v)",
   147  						key, oldValue, newValue,
   148  					)
   149  				}
   150  			}
   151  			// It's valid to go from not having to having.
   152  		}
   153  		// TODO(axw) figure out how we intend to handle changing
   154  		// secrets, such as application key
   155  	}
   156  
   157  	// Resource group names must not exceed 80 characters. Resource group
   158  	// names are based on the model UUID and model name, the latter of
   159  	// which the model creator controls.
   160  	modelTag := names.NewModelTag(newCfg.UUID())
   161  	resourceGroup := resourceGroupName(modelTag, newCfg.Name())
   162  	if n := len(resourceGroup); n > resourceNameLengthMax {
   163  		smallestResourceGroup := resourceGroupName(modelTag, "")
   164  		return nil, errors.Errorf(`resource group name %q is too long
   165  
   166  Please choose a model name of no more than %d characters.`,
   167  			resourceGroup,
   168  			resourceNameLengthMax-len(smallestResourceGroup),
   169  		)
   170  	}
   171  
   172  	location := canonicalLocation(validated[configAttrLocation].(string))
   173  	endpoint := validated[configAttrEndpoint].(string)
   174  	storageEndpoint := validated[configAttrStorageEndpoint].(string)
   175  	appId := validated[configAttrAppId].(string)
   176  	subscriptionId := validated[configAttrSubscriptionId].(string)
   177  	tenantId := validated[configAttrTenantId].(string)
   178  	appPassword := validated[configAttrAppPassword].(string)
   179  	storageAccount, _ := validated[configAttrStorageAccount].(string)
   180  	storageAccountKey, _ := validated[configAttrStorageAccountKey].(string)
   181  	storageAccountType := validated[configAttrStorageAccountType].(string)
   182  
   183  	if newCfg.FirewallMode() == config.FwGlobal {
   184  		// We do not currently support the "global" firewall mode.
   185  		return nil, errNoFwGlobal
   186  	}
   187  
   188  	if !isKnownStorageAccountType(storageAccountType) {
   189  		return nil, errors.Errorf(
   190  			"invalid storage account type %q, expected one of: %q",
   191  			storageAccountType, knownStorageAccountTypes,
   192  		)
   193  	}
   194  
   195  	// The Azure storage code wants the endpoint host only, not the URL.
   196  	storageEndpointURL, err := url.Parse(storageEndpoint)
   197  	if err != nil {
   198  		return nil, errors.Annotate(err, "parsing storage endpoint URL")
   199  	}
   200  
   201  	token, err := azure.NewServicePrincipalToken(
   202  		appId, appPassword, tenantId,
   203  		azure.AzureResourceManagerScope,
   204  	)
   205  	if err != nil {
   206  		return nil, errors.Annotate(err, "constructing service principal token")
   207  	}
   208  
   209  	azureConfig := &azureModelConfig{
   210  		newCfg,
   211  		token,
   212  		subscriptionId,
   213  		location,
   214  		endpoint,
   215  		storageEndpointURL.Host,
   216  		storageAccount,
   217  		storageAccountKey,
   218  		storage.AccountType(storageAccountType),
   219  	}
   220  
   221  	return azureConfig, nil
   222  }
   223  
   224  // isKnownStorageAccountType reports whether or not the given string identifies
   225  // a known storage account type.
   226  func isKnownStorageAccountType(t string) bool {
   227  	for _, knownStorageAccountType := range knownStorageAccountTypes {
   228  		if t == knownStorageAccountType {
   229  			return true
   230  		}
   231  	}
   232  	return false
   233  }
   234  
   235  // canonicalLocation returns the canonicalized location string. This involves
   236  // stripping whitespace, and lowercasing. The ARM APIs do not support embedded
   237  // whitespace, whereas the old Service Management APIs used to; we allow the
   238  // user to provide either, and canonicalize them to one form that ARM allows.
   239  func canonicalLocation(s string) string {
   240  	s = strings.Replace(s, " ", "", -1)
   241  	return strings.ToLower(s)
   242  }