github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names/v5"
    13  	"github.com/juju/schema"
    14  	"gopkg.in/juju/environschema.v1"
    15  
    16  	"github.com/juju/juju/environs/config"
    17  )
    18  
    19  const (
    20  	// configAttrLoadBalancerSkuName mirrors the LoadBalancerSkuName type in the Azure SDK
    21  	configAttrLoadBalancerSkuName = "load-balancer-sku-name"
    22  
    23  	// configAttrResourceGroupName specifies an existing resource group to use
    24  	// rather than Juju creating it.
    25  	configAttrResourceGroupName = "resource-group-name"
    26  
    27  	// configNetwork is the virtual network each machine's primary NIC
    28  	// is attached to.
    29  	configAttrNetwork = "network"
    30  
    31  	// The below bits are internal book-keeping things, rather than
    32  	// configuration. Config is just what we have to work with.
    33  
    34  	// resourceNameLengthMax is the maximum length of resource
    35  	// names in Azure.
    36  	resourceNameLengthMax = 80
    37  )
    38  
    39  var configSchema = environschema.Fields{
    40  	configAttrLoadBalancerSkuName: {
    41  		Description: "mirrors the LoadBalancerSkuName type in the Azure SDK",
    42  		Type:        environschema.Tstring,
    43  		Mandatory:   true,
    44  	},
    45  	configAttrResourceGroupName: {
    46  		Description: "If set, use the specified resource group for all model artefacts instead of creating one based on the model UUID.",
    47  		Type:        environschema.Tstring,
    48  		Immutable:   true,
    49  	},
    50  	configAttrNetwork: {
    51  		Description: "If set, use the specified virtual network for all model machines instead of creating one.",
    52  		Type:        environschema.Tstring,
    53  		Immutable:   true,
    54  	},
    55  }
    56  
    57  var configDefaults = schema.Defaults{
    58  	configAttrLoadBalancerSkuName: string(armnetwork.LoadBalancerSKUNameStandard),
    59  	configAttrResourceGroupName:   schema.Omit,
    60  	configAttrNetwork:             schema.Omit,
    61  }
    62  
    63  // Schema returns the configuration schema for an environment.
    64  func (azureEnvironProvider) Schema() environschema.Fields {
    65  	fields, err := config.Schema(configSchema)
    66  	if err != nil {
    67  		panic(err)
    68  	}
    69  	return fields
    70  }
    71  
    72  // ConfigSchema returns extra config attributes specific
    73  // to this provider only.
    74  func (p azureEnvironProvider) ConfigSchema() schema.Fields {
    75  	return configFields
    76  }
    77  
    78  // ConfigDefaults returns the default values for the
    79  // provider specific config attributes.
    80  func (p azureEnvironProvider) ConfigDefaults() schema.Defaults {
    81  	return configDefaults
    82  }
    83  
    84  var configFields = func() schema.Fields {
    85  	fs, _, err := configSchema.ValidationSchema()
    86  	if err != nil {
    87  		panic(err)
    88  	}
    89  	return fs
    90  }()
    91  
    92  type azureModelConfig struct {
    93  	*config.Config
    94  
    95  	// Azure specific config.
    96  	loadBalancerSkuName string
    97  	resourceGroupName   string
    98  	virtualNetworkName  string
    99  }
   100  
   101  // knownLoadBalancerSkuNames returns a list of valid load balancer SKU names.
   102  func knownLoadBalancerSkuNames() (skus []string) {
   103  	for _, name := range armnetwork.PossibleLoadBalancerSKUNameValues() {
   104  		skus = append(skus, string(name))
   105  	}
   106  	return skus
   107  }
   108  
   109  // Validate ensures that the provided configuration is valid for this
   110  // provider, and that changes between the old (if provided) and new
   111  // configurations are valid.
   112  func (*azureEnvironProvider) Validate(newCfg, oldCfg *config.Config) (*config.Config, error) {
   113  	_, err := validateConfig(newCfg, oldCfg)
   114  	if err != nil {
   115  		return nil, errors.Trace(err)
   116  	}
   117  	return newCfg, nil
   118  }
   119  
   120  func validateConfig(newCfg, oldCfg *config.Config) (*azureModelConfig, error) {
   121  	err := config.Validate(newCfg, oldCfg)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	validated, err := newCfg.ValidateUnknownAttrs(configFields, configDefaults)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	for key, field := range configSchema {
   132  		newValue, haveValue := validated[key]
   133  		if (!haveValue || fmt.Sprintf("%v", newValue) == "") && field.Mandatory {
   134  			return nil, errors.NotValidf("empty value for %q", key)
   135  		}
   136  		if oldCfg == nil {
   137  			continue
   138  		}
   139  		if !field.Immutable {
   140  			continue
   141  		}
   142  		// Ensure immutable configuration isn't changed.
   143  		oldUnknownAttrs := oldCfg.UnknownAttrs()
   144  		oldValue, hadValue := oldUnknownAttrs[key]
   145  		if hadValue {
   146  			if !haveValue {
   147  				return nil, errors.Errorf(
   148  					"cannot remove immutable %q config", key,
   149  				)
   150  			}
   151  			if newValue != oldValue {
   152  				return nil, errors.Errorf(
   153  					"cannot change immutable %q config (%v -> %v)",
   154  					key, oldValue, newValue,
   155  				)
   156  			}
   157  		}
   158  		// It's valid to go from not having to having.
   159  	}
   160  
   161  	// Resource group names must not exceed 80 characters. Resource group
   162  	// names are based on the model UUID and model name, the latter of
   163  	// which the model creator controls.
   164  	var userSpecifiedResourceGroup string
   165  	resourceGroup, ok := validated[configAttrResourceGroupName].(string)
   166  	if ok && resourceGroup != "" {
   167  		userSpecifiedResourceGroup = resourceGroup
   168  		if len(resourceGroup) > resourceNameLengthMax {
   169  			return nil, errors.Errorf(`resource group name %q is too long
   170  
   171  Please choose a name of no more than %d characters.`,
   172  				resourceGroup,
   173  				resourceNameLengthMax,
   174  			)
   175  		}
   176  	} else {
   177  		modelTag := names.NewModelTag(newCfg.UUID())
   178  		resourceGroup = resourceGroupName(modelTag, newCfg.Name())
   179  		if n := len(resourceGroup); n > resourceNameLengthMax {
   180  			smallestResourceGroup := resourceGroupName(modelTag, "")
   181  			return nil, errors.Errorf(`resource group name %q is too long
   182  
   183  Please choose a model name of no more than %d characters.`,
   184  				resourceGroup,
   185  				resourceNameLengthMax-len(smallestResourceGroup),
   186  			)
   187  		}
   188  	}
   189  
   190  	if newCfg.FirewallMode() == config.FwGlobal {
   191  		return nil, errors.New("global firewall mode is not supported")
   192  	}
   193  
   194  	loadBalancerSkuName, ok := validated[configAttrLoadBalancerSkuName].(string)
   195  	if ok {
   196  		loadBalancerSkuNameTitle := strings.Title(loadBalancerSkuName)
   197  		if loadBalancerSkuName != loadBalancerSkuNameTitle {
   198  			loadBalancerSkuName = loadBalancerSkuNameTitle
   199  			logger.Infof("using %q for config parameter %s", loadBalancerSkuName, configAttrLoadBalancerSkuName)
   200  		}
   201  		if !isKnownLoadBalancerSkuName(loadBalancerSkuName) {
   202  			return nil, errors.Errorf(
   203  				"invalid load balancer SKU name %q, expected one of: %q",
   204  				loadBalancerSkuName, knownLoadBalancerSkuNames(),
   205  			)
   206  		}
   207  	} else {
   208  		loadBalancerSkuName = string(armnetwork.LoadBalancerSKUNameStandard)
   209  	}
   210  
   211  	networkName, _ := validated[configAttrNetwork].(string)
   212  
   213  	azureConfig := &azureModelConfig{
   214  		Config:              newCfg,
   215  		loadBalancerSkuName: loadBalancerSkuName,
   216  		resourceGroupName:   userSpecifiedResourceGroup,
   217  		virtualNetworkName:  networkName,
   218  	}
   219  	return azureConfig, nil
   220  }
   221  
   222  // isKnownLoadBalancerSkuName reports whether or not the given string
   223  // a valid storage SKU within the Azure SDK
   224  func isKnownLoadBalancerSkuName(n string) bool {
   225  	for _, skuName := range knownLoadBalancerSkuNames() {
   226  		if n == skuName {
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  // canonicalLocation returns the canonicalized location string. This involves
   234  // stripping whitespace, and lowercasing. The ARM APIs do not support embedded
   235  // whitespace, whereas the old Service Management APIs used to; we allow the
   236  // user to provide either, and canonicalize them to one form that ARM allows.
   237  func canonicalLocation(s string) string {
   238  	s = strings.Replace(s, " ", "", -1)
   239  	return strings.ToLower(s)
   240  }