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 }