pkg/engine/engine.go (958 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. package engine import ( "bytes" "compress/gzip" "encoding/base64" "encoding/json" "fmt" "io" "net" "net/http" "regexp" "sort" "strconv" "strings" "text/template" "github.com/Azure/aks-engine-azurestack/pkg/api" "github.com/Azure/aks-engine-azurestack/pkg/api/common" "github.com/Azure/aks-engine-azurestack/pkg/helpers" "github.com/Azure/aks-engine-azurestack/pkg/helpers/to" "github.com/pkg/errors" _ "k8s.io/client-go/plugin/pkg/client/auth/azure" // register azure (AD) authentication plugin ) var commonTemplateFiles = []string{agentOutputs, agentParams, masterOutputs, iaasOutputs, masterParams, windowsParams} var kubernetesParamFiles = []string{armParameters, kubernetesParams, masterParams, agentParams, windowsParams} var keyvaultSecretPathRe *regexp.Regexp func init() { keyvaultSecretPathRe = regexp.MustCompile(`^(/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.KeyVault/vaults/\S+)/secrets/([^/\s]+)(/(\S+))?$`) } // GenerateKubeConfig returns a JSON string representing the KubeConfig func GenerateKubeConfig(properties *api.Properties, location string) (string, error) { if properties == nil { return "", errors.New("Properties nil in GenerateKubeConfig") } if properties.CertificateProfile == nil { return "", errors.New("CertificateProfile property may not be nil in GenerateKubeConfig") } b, err := Asset(kubeConfigJSON) if err != nil { return "", errors.Wrapf(err, "error reading kube config template file %s", kubeConfigJSON) } kubeconfig := string(b) // variable replacement kubeconfig = strings.Replace(kubeconfig, "{{WrapAsVerbatim \"parameters('caCertificate')\"}}", base64.StdEncoding.EncodeToString([]byte(properties.CertificateProfile.CaCertificate)), -1) if properties.OrchestratorProfile != nil && properties.OrchestratorProfile.KubernetesConfig != nil && properties.OrchestratorProfile.KubernetesConfig.PrivateCluster != nil && to.Bool(properties.OrchestratorProfile.KubernetesConfig.PrivateCluster.Enabled) { if properties.MasterProfile.HasMultipleNodes() { // more than 1 master, use the internal lb IP firstMasterIP := net.ParseIP(properties.MasterProfile.FirstConsecutiveStaticIP).To4() if firstMasterIP == nil { return "", errors.Errorf("MasterProfile.FirstConsecutiveStaticIP '%s' is an invalid IP address", properties.MasterProfile.FirstConsecutiveStaticIP) } lbIP := net.IP{firstMasterIP[0], firstMasterIP[1], firstMasterIP[2], firstMasterIP[3] + byte(DefaultInternalLbStaticIPOffset)} kubeconfig = strings.Replace(kubeconfig, "{{WrapAsVerbatim \"reference(concat('Microsoft.Network/publicIPAddresses/', variables('masterPublicIPAddressName'))).dnsSettings.fqdn\"}}", lbIP.String(), -1) } else { // Master count is 1, use the master IP kubeconfig = strings.Replace(kubeconfig, "{{WrapAsVerbatim \"reference(concat('Microsoft.Network/publicIPAddresses/', variables('masterPublicIPAddressName'))).dnsSettings.fqdn\"}}", properties.MasterProfile.FirstConsecutiveStaticIP, -1) } } else { kubeconfig = strings.Replace(kubeconfig, "{{WrapAsVerbatim \"reference(concat('Microsoft.Network/publicIPAddresses/', variables('masterPublicIPAddressName'))).dnsSettings.fqdn\"}}", api.FormatProdFQDNByLocation(properties.MasterProfile.DNSPrefix, location, properties.GetCustomCloudName()), -1) } kubeconfig = strings.Replace(kubeconfig, "{{WrapAsVariable \"resourceGroup\"}}", properties.MasterProfile.DNSPrefix, -1) var authInfo string if properties.AADProfile == nil { authInfo = fmt.Sprintf("{\"client-certificate-data\":\"%v\",\"client-key-data\":\"%v\"}", base64.StdEncoding.EncodeToString([]byte(properties.CertificateProfile.KubeConfigCertificate)), base64.StdEncoding.EncodeToString([]byte(properties.CertificateProfile.KubeConfigPrivateKey))) } else { tenantID := properties.AADProfile.TenantID if len(tenantID) == 0 { tenantID = "common" } authInfo = fmt.Sprintf("{\"auth-provider\":{\"name\":\"azure\",\"config\":{\"environment\":\"%v\",\"tenant-id\":\"%v\",\"apiserver-id\":\"%v\",\"client-id\":\"%v\"}}}", helpers.GetTargetEnv(location, properties.GetCustomCloudName()), tenantID, properties.AADProfile.ServerAppID, properties.AADProfile.ClientAppID) } kubeconfig = strings.Replace(kubeconfig, "{{authInfo}}", authInfo, -1) return kubeconfig, nil } // generateConsecutiveIPsList takes a starting IP address and returns a string slice of length "count" of subsequent, consecutive IP addresses func generateConsecutiveIPsList(count int, firstAddr string) ([]string, error) { ipaddr := net.ParseIP(firstAddr).To4() if ipaddr == nil { return nil, errors.Errorf("IPAddr '%s' is an invalid IP address", firstAddr) } if int(ipaddr[3])+count >= 255 { return nil, errors.Errorf("IPAddr '%s' + %d will overflow the fourth octet", firstAddr, count) } ret := make([]string, count) for i := 0; i < count; i++ { nextAddress := fmt.Sprintf("%d.%d.%d.%d", ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3]+byte(i)) ipaddr := net.ParseIP(nextAddress).To4() if ipaddr == nil { return nil, errors.Errorf("IPAddr '%s' is an invalid IP address", nextAddress) } ret[i] = nextAddress } return ret, nil } func addValue(m paramsMap, k string, v interface{}) { m[k] = paramsMap{ "value": v, } } func addKeyvaultReference(m paramsMap, k string, vaultID, secretName, secretVersion string) { m[k] = paramsMap{ "reference": &KeyVaultRef{ KeyVault: KeyVaultID{ ID: vaultID, }, SecretName: secretName, SecretVersion: secretVersion, }, } } func addSecret(m paramsMap, k string, v interface{}, encode bool) { str, ok := v.(string) if !ok { addValue(m, k, v) return } parts := keyvaultSecretPathRe.FindStringSubmatch(str) if parts == nil || len(parts) != 5 { if encode { addValue(m, k, base64.StdEncoding.EncodeToString([]byte(str))) } else { addValue(m, k, str) } return } addKeyvaultReference(m, k, parts[1], parts[2], parts[4]) } func makeMasterExtensionScriptCommands(cs *api.ContainerService) string { curlCaCertOpt := "" if cs.Properties.IsAzureStackCloud() { curlCaCertOpt = fmt.Sprintf("--cacert %s", common.AzureStackCaCertLocation) } return makeExtensionScriptCommands(cs.Properties.MasterProfile.PreprovisionExtension, curlCaCertOpt, cs.Properties.ExtensionProfiles) } func makeAgentExtensionScriptCommands(cs *api.ContainerService, profile *api.AgentPoolProfile) string { if profile.OSType == api.Windows { return makeWindowsExtensionScriptCommands(profile.PreprovisionExtension, cs.Properties.ExtensionProfiles) } curlCaCertOpt := "" if cs.Properties.IsAzureStackCloud() { curlCaCertOpt = fmt.Sprintf("--cacert %s", common.AzureStackCaCertLocation) } return makeExtensionScriptCommands(profile.PreprovisionExtension, curlCaCertOpt, cs.Properties.ExtensionProfiles) } func makeExtensionScriptCommands(extension *api.Extension, curlCaCertOpt string, extensionProfiles []*api.ExtensionProfile) string { var extensionProfile *api.ExtensionProfile for _, eP := range extensionProfiles { if strings.EqualFold(eP.Name, extension.Name) { extensionProfile = eP break } } if extensionProfile == nil { panic(fmt.Sprintf("%s extension referenced was not found in the extension profile", extension.Name)) } extensionsParameterReference := fmt.Sprintf("parameters('%sParameters')", extensionProfile.Name) scriptURL := getExtensionURL(extensionProfile.RootURL, extensionProfile.Name, extensionProfile.Version, extensionProfile.Script, extensionProfile.URLQuery) scriptFilePath := fmt.Sprintf("/opt/azure/containers/extensions/%s/%s", extensionProfile.Name, extensionProfile.Script) return fmt.Sprintf("- sudo /usr/bin/curl --retry 5 --retry-delay 10 --retry-max-time 30 -o %s --create-dirs %s \"%s\" \n- sudo /bin/chmod 744 %s \n- sudo %s ',%s,' > /var/log/%s-output.log", scriptFilePath, curlCaCertOpt, scriptURL, scriptFilePath, scriptFilePath, extensionsParameterReference, extensionProfile.Name) } func makeWindowsExtensionScriptCommands(extension *api.Extension, extensionProfiles []*api.ExtensionProfile) string { var extensionProfile *api.ExtensionProfile for _, eP := range extensionProfiles { if strings.EqualFold(eP.Name, extension.Name) { extensionProfile = eP break } } if extensionProfile == nil { panic(fmt.Sprintf("%s extension referenced was not found in the extension profile", extension.Name)) } scriptURL := getExtensionURL(extensionProfile.RootURL, extensionProfile.Name, extensionProfile.Version, extensionProfile.Script, extensionProfile.URLQuery) scriptFileDir := fmt.Sprintf("$env:SystemDrive:/AzureData/extensions/%s", extensionProfile.Name) scriptFilePath := fmt.Sprintf("%s/%s", scriptFileDir, extensionProfile.Script) return fmt.Sprintf("New-Item -ItemType Directory -Force -Path \"%s\" ; Invoke-WebRequest -Uri \"%s\" -OutFile \"%s\" ; powershell \"%s `\"',parameters('%sParameters'),'`\"\"\n", scriptFileDir, scriptURL, scriptFilePath, scriptFilePath, extensionProfile.Name) } func getVNETAddressPrefixes(properties *api.Properties) string { visitedSubnets := make(map[string]bool) var buf bytes.Buffer buf.WriteString(`"[variables('masterSubnet')]"`) visitedSubnets[properties.MasterProfile.Subnet] = true for _, profile := range properties.AgentPoolProfiles { if _, ok := visitedSubnets[profile.Subnet]; !ok { buf.WriteString(fmt.Sprintf(",\n \"[variables('%sSubnet')]\"", profile.Name)) } } return buf.String() } func getVNETSubnetDependencies(properties *api.Properties) string { agentString := ` "[concat('Microsoft.Network/networkSecurityGroups/', variables('%sNSGName'))]"` var buf bytes.Buffer for index, agentProfile := range properties.AgentPoolProfiles { if index > 0 { buf.WriteString(",\n") } buf.WriteString(fmt.Sprintf(agentString, agentProfile.Name)) } return buf.String() } func getVNETSubnets(properties *api.Properties, addNSG bool) string { masterString := `{ "name": "[variables('masterSubnetName')]", "properties": { "addressPrefix": "[variables('masterSubnet')]" } }` agentString := ` { "name": "[variables('%sSubnetName')]", "properties": { "addressPrefix": "[variables('%sSubnet')]" } }` agentStringNSG := ` { "name": "[variables('%sSubnetName')]", "properties": { "addressPrefix": "[variables('%sSubnet')]", "networkSecurityGroup": { "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('%sNSGName'))]" } } }` var buf bytes.Buffer buf.WriteString(masterString) for _, agentProfile := range properties.AgentPoolProfiles { buf.WriteString(",\n") if addNSG { buf.WriteString(fmt.Sprintf(agentStringNSG, agentProfile.Name, agentProfile.Name, agentProfile.Name)) } else { buf.WriteString(fmt.Sprintf(agentString, agentProfile.Name, agentProfile.Name)) } } return buf.String() } func getLBRule(name string, port int) string { return fmt.Sprintf(` { "name": "LBRule%d", "properties": { "backendAddressPool": { "id": "[concat(variables('%sLbID'), '/backendAddressPools/', variables('%sLbBackendPoolName'))]" }, "backendPort": %d, "enableFloatingIP": false, "frontendIPConfiguration": { "id": "[variables('%sLbIPConfigID')]" }, "frontendPort": %d, "idleTimeoutInMinutes": 5, "loadDistribution": "Default", "probe": { "id": "[concat(variables('%sLbID'),'/probes/tcp%dProbe')]" }, "protocol": "Tcp" } }`, port, name, name, port, name, port, name, port) } func getLBRules(name string, ports []int) string { var buf bytes.Buffer for index, port := range ports { if index > 0 { buf.WriteString(",\n") } buf.WriteString(getLBRule(name, port)) } return buf.String() } func getProbe(port int) string { return fmt.Sprintf(` { "name": "tcp%dProbe", "properties": { "intervalInSeconds": 5, "numberOfProbes": 2, "port": %d, "protocol": "Tcp" } }`, port, port) } func getProbes(ports []int) string { var buf bytes.Buffer for index, port := range ports { if index > 0 { buf.WriteString(",\n") } buf.WriteString(getProbe(port)) } return buf.String() } func getSecurityRule(port int, portIndex int) string { // BaseLBPriority specifies the base lb priority. BaseLBPriority := 200 return fmt.Sprintf(` { "name": "Allow_%d", "properties": { "access": "Allow", "description": "Allow traffic from the Internet to port %d", "destinationAddressPrefix": "*", "destinationPortRange": "%d", "direction": "Inbound", "priority": %d, "protocol": "*", "sourceAddressPrefix": "Internet", "sourcePortRange": "*" } }`, port, port, port, BaseLBPriority+portIndex) } func getDataDisks(a *api.AgentPoolProfile) string { if !a.HasDisks() { return "" } var buf bytes.Buffer buf.WriteString("\"dataDisks\": [\n") dataDisks := ` { "createOption": "Empty", "diskSizeGB": "%d", "lun": %d, "caching": "ReadOnly", "name": "[concat(variables('%sVMNamePrefix'), copyIndex(),'-datadisk%d')]", "vhd": { "uri": "[concat('http://',variables('storageAccountPrefixes')[mod(add(add(div(copyIndex(),variables('maxVMsPerStorageAccount')),variables('%sStorageAccountOffset')),variables('dataStorageAccountPrefixSeed')),variables('storageAccountPrefixesCount'))],variables('storageAccountPrefixes')[div(add(add(div(copyIndex(),variables('maxVMsPerStorageAccount')),variables('%sStorageAccountOffset')),variables('dataStorageAccountPrefixSeed')),variables('storageAccountPrefixesCount'))],variables('%sDataAccountName'),'.blob.core.windows.net/vhds/',variables('%sVMNamePrefix'),copyIndex(), '--datadisk%d.vhd')]" } }` managedDataDisks := ` { "diskSizeGB": "%d", "lun": %d, "caching": "ReadOnly", "createOption": "Empty" }` for i, diskSize := range a.DiskSizesGB { if i > 0 { buf.WriteString(",\n") } if a.StorageProfile == api.StorageAccount { buf.WriteString(fmt.Sprintf(dataDisks, diskSize, i, a.Name, i, a.Name, a.Name, a.Name, a.Name, i)) } else if a.StorageProfile == api.ManagedDisks { buf.WriteString(fmt.Sprintf(managedDataDisks, diskSize, i)) } } buf.WriteString("\n ],") return buf.String() } func getSecurityRules(ports []int) string { var buf bytes.Buffer for index, port := range ports { if index > 0 { buf.WriteString(",\n") } buf.WriteString(getSecurityRule(port, index)) } return buf.String() } // getSingleLine returns the file as a single line func (t *TemplateGenerator) getSingleLine(textFilename string, cs *api.ContainerService, profile interface{}) (string, error) { b, err := Asset(textFilename) if err != nil { return "", t.Translator.Errorf("yaml file %s does not exist", textFilename) } // use go templates to process the text filename templ := template.New("customdata template").Funcs(t.getTemplateFuncMap(cs)) if _, err = templ.New(textFilename).Parse(string(b)); err != nil { return "", t.Translator.Errorf("error parsing file %s: %v", textFilename, err) } var buffer bytes.Buffer if err = templ.ExecuteTemplate(&buffer, textFilename, profile); err != nil { return "", t.Translator.Errorf("error executing template for file %s: %v", textFilename, err) } expandedTemplate := buffer.String() return expandedTemplate, nil } // getSingleLineForTemplate returns the file as a single line for embedding in an arm template func (t *TemplateGenerator) getSingleLineForTemplate(textFilename string, cs *api.ContainerService, profile interface{}) (string, error) { expandedTemplate, err := t.getSingleLine(textFilename, cs, profile) if err != nil { return "", err } textStr := escapeSingleLine(expandedTemplate) return textStr, nil } func escapeSingleLine(escapedStr string) string { // template.JSEscapeString leaves undesirable chars that don't work with pretty print escapedStr = strings.Replace(escapedStr, "\\", "\\\\", -1) escapedStr = strings.Replace(escapedStr, "\r\n", "\\n", -1) escapedStr = strings.Replace(escapedStr, "\n", "\\n", -1) escapedStr = strings.Replace(escapedStr, "\"", "\\\"", -1) return escapedStr } // getBase64EncodedGzippedCustomScript will return a base64 of the CSE func getBase64EncodedGzippedCustomScript(csFilename string, cs *api.ContainerService) string { b, err := Asset(csFilename) if err != nil { // this should never happen and this is a bug panic(fmt.Sprintf("BUG: %s", err.Error())) } // translate the parameters templ := template.New("ContainerService template").Funcs(getContainerServiceFuncMap(cs)) _, err = templ.Parse(string(b)) if err != nil { // this should never happen and this is a bug panic(fmt.Sprintf("BUG: %s", err.Error())) } var buffer bytes.Buffer _ = templ.Execute(&buffer, cs) csStr := buffer.String() csStr = strings.Replace(csStr, "\r\n", "\n", -1) return getBase64EncodedGzippedCustomScriptFromStr(csStr) } func getStringFromBase64(str string) (string, error) { decodedBytes, err := base64.StdEncoding.DecodeString(str) return string(decodedBytes), err } // getBase64EncodedGzippedCustomScriptFromStr will return a base64-encoded string of the gzip'd source data func getBase64EncodedGzippedCustomScriptFromStr(str string) string { var gzipB bytes.Buffer w := gzip.NewWriter(&gzipB) _, _ = w.Write([]byte(str)) w.Close() return base64.StdEncoding.EncodeToString(gzipB.Bytes()) } func getComponentFuncMap(component api.KubernetesComponent, cs *api.ContainerService) template.FuncMap { ret := template.FuncMap{ "ContainerImage": func(name string) string { if i := component.GetContainersIndexByName(name); i > -1 { return component.Containers[i].Image } return "" }, "ContainerCPUReqs": func(name string) string { if i := component.GetContainersIndexByName(name); i > -1 { return component.Containers[i].CPURequests } return "" }, "ContainerCPULimits": func(name string) string { if i := component.GetContainersIndexByName(name); i > -1 { return component.Containers[i].CPULimits } return "" }, "ContainerMemReqs": func(name string) string { if i := component.GetContainersIndexByName(name); i > -1 { return component.Containers[i].MemoryRequests } return "" }, "ContainerMemLimits": func(name string) string { if i := component.GetContainersIndexByName(name); i > -1 { return component.Containers[i].MemoryLimits } return "" }, "ContainerConfig": func(name string) string { return component.Config[name] }, "IsCustomCloudProfile": func() bool { return cs.Properties.IsCustomCloudProfile() }, "IsAzureStackCloud": func() bool { return cs.Properties.IsAzureStackCloud() }, "IsKubernetesVersionGe": func(version string) bool { return common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, version) }, } if component.Name == common.APIServerComponentName { ret["GetAPIServerArgs"] = func() string { return common.GetOrderedEscapedKeyValsString(cs.Properties.OrchestratorProfile.KubernetesConfig.APIServerConfig) } } if component.Name == common.ControllerManagerComponentName { ret["GetControllerManagerArgs"] = func() string { return common.GetOrderedEscapedKeyValsString(cs.Properties.OrchestratorProfile.KubernetesConfig.ControllerManagerConfig) } } if component.Name == common.SchedulerComponentName { ret["GetSchedulerArgs"] = func() string { return common.GetOrderedEscapedKeyValsString(cs.Properties.OrchestratorProfile.KubernetesConfig.SchedulerConfig) } } if component.Name == common.CloudControllerManagerComponentName { ret["GetCloudControllerManagerArgs"] = func() string { return common.GetOrderedEscapedKeyValsString(cs.Properties.OrchestratorProfile.KubernetesConfig.CloudControllerManagerConfig) } } return ret } func getAddonFuncMap(addon api.KubernetesAddon, cs *api.ContainerService) template.FuncMap { return template.FuncMap{ "ContainerImage": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].Image }, "ContainerCPUReqs": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].CPURequests }, "ContainerCPULimits": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].CPULimits }, "ContainerMemReqs": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].MemoryRequests }, "ContainerMemLimits": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].MemoryLimits }, "ContainerConfig": func(name string) string { return addon.Config[name] }, "ContainerConfigBase64": func(name string) string { return base64.StdEncoding.EncodeToString([]byte(addon.Config[name])) }, "HasWindows": func() bool { return cs.Properties.HasWindows() }, "IsCustomCloudProfile": func() bool { return cs.Properties.IsCustomCloudProfile() }, "HasLinux": func() bool { return cs.Properties.AnyAgentIsLinux() }, "IsAzureStackCloud": func() bool { return cs.Properties.IsAzureStackCloud() }, "NeedsStorageAccountStorageClasses": func() bool { return len(cs.Properties.AgentPoolProfiles) > 0 && cs.Properties.AgentPoolProfiles[0].StorageProfile == api.StorageAccount }, "NeedsManagedDiskStorageClasses": func() bool { return len(cs.Properties.AgentPoolProfiles) > 0 && cs.Properties.AgentPoolProfiles[0].StorageProfile == api.ManagedDisks }, "UsesCloudControllerManager": func() bool { return to.Bool(cs.Properties.OrchestratorProfile.KubernetesConfig.UseCloudControllerManager) }, "HasAvailabilityZones": func() bool { return cs.Properties.HasAvailabilityZones() }, "HasAgentPoolAvailabilityZones": func() bool { return cs.Properties.HasAgentPoolAvailabilityZones() }, "GetAgentPoolZones": func() string { if len(cs.Properties.AgentPoolProfiles) == 0 { return "" } var zones string for _, pool := range cs.Properties.AgentPoolProfiles { if pool.AvailabilityZones != nil { for _, zone := range pool.AvailabilityZones { zones += fmt.Sprintf("\n - %s-%s", cs.Location, zone) } } if zones != "" { return zones } } return zones }, "CSIControllerReplicas": func() string { replicas := "2" if cs.Properties.HasWindows() && !cs.Properties.AnyAgentIsLinux() { replicas = "1" } return replicas }, "ShouldEnableCSISnapshotFeature": func(csiDriverName string) bool { // Snapshot is not available for Windows clusters if cs.Properties.HasWindows() && !cs.Properties.AnyAgentIsLinux() { return false } switch csiDriverName { case common.AzureDiskCSIDriverAddonName: // Snapshot feature for Azure Disk CSI Driver is in beta, requiring K8s 1.17+ return common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.17.0") case common.AzureFileCSIDriverAddonName: // Snapshot feature for Azure File CSI Driver is in alpha, requiring K8s 1.13-1.16 return common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.13.0") && !common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.17.0") } return false }, "IsKubernetesVersionGe": func(version string) bool { return common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, version) }, "GetAADPodIdentityTaintKey": func() string { return common.AADPodIdentityTaintKey }, "GetMode": func() string { return addon.Mode }, "GetClusterSubnet": func() string { return cs.Properties.OrchestratorProfile.KubernetesConfig.ClusterSubnet }, "IsAzureCNI": func() bool { return cs.Properties.OrchestratorProfile.IsAzureCNI() }, "GetCRDAPIVersion": func() string { if common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.22.0") { return "apiextensions.k8s.io/v1" } return "apiextensions.k8s.io/v1beta1" }, "GetRBACAPIVersion": func() string { if common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.22.0") { return "rbac.authorization.k8s.io/v1" } return "rbac.authorization.k8s.io/v1beta1" }, "GetStorageAPIVersion": func() string { if common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.22.0") { return "storage.k8s.io/v1" } return "storage.k8s.io/v1beta1" }, "GetWebhookAPIVersion": func() string { if common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, "1.22.0") { return "admissionregistration.k8s.io/v1" } return "admissionregistration.k8s.io/v1beta1" }, "ShouldEnforceKubernetesDisaStig": func() bool { return cs.Properties.FeatureFlags.IsFeatureEnabled("EnforceKubernetesDisaStig") }, } } func getClusterAutoscalerAddonFuncMap(addon api.KubernetesAddon, cs *api.ContainerService) template.FuncMap { return template.FuncMap{ "ContainerImage": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].Image }, "ContainerCPUReqs": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].CPURequests }, "ContainerCPULimits": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].CPULimits }, "ContainerMemReqs": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].MemoryRequests }, "ContainerMemLimits": func(name string) string { i := addon.GetAddonContainersIndexByName(name) return addon.Containers[i].MemoryLimits }, "ContainerConfig": func(name string) string { return addon.Config[name] }, "GetMode": func() string { return addon.Mode }, "GetClusterAutoscalerNodesConfig": func() string { return api.GetClusterAutoscalerNodesConfig(addon, cs) }, "GetBase64EncodedVMType": func() string { return base64.StdEncoding.EncodeToString([]byte(cs.Properties.GetVMType())) }, "GetVolumeMounts": func() string { if to.Bool(cs.Properties.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) { return "\n - mountPath: /var/lib/waagent/\n name: waagent\n readOnly: true" } return "" }, "GetVolumes": func() string { if to.Bool(cs.Properties.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) { return "\n - hostPath:\n path: /var/lib/waagent/\n name: waagent" } return "" }, "GetHostNetwork": func() string { if to.Bool(cs.Properties.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) { return "\n hostNetwork: true" } return "" }, "GetCloud": func() string { cloudSpecConfig := cs.GetCloudSpecConfig() return cloudSpecConfig.CloudName }, "UseManagedIdentity": func() string { if to.Bool(cs.Properties.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) { return "true" } return "false" }, "IsKubernetesVersionGe": func(version string) bool { return common.IsKubernetesVersionGe(cs.Properties.OrchestratorProfile.OrchestratorVersion, version) }, } } func getComponentsString(cs *api.ContainerService, sourcePath string) string { properties := cs.Properties var result string settingsMap := kubernetesComponentSettingsInit(properties) var componentNames []string for componentName := range settingsMap { componentNames = append(componentNames, componentName) } sort.Strings(componentNames) for _, componentName := range componentNames { setting := settingsMap[componentName] if component, isEnabled := cs.Properties.OrchestratorProfile.KubernetesConfig.IsComponentEnabled(componentName); isEnabled { var input string if setting.base64Data != "" { var err error input, err = getStringFromBase64(setting.base64Data) if err != nil { return "" } } else if setting.sourceFile != "" { orchProfile := properties.OrchestratorProfile versions := strings.Split(orchProfile.OrchestratorVersion, ".") templ := template.New("component resolver template").Funcs(getComponentFuncMap(component, cs)) componentFile := getCustomDataFilePath(setting.sourceFile, sourcePath, versions[0]+"."+versions[1]) componentFileBytes, err := Asset(componentFile) if err != nil { return "" } _, err = templ.Parse(string(componentFileBytes)) if err != nil { return "" } var buffer bytes.Buffer _ = templ.Execute(&buffer, component) input = buffer.String() } if componentName == common.ClusterInitComponentName { result += getComponentString(input, "/opt/azure/containers", setting.destinationFile) } else { result += getComponentString(input, "/etc/kubernetes/manifests", setting.destinationFile) } } } return result } func getAddonsString(cs *api.ContainerService, sourcePath string) string { properties := cs.Properties var result string settingsMap := kubernetesAddonSettingsInit(properties) var addonNames []string for addonName := range settingsMap { addonNames = append(addonNames, addonName) } sort.Strings(addonNames) for _, addonName := range addonNames { setting := settingsMap[addonName] if cs.Properties.OrchestratorProfile.KubernetesConfig.IsAddonEnabled(addonName) { var input string if setting.base64Data != "" { var err error input, err = getStringFromBase64(setting.base64Data) if err != nil { return "" } } else { orchProfile := properties.OrchestratorProfile versions := strings.Split(orchProfile.OrchestratorVersion, ".") addon := orchProfile.KubernetesConfig.GetAddonByName(addonName) var templ *template.Template switch addonName { case "cluster-autoscaler": templ = template.New("addon resolver template").Funcs(getClusterAutoscalerAddonFuncMap(addon, cs)) default: templ = template.New("addon resolver template").Funcs(getAddonFuncMap(addon, cs)) } addonFile := getCustomDataFilePath(setting.sourceFile, sourcePath, versions[0]+"."+versions[1]) addonFileBytes, err := Asset(addonFile) if err != nil { return "" } _, err = templ.Parse(string(addonFileBytes)) if err != nil { return "" } var buffer bytes.Buffer _ = templ.Execute(&buffer, addon) input = buffer.String() } result += getComponentString(input, "/etc/kubernetes/addons", setting.destinationFile) } } return result } func getKubernetesSubnets(properties *api.Properties) string { subnetString := `{ "name": "podCIDR%d", "properties": { "addressPrefix": "10.244.%d.0/24", "networkSecurityGroup": { "id": "[variables('nsgID')]" }, "routeTable": { "id": "[variables('routeTableID')]" } } }` var buf bytes.Buffer cidrIndex := getKubernetesPodStartIndex(properties) for _, agentProfile := range properties.AgentPoolProfiles { if agentProfile.OSType == api.Windows { for i := 0; i < agentProfile.Count; i++ { buf.WriteString(",\n") buf.WriteString(fmt.Sprintf(subnetString, cidrIndex, cidrIndex)) cidrIndex++ } } } return buf.String() } func getKubernetesPodStartIndex(properties *api.Properties) int { nodeCount := 0 nodeCount += properties.MasterProfile.Count for _, agentProfile := range properties.AgentPoolProfiles { if agentProfile.OSType != api.Windows { nodeCount += agentProfile.Count } } return nodeCount + 1 } func getMasterLinkedTemplateText(orchestratorType string, extensionProfile *api.ExtensionProfile, singleOrAll string) (string, error) { extTargetVMNamePrefix := "variables('masterVMNamePrefix')" // Due to upgrade k8s sometimes needs to install just some of the nodes. loopCount := "[sub(variables('masterCount'), variables('masterOffset'))]" loopOffset := "variables('masterOffset')" if strings.EqualFold(singleOrAll, "single") { loopCount = "1" } return internalGetPoolLinkedTemplateText(extTargetVMNamePrefix, orchestratorType, loopCount, loopOffset, extensionProfile) } func getAgentPoolLinkedTemplateText(agentPoolProfile *api.AgentPoolProfile, orchestratorType string, extensionProfile *api.ExtensionProfile, singleOrAll string) (string, error) { extTargetVMNamePrefix := fmt.Sprintf("variables('%sVMNamePrefix')", agentPoolProfile.Name) loopCount := fmt.Sprintf("[variables('%sCount'))]", agentPoolProfile.Name) loopOffset := "" // Availability sets can have an offset since we don't redeploy vms. // So we don't want to rerun these extensions in scale up scenarios. if agentPoolProfile.IsAvailabilitySets() { loopCount = fmt.Sprintf("[sub(variables('%sCount'), variables('%sOffset'))]", agentPoolProfile.Name, agentPoolProfile.Name) loopOffset = fmt.Sprintf("variables('%sOffset')", agentPoolProfile.Name) } if strings.EqualFold(singleOrAll, "single") { loopCount = "1" } return internalGetPoolLinkedTemplateText(extTargetVMNamePrefix, orchestratorType, loopCount, loopOffset, extensionProfile) } func internalGetPoolLinkedTemplateText(extTargetVMNamePrefix, orchestratorType, loopCount, loopOffset string, extensionProfile *api.ExtensionProfile) (string, error) { dta, e := getLinkedTemplateTextForURL(extensionProfile.RootURL, orchestratorType, extensionProfile.Name, extensionProfile.Version, extensionProfile.URLQuery) if e != nil { return "", e } if strings.Contains(extTargetVMNamePrefix, "master") { dta = strings.Replace(dta, "EXTENSION_TARGET_VM_TYPE", "master", -1) } else { dta = strings.Replace(dta, "EXTENSION_TARGET_VM_TYPE", "agent", -1) } extensionsParameterReference := fmt.Sprintf("[parameters('%sParameters')]", extensionProfile.Name) dta = strings.Replace(dta, "EXTENSION_PARAMETERS_REPLACE", extensionsParameterReference, -1) dta = strings.Replace(dta, "EXTENSION_URL_REPLACE", extensionProfile.RootURL, -1) dta = strings.Replace(dta, "EXTENSION_TARGET_VM_NAME_PREFIX", extTargetVMNamePrefix, -1) if _, err := strconv.Atoi(loopCount); err == nil { dta = strings.Replace(dta, "\"EXTENSION_LOOP_COUNT\"", loopCount, -1) } else { dta = strings.Replace(dta, "EXTENSION_LOOP_COUNT", loopCount, -1) } dta = strings.Replace(dta, "EXTENSION_LOOP_OFFSET", loopOffset, -1) return dta, nil } func validateProfileOptedForExtension(extensionName string, profileExtensions []api.Extension) (bool, string) { for _, extension := range profileExtensions { if extensionName == extension.Name { return true, extension.SingleOrAll } } return false, "" } // getLinkedTemplateTextForURL returns the string data from // template-link.json in the following directory: // extensionsRootURL/extensions/extensionName/version // It returns an error if the extension cannot be found // or loaded. getLinkedTemplateTextForURL provides the ability // to pass a root extensions url for testing func getLinkedTemplateTextForURL(rootURL, orchestrator, extensionName, version, query string) (string, error) { supportsExtension, err := orchestratorSupportsExtension(rootURL, orchestrator, extensionName, version, query) if !supportsExtension { return "", errors.Wrap(err, "Extension not supported for orchestrator") } templateLinkBytes, err := getExtensionResource(rootURL, extensionName, version, "template-link.json", query) if err != nil { return "", err } return string(templateLinkBytes), nil } func orchestratorSupportsExtension(rootURL, orchestrator, extensionName, version, query string) (bool, error) { orchestratorBytes, err := getExtensionResource(rootURL, extensionName, version, "supported-orchestrators.json", query) if err != nil { return false, err } var supportedOrchestrators []string err = json.Unmarshal(orchestratorBytes, &supportedOrchestrators) if err != nil { return false, errors.Errorf("Unable to parse supported-orchestrators.json for Extension %s Version %s", extensionName, version) } if !stringInSlice(orchestrator, supportedOrchestrators) { return false, errors.Errorf("Orchestrator: %s not in list of supported orchestrators for Extension: %s Version %s", orchestrator, extensionName, version) } return true, nil } func getExtensionResource(rootURL, extensionName, version, fileName, query string) ([]byte, error) { requestURL := getExtensionURL(rootURL, extensionName, version, fileName, query) res, err := http.Get(requestURL) if err != nil { return nil, errors.Wrapf(err, "Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s", extensionName, version, fileName, requestURL) } defer res.Body.Close() if res.StatusCode != 200 { return nil, errors.Errorf("Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s StatusCode: %s: Status: %s", extensionName, version, fileName, requestURL, strconv.Itoa(res.StatusCode), res.Status) } body, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrapf(err, "Unable to GET extension resource for extension: %s with version %s with filename %s at URL: %s", extensionName, version, fileName, requestURL) } return body, nil } func getExtensionURL(rootURL, extensionName, version, fileName, query string) string { extensionsDir := "extensions" url := rootURL + extensionsDir + "/" + extensionName + "/" + version + "/" + fileName if query != "" { url += "?" + query } return url } func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } func wrapAsVariableObject(o, v string) string { return fmt.Sprintf("',variables('%s').%s,'", o, v) } func getSSHPublicKeysPowerShell(linuxProfile *api.LinuxProfile) string { str := "" if linuxProfile != nil { lastItem := len(linuxProfile.SSH.PublicKeys) - 1 for i, publicKey := range linuxProfile.SSH.PublicKeys { str += `"` + strings.TrimSpace(publicKey.KeyData) + `"` if i < lastItem { str += ", " } } } return str } func getWindowsMasterSubnetARMParam(masterProfile *api.MasterProfile) string { if masterProfile != nil && masterProfile.IsCustomVNET() { return "',parameters('vnetCidr'),'" } return "',parameters('masterSubnet'),'" }