pkg/api/common/helper.go (542 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. package common import ( "bufio" "bytes" "encoding/json" "fmt" "regexp" "sort" "strings" "github.com/BurntSushi/toml" "github.com/pkg/errors" validator "gopkg.in/go-playground/validator.v9" ) // HandleValidationErrors is the helper function to catch validator.ValidationError // based on Namespace of the error, and return customized error message. func HandleValidationErrors(e validator.ValidationErrors) error { err := e[0] ns := err.Namespace() switch ns { case "Properties.OrchestratorProfile", "Properties.OrchestratorProfile.OrchestratorType", "Properties.MasterProfile", "Properties.MasterProfile.DNSPrefix", "Properties.MasterProfile.VMSize", "Properties.LinuxProfile", "Properties.ServicePrincipalProfile.ClientID", "Properties.WindowsProfile.AdminUsername", "Properties.WindowsProfile.AdminPassword": return errors.Errorf("missing %s", ns) case "Properties.MasterProfile.Count": return errors.New("MasterProfile count needs to be 1, 3, or 5") case "Properties.MasterProfile.OSDiskSizeGB": return errors.Errorf("Invalid os disk size of %d specified. The range of valid values are [%d, %d]", err.Value().(int), MinDiskSizeGB, MaxDiskSizeGB) case "Properties.MasterProfile.IPAddressCount": return errors.Errorf("MasterProfile.IPAddressCount needs to be in the range [%d,%d]", MinIPAddressCount, MaxIPAddressCount) case "Properties.MasterProfile.StorageProfile": return errors.Errorf("Unknown storageProfile '%s'. Specify either %s or %s", err.Value().(string), StorageAccount, ManagedDisks) default: if strings.HasPrefix(ns, "Properties.AgentPoolProfiles") { switch { case strings.HasSuffix(ns, ".Name") || strings.HasSuffix(ns, "VMSize"): return errors.Errorf("missing %s", ns) case strings.HasSuffix(ns, ".Count"): return errors.Errorf("AgentPoolProfile count needs to be in the range [%d,%d]", MinAgentCount, MaxAgentCount) case strings.HasSuffix(ns, ".OSDiskSizeGB"): return errors.Errorf("Invalid os disk size of %d specified. The range of valid values are [%d, %d]", err.Value().(int), MinDiskSizeGB, MaxDiskSizeGB) case strings.Contains(ns, ".Ports"): return errors.Errorf("AgentPoolProfile Ports must be in the range[%d, %d]", MinPort, MaxPort) case strings.HasSuffix(ns, ".StorageProfile"): return errors.Errorf("Unknown storageProfile '%s'. Specify %s, %s, or %s", err.Value().(string), StorageAccount, ManagedDisks, Ephemeral) case strings.Contains(ns, ".DiskSizesGB"): return errors.Errorf("A maximum of %d disks may be specified, The range of valid disk size values are [%d, %d]", MaxDisks, MinDiskSizeGB, MaxDiskSizeGB) case strings.HasSuffix(ns, ".IPAddressCount"): return errors.Errorf("AgentPoolProfile.IPAddressCount needs to be in the range [%d,%d]", MinIPAddressCount, MaxIPAddressCount) default: break } } } return errors.Errorf("Namespace %s is not caught, %+v", ns, e) } // ValidateDNSPrefix is a helper function to check that a DNS Prefix is valid func ValidateDNSPrefix(dnsName string) error { dnsNameRegex := `^([A-Za-z][A-Za-z0-9-]{1,43}[A-Za-z0-9])$` re, err := regexp.Compile(dnsNameRegex) if err != nil { return err } if !re.MatchString(dnsName) { return errors.Errorf("DNSPrefix '%s' is invalid. The DNSPrefix must contain between 3 and 45 characters and can contain only letters, numbers, and hyphens. It must start with a letter and must end with a letter or a number. (length was %d)", dnsName, len(dnsName)) } return nil } // IsNvidiaEnabledSKU determines if an VM SKU has nvidia driver support func IsNvidiaEnabledSKU(vmSize string) bool { /* If a new GPU sku becomes available, add a key to this map, but only if you have a confirmation that we have an agreement with NVIDIA for this specific gpu. */ dm := map[string]bool{ // K80 "Standard_NC6": true, "Standard_NC12": true, "Standard_NC24": true, "Standard_NC24r": true, // M60 "Standard_NV6": true, "Standard_NV12": true, "Standard_NV12s_v3": true, "Standard_NV24": true, "Standard_NV24s_v3": true, "Standard_NV24r": true, "Standard_NV48s_v3": true, // P40 "Standard_ND6s": true, "Standard_ND12s": true, "Standard_ND24s": true, "Standard_ND24rs": true, // P100 "Standard_NC6s_v2": true, "Standard_NC12s_v2": true, "Standard_NC24s_v2": true, "Standard_NC24rs_v2": true, // V100 "Standard_NC6s_v3": true, "Standard_NC12s_v3": true, "Standard_NC24s_v3": true, "Standard_NC24rs_v3": true, "Standard_ND40s_v3": true, "Standard_ND40rs_v2": true, // T4 "Standard_NC4as_T4_v3": true, "Standard_NC8as_T4_v3": true, "Standard_NC16as_T4_v3": true, "Standard_NC64as_T4_v3": true, } // Trim the optional _Promo suffix. vmSize = strings.TrimSuffix(vmSize, "_Promo") if _, ok := dm[vmSize]; ok { return dm[vmSize] } return false } // GetNSeriesVMCasesForTesting returns a struct w/ VM SKUs and whether or not we expect them to be nvidia-enabled func GetNSeriesVMCasesForTesting() []struct { VMSKU string Expected bool } { cases := []struct { VMSKU string Expected bool }{ { "Standard_NC6", true, }, { "Standard_NC6_Promo", true, }, { "Standard_NC12", true, }, { "Standard_NC24", true, }, { "Standard_NC24r", true, }, { "Standard_NV6", true, }, { "Standard_NV12", true, }, { "Standard_NV24", true, }, { "Standard_NV24r", true, }, { "Standard_ND6s", true, }, { "Standard_ND12s", true, }, { "Standard_ND24s", true, }, { "Standard_ND24rs", true, }, { "Standard_NC6s_v2", true, }, { "Standard_NC12s_v2", true, }, { "Standard_NC24s_v2", true, }, { "Standard_NC24rs_v2", true, }, { "Standard_NC24rs_v2", true, }, { "Standard_NC6s_v3", true, }, { "Standard_NC12s_v3", true, }, { "Standard_NC24s_v3", true, }, { "Standard_NC24rs_v3", true, }, { "Standard_D2_v2", false, }, { "gobledygook", false, }, { "", false, }, } return cases } // GetDCSeriesVMCasesForTesting returns a struct w/ VM SKUs and whether or not we expect them to be SGX-enabled func GetDCSeriesVMCasesForTesting() []struct { VMSKU string Expected bool } { cases := []struct { VMSKU string Expected bool }{ { "Standard_DC2s", true, }, { "Standard_DC4s", true, }, { "Standard_NC12", false, }, { "gobledygook", false, }, { "", false, }, } return cases } // IsSgxEnabledSKU determines if an VM SKU has SGX driver support func IsSgxEnabledSKU(vmSize string) bool { switch vmSize { case "Standard_DC2s", "Standard_DC4s": return true } return false } // GetMasterKubernetesLabels returns a k8s API-compliant labels string. // The `kubernetes.io/role` and `node-role.kubernetes.io` labels are disallowed // by the kubelet `--node-labels` argument in Kubernetes 1.16 and later. func GetMasterKubernetesLabels(rg string, deprecated bool) string { var buf bytes.Buffer buf.WriteString("kubernetes.azure.com/role=master") buf.WriteString(",node.kubernetes.io/exclude-from-external-load-balancers=true") buf.WriteString(",node.kubernetes.io/exclude-disruption=true") if deprecated { buf.WriteString(",kubernetes.io/role=master") buf.WriteString(",node-role.kubernetes.io/master=") } buf.WriteString(fmt.Sprintf(",kubernetes.azure.com/cluster=%s", rg)) return buf.String() } // GetStorageAccountType returns the managed disk storage tier for a given VM size. func GetStorageAccountType(sizeName string) (string, error) { spl := strings.Split(sizeName, "_") if len(spl) < 2 { return "", errors.Errorf("Invalid sizeName: %s", sizeName) } capability := spl[1] if strings.Contains(strings.ToLower(capability), "s") { return "Premium_LRS", nil } return "Standard_LRS", nil } // GetOrderedEscapedKeyValsString returns an ordered string of escaped, quoted key=val func GetOrderedEscapedKeyValsString(config map[string]string) string { keys := []string{} for key := range config { keys = append(keys, key) } sort.Strings(keys) var buf bytes.Buffer for _, key := range keys { buf.WriteString(fmt.Sprintf("\"%s=%s\", ", key, config[key])) } return strings.TrimSuffix(buf.String(), ", ") } // GetOrderedNewlinedKeyValsStringForCloudInit returns an ordered string of key = val, separated by newlines func GetOrderedNewlinedKeyValsStringForCloudInit(config map[string]string) string { keys := []string{} for key := range config { keys = append(keys, key) } sort.Strings(keys) var buf bytes.Buffer for _, key := range keys { buf.WriteString(fmt.Sprintf("%s = %s\n%4s", key, config[key], " ")) } return strings.TrimSuffix(buf.String(), fmt.Sprintf("\n%4s", " ")) } // SliceIntIsNonEmpty is a simple convenience to determine if a []int is non-empty func SliceIntIsNonEmpty(s []int) bool { return len(s) > 0 } // WrapAsARMVariable formats a string for inserting an ARM variable into an ARM expression func WrapAsARMVariable(s string) string { return fmt.Sprintf("',variables('%s'),'", s) } // WrapAsParameter formats a string for inserting an ARM parameter into an ARM expression func WrapAsParameter(s string) string { return fmt.Sprintf("',parameters('%s'),'", s) } // WrapAsVerbatim formats a string for inserting a literal string into an ARM expression func WrapAsVerbatim(s string) string { return fmt.Sprintf("',%s,'", s) } // GetDockerConfig transforms the default docker config with overrides. Overrides may be nil. func GetDockerConfig(opts map[string]string, overrides []func(*DockerConfig) error) (string, error) { config := GetDefaultDockerConfig() for i := range overrides { if err := overrides[i](&config); err != nil { return "", err } } dataDir, ok := opts[ContainerDataDirKey] if ok { config.DataRoot = dataDir } b, err := json.MarshalIndent(config, "", " ") return string(b), err } // GetContainerdConfig transforms the default containerd config with overrides. Overrides may be nil. func GetContainerdConfig(opts map[string]string, overrides []func(*ContainerdConfig) error) (string, error) { config := GetDefaultContainerdConfig() for i := range overrides { if err := overrides[i](&config); err != nil { return "", err } } dataDir, ok := opts[ContainerDataDirKey] if ok { config.Root = dataDir } buf := new(bytes.Buffer) err := toml.NewEncoder(buf).Encode(config) return buf.String(), err } // ContainerdKubenetOverride transforms a containerd config to set details required when using kubenet. func ContainerdKubenetOverride(config *ContainerdConfig) error { config.Plugins.IoContainerdGrpcV1Cri.CNI.ConfTemplate = "/etc/containerd/kubenet_template.conf" return nil } // ContainerdSandboxImageOverrider produces a function to transform containerd config by setting the SandboxImage. func ContainerdSandboxImageOverrider(image string) func(*ContainerdConfig) error { return func(config *ContainerdConfig) error { config.Plugins.IoContainerdGrpcV1Cri.SandboxImage = image return nil } } // DockerNvidiaOverride transforms a docker config to supply nvidia runtime configuration. func DockerNvidiaOverride(config *DockerConfig) error { if config.DockerDaemonRuntimes == nil { config.DockerDaemonRuntimes = make(map[string]DockerDaemonRuntime) } config.DefaultRuntime = "nvidia" config.DockerDaemonRuntimes["nvidia"] = DockerDaemonRuntime{ Path: "/usr/bin/nvidia-container-runtime", RuntimeArgs: []string{}, } return nil } // IndentString pads each line of an original string with N spaces and returns the new value. func IndentString(original string, spaces int) string { out := bytes.NewBuffer(nil) scanner := bufio.NewScanner(strings.NewReader(original)) for scanner.Scan() { for i := 0; i < spaces; i++ { out.WriteString(" ") } out.WriteString(scanner.Text()) out.WriteString("\n") } return out.String() } func GetDockerConfigTestCases() map[string]string { return map[string]string{ "default": defaultDockerConfigString, "gpu": dockerNvidiaConfigString, "reroot": dockerRerootConfigString, "all": dockerAllConfigString, } } func GetContainerdConfigTestCases() map[string]string { return map[string]string{ "default": containerdImageConfigString, "kubenet": containerdImageKubenetConfigString, "reroot": containerdImageRerootConfigString, "all": containerdAllConfigString, } } var defaultContainerdConfigString = `oom_score = 0 version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] [plugins."io.containerd.grpc.v1.cri".cni] [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdRerootConfigString = `oom_score = 0 root = "/mnt/containerd" version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] [plugins."io.containerd.grpc.v1.cri".cni] [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdKubenetConfigString = `oom_score = 0 version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] [plugins."io.containerd.grpc.v1.cri".cni] conf_template = "/etc/containerd/kubenet_template.conf" [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdImageConfigString = `oom_score = 0 version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "foo/oss/kubernetes/pause:3.8" [plugins."io.containerd.grpc.v1.cri".cni] [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdImageRerootConfigString = `oom_score = 0 root = "/mnt/containerd" version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "foo/oss/kubernetes/pause:3.8" [plugins."io.containerd.grpc.v1.cri".cni] [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdImageKubenetConfigString = `oom_score = 0 version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "foo/oss/kubernetes/pause:3.8" [plugins."io.containerd.grpc.v1.cri".cni] conf_template = "/etc/containerd/kubenet_template.conf" [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var containerdAllConfigString = `oom_score = 0 root = "/mnt/containerd" version = 2 [plugins] [plugins."io.containerd.grpc.v1.cri"] sandbox_image = "foo/oss/kubernetes/pause:3.8" [plugins."io.containerd.grpc.v1.cri".cni] conf_template = "/etc/containerd/kubenet_template.conf" [plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.untrusted] runtime_type = "io.containerd.runc.v2" ` var defaultDockerConfigString = `{ "live-restore": true, "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }` var dockerRerootConfigString = `{ "data-root": "/mnt/docker", "live-restore": true, "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }` var dockerNvidiaConfigString = `{ "live-restore": true, "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" }, "default-runtime": "nvidia", "runtimes": { "nvidia": { "path": "/usr/bin/nvidia-container-runtime", "runtimeArgs": [] } } }` var dockerAllConfigString = `{ "data-root": "/mnt/docker", "live-restore": true, "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" }, "default-runtime": "nvidia", "runtimes": { "nvidia": { "path": "/usr/bin/nvidia-container-runtime", "runtimeArgs": [] } } }` // ShouldDisablePodSecurityPolicyAddon returns true if the PodSecurityPolicyAddon should be forcefully disabled func ShouldDisablePodSecurityPolicyAddon(version string) bool { return IsKubernetesVersionGe(version, PodSecurityPolicyRemovedVersion) }