pkg/api/vlabs/validate.go (1,715 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
package vlabs
import (
"encoding/base64"
"fmt"
"net"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"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/Azure/aks-engine-azurestack/pkg/versions"
compute "github.com/Azure/azure-sdk-for-go/profile/p20200901/resourcemanager/compute/armcompute"
"github.com/blang/semver"
"github.com/google/uuid"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
validator "gopkg.in/go-playground/validator.v9"
)
var (
validate *validator.Validate
keyvaultIDRegex *regexp.Regexp
labelValueRegex *regexp.Regexp
labelKeyRegex *regexp.Regexp
diskEncryptionSetIDRegex *regexp.Regexp
proximityPlacementGroupIDRegex *regexp.Regexp
// Any version has to be available in a container image from mcr.microsoft.com/oss/etcd-io/etcd:v[Version]
etcdValidVersions = [...]string{"2.2.5", "2.3.0", "2.3.1", "2.3.2", "2.3.3", "2.3.4", "2.3.5", "2.3.6", "2.3.7", "2.3.8",
"3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17",
"3.1.0", "3.1.1", "3.1.2", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10",
"3.2.0", "3.2.1", "3.2.2", "3.2.3", "3.2.4", "3.2.5", "3.2.6", "3.2.7", "3.2.8", "3.2.9", "3.2.11", "3.2.12",
"3.2.13", "3.2.14", "3.2.15", "3.2.16", "3.2.23", "3.2.24", "3.2.25", "3.2.26", "3.3.0", "3.3.1", "3.3.8", "3.3.9", "3.3.10", "3.3.13", "3.3.15", "3.3.18", "3.3.19", "3.3.22", "3.3.25"}
containerdValidVersions = [...]string{"1.3.2", "1.3.3", "1.3.4", "1.3.5", "1.3.6", "1.3.7", "1.3.8", "1.3.9", "1.4.4", "1.4.6", "1.4.7", "1.4.8", "1.4.9", "1.4.11", "1.5.11", "1.5.13", "1.5.16", "1.6.21", "1.6.28", "1.6.36"}
kubernetesImageBaseTypeValidVersions = [...]string{"", common.KubernetesImageBaseTypeGCR, common.KubernetesImageBaseTypeMCR}
cachingTypesValidValues = [...]string{"", string(compute.CachingTypesNone), string(compute.CachingTypesReadWrite), string(compute.CachingTypesReadOnly)}
linuxEth0MTUAllowedValues = [...]int{1500, 3900}
networkPluginPlusPolicyAllowed = []k8sNetworkConfig{
{
networkPlugin: "",
networkPolicy: "",
},
{
networkPlugin: "azure",
networkPolicy: "",
},
{
networkPlugin: "azure",
networkPolicy: "azure",
},
{
networkPlugin: "kubenet",
networkPolicy: "",
},
{
networkPlugin: "flannel",
networkPolicy: "",
},
{
networkPlugin: NetworkPluginCilium,
networkPolicy: NetworkPolicyCilium,
},
{
networkPlugin: "kubenet",
networkPolicy: "calico",
},
{
networkPlugin: "azure",
networkPolicy: "calico",
},
{
networkPlugin: "",
networkPolicy: "calico",
},
{
networkPlugin: "",
networkPolicy: NetworkPolicyCilium,
},
{
networkPlugin: NetworkPluginAntrea,
networkPolicy: NetworkPolicyAntrea,
},
{
networkPlugin: "azure",
networkPolicy: NetworkPolicyAntrea,
},
{
networkPlugin: "",
networkPolicy: NetworkPolicyAntrea,
},
{
networkPlugin: "",
networkPolicy: "azure", // for backwards-compatibility w/ prior networkPolicy usage
},
{
networkPlugin: "",
networkPolicy: "none", // for backwards-compatibility w/ prior networkPolicy usage
},
}
)
const (
labelKeyPrefixMaxLength = 253
labelValueFormat = "^([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$"
labelKeyFormat = "^(([a-zA-Z0-9-]+[.])*[a-zA-Z0-9-]+[/])?([A-Za-z0-9][-A-Za-z0-9_.]{0,61})?[A-Za-z0-9]$"
)
type k8sNetworkConfig struct {
networkPlugin string
networkPolicy string
}
func init() {
validate = validator.New()
keyvaultIDRegex = regexp.MustCompile(`^/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.KeyVault/vaults/[^/\s]+$`)
labelValueRegex = regexp.MustCompile(labelValueFormat)
labelKeyRegex = regexp.MustCompile(labelKeyFormat)
diskEncryptionSetIDRegex = regexp.MustCompile(`^/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.Compute/diskEncryptionSets/[^/\s]+$`)
proximityPlacementGroupIDRegex = regexp.MustCompile(`^/subscriptions/\S+/resourceGroups/\S+/providers/Microsoft.Compute/proximityPlacementGroups/[^/\s]+$`)
}
// Validate implements APIObject
func (a *Properties) validate(isUpdate bool) error {
if e := validate.Struct(a); e != nil {
return handleValidationErrors(e.(validator.ValidationErrors))
}
if e := a.ValidateOrchestratorProfile(isUpdate); e != nil {
return e
}
if e := a.validateMasterProfile(isUpdate); e != nil {
return e
}
if e := a.validateAgentPoolProfiles(isUpdate); e != nil {
return e
}
if e := a.validateZones(); e != nil {
return e
}
if e := a.validateLinuxProfile(); e != nil {
return e
}
if e := a.validateAddons(isUpdate); e != nil {
return e
}
if e := a.validateExtensions(); e != nil {
return e
}
if e := a.validateVNET(); e != nil {
return e
}
if e := a.validateServicePrincipalProfile(); e != nil {
return e
}
if e := a.validateAADProfile(); e != nil {
return e
}
if e := a.validateCustomKubeComponent(); e != nil {
return e
}
if e := a.validateAzureStackSupport(); e != nil {
return e
}
if e := a.validateWindowsProfile(isUpdate); e != nil {
return e
}
return nil
}
func handleValidationErrors(e validator.ValidationErrors) error {
// Override any version specific validation error message
// common.HandleValidationErrors if the validation error message is general
return common.HandleValidationErrors(e)
}
// ValidateOrchestratorProfile validates the orchestrator profile and the addons dependent on the version of the orchestrator
func (a *Properties) ValidateOrchestratorProfile(isUpdate bool) error {
o := a.OrchestratorProfile
// On updates we only need to make sure there is a supported patch version for the minor version
if !isUpdate {
version := common.RationalizeReleaseAndVersion(
o.OrchestratorType,
o.OrchestratorRelease,
o.OrchestratorVersion,
isUpdate,
a.HasWindows(),
a.IsAzureStackCloud())
if a.IsAzureStackCloud() {
if version == "" && a.HasWindows() {
return errors.Errorf("the following OrchestratorProfile configuration is not supported on Azure Stack with OsType \"Windows\": OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please use one of the following versions: %v", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion, common.GetAllSupportedKubernetesVersions(false, true, true))
} else if version == "" {
return errors.Errorf("the following OrchestratorProfile configuration is not supported on Azure Stack: OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please use one of the following versions: %v", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion, common.GetAllSupportedKubernetesVersions(false, false, true))
}
} else {
if version == "" && a.HasWindows() {
return errors.Errorf("the following OrchestratorProfile configuration is not supported with OsType \"Windows\": OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please use one of the following versions: %v", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion, common.GetAllSupportedKubernetesVersions(false, true, false))
} else if version == "" {
return errors.Errorf("the following OrchestratorProfile configuration is not supported: OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please use one of the following versions: %v", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion, common.GetAllSupportedKubernetesVersions(false, false, false))
}
}
sv, err := semver.Make(version)
if err != nil {
return errors.Errorf("could not validate version %s", version)
}
if a.HasAvailabilityZones() {
minVersion, err := semver.Make("1.12.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.New("availabilityZone is only available in Kubernetes version 1.12 or greater")
}
}
if o.KubernetesConfig != nil {
err := o.KubernetesConfig.Validate(version, a.HasWindows(), a.FeatureFlags.IsIPv6DualStackEnabled(), a.FeatureFlags.IsIPv6OnlyEnabled(), isUpdate)
if err != nil {
return err
}
if o.KubernetesConfig.EnableAggregatedAPIs {
if !o.KubernetesConfig.IsRBACEnabled() {
return errors.New("enableAggregatedAPIs requires the enableRbac feature as a prerequisite")
}
}
if to.Bool(o.KubernetesConfig.EnableDataEncryptionAtRest) {
if o.KubernetesConfig.EtcdEncryptionKey != "" {
_, err = base64.StdEncoding.DecodeString(o.KubernetesConfig.EtcdEncryptionKey)
if err != nil {
return errors.New("etcdEncryptionKey must be base64 encoded. Please provide a valid base64 encoded value or leave the etcdEncryptionKey empty to auto-generate the value")
}
}
}
if to.Bool(o.KubernetesConfig.EnableEncryptionWithExternalKms) {
if to.Bool(a.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) && a.OrchestratorProfile.KubernetesConfig.UserAssignedID == "" {
log.Warnf("Clusters with enableEncryptionWithExternalKms=true and system-assigned identity are not upgradable! You will not be able to upgrade your cluster using `aks-engine-azurestack upgrade`")
}
}
if to.Bool(o.KubernetesConfig.EnablePodSecurityPolicy) {
log.Warnf("EnablePodSecurityPolicy is deprecated in favor of the addon pod-security-policy.")
if !o.KubernetesConfig.IsRBACEnabled() {
return errors.Errorf("enablePodSecurityPolicy requires the enableRbac feature as a prerequisite")
}
if len(o.KubernetesConfig.PodSecurityPolicyConfig) > 0 {
log.Warnf("Raw manifest for PodSecurityPolicy using PodSecurityPolicyConfig is deprecated in favor of the addon pod-security-policy. This will be ignored.")
}
}
if o.KubernetesConfig.LoadBalancerSku != "" {
if !strings.EqualFold(o.KubernetesConfig.LoadBalancerSku, StandardLoadBalancerSku) && !strings.EqualFold(o.KubernetesConfig.LoadBalancerSku, BasicLoadBalancerSku) {
return errors.Errorf("Invalid value for loadBalancerSku, only %s and %s are supported", StandardLoadBalancerSku, BasicLoadBalancerSku)
}
}
if o.KubernetesConfig.LoadBalancerSku == StandardLoadBalancerSku {
if !to.Bool(a.OrchestratorProfile.KubernetesConfig.ExcludeMasterFromStandardLB) {
return errors.Errorf("standard loadBalancerSku should exclude master nodes. Please set KubernetesConfig \"ExcludeMasterFromStandardLB\" to \"true\"")
}
}
if o.KubernetesConfig.LoadBalancerSku == BasicLoadBalancerSku {
if o.KubernetesConfig.LoadBalancerOutboundIPs != nil {
return errors.Errorf("kubernetesConfig.loadBalancerOutboundIPs configuration only supported for Standard loadBalancerSku=Standard")
}
}
if o.KubernetesConfig.DockerEngineVersion != "" {
log.Warnf("docker-engine is deprecated in favor of moby, but you passed in a dockerEngineVersion configuration. This will be ignored.")
}
if o.KubernetesConfig.MaximumLoadBalancerRuleCount < 0 {
return errors.New("maximumLoadBalancerRuleCount shouldn't be less than 0")
}
if o.KubernetesConfig.LoadBalancerOutboundIPs != nil {
if to.Int(o.KubernetesConfig.LoadBalancerOutboundIPs) > common.MaxLoadBalancerOutboundIPs {
return errors.Errorf("kubernetesConfig.loadBalancerOutboundIPs was set to %d, the maximum allowed is %d", to.Int(o.KubernetesConfig.LoadBalancerOutboundIPs), common.MaxLoadBalancerOutboundIPs)
}
}
// https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-outbound-rules-overview
if o.KubernetesConfig.LoadBalancerSku == StandardLoadBalancerSku && o.KubernetesConfig.OutboundRuleIdleTimeoutInMinutes != 0 && (o.KubernetesConfig.OutboundRuleIdleTimeoutInMinutes < 4 || o.KubernetesConfig.OutboundRuleIdleTimeoutInMinutes > 120) {
return errors.New("outboundRuleIdleTimeoutInMinutes shouldn't be less than 4 or greater than 120")
}
if a.IsAzureStackCloud() {
if common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.21.0") && !to.Bool(o.KubernetesConfig.UseCloudControllerManager) {
return errors.New("useCloudControllerManager should be set to true for Kubernetes v1.21+ clusters on Azure Stack Hub")
}
if common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.24.0") && o.KubernetesConfig.ContainerRuntime == Docker {
return errors.Errorf("Docker runtime is no longer supported for v1.24+ clusters, use %s containerRuntime value instead", Containerd)
}
if to.Bool(o.KubernetesConfig.UseInstanceMetadata) {
return errors.New("useInstanceMetadata shouldn't be set to true as feature not yet supported on Azure Stack")
}
if o.KubernetesConfig.EtcdDiskSizeGB != "" {
etcdDiskSizeGB, err := strconv.Atoi(o.KubernetesConfig.EtcdDiskSizeGB)
if err != nil {
return errors.Errorf("could not convert EtcdDiskSizeGB to int")
}
if etcdDiskSizeGB > MaxAzureStackManagedDiskSize {
return errors.Errorf("EtcdDiskSizeGB max size supported on Azure Stack is %d", MaxAzureStackManagedDiskSize)
}
}
}
if o.KubernetesConfig.EtcdStorageLimitGB != 0 {
if o.KubernetesConfig.EtcdStorageLimitGB > 8 {
log.Warnf("EtcdStorageLimitGB of %d is larger than the recommended maximum of 8", o.KubernetesConfig.EtcdStorageLimitGB)
}
if o.KubernetesConfig.EtcdStorageLimitGB < 2 {
return errors.Errorf("EtcdStorageLimitGB value of %d is too small, the minimum allowed is 2", o.KubernetesConfig.EtcdStorageLimitGB)
}
}
}
} else {
version := common.RationalizeReleaseAndVersion(
o.OrchestratorType,
o.OrchestratorRelease,
o.OrchestratorVersion,
false,
a.HasWindows(),
a.IsAzureStackCloud())
if version == "" {
patchVersion := common.GetValidPatchVersion(o.OrchestratorType, o.OrchestratorVersion, isUpdate, a.HasWindows(), a.IsAzureStackCloud())
// if there isn't a supported patch version for this version fail
if patchVersion == "" {
if a.HasWindows() {
return errors.Errorf("the following OrchestratorProfile configuration is not supported with Windows agentpools: OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please check supported Release or Version for this build of aks-engine", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion)
}
return errors.Errorf("the following OrchestratorProfile configuration is not supported: OrchestratorType: \"%s\", OrchestratorRelease: \"%s\", OrchestratorVersion: \"%s\". Please check supported Release or Version for this build of aks-engine", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion)
}
}
}
if a.HasFlatcar() && o.KubernetesConfig.NetworkPlugin == "azure" && o.KubernetesConfig.NetworkMode == NetworkModeBridge {
return errors.Errorf("Flatcar node pools require 'transparent' networkMode with Azure CNI")
}
return a.validateContainerRuntime(isUpdate)
}
func (a *Properties) validateMasterProfile(isUpdate bool) error {
m := a.MasterProfile
if m.Count == 1 && !isUpdate {
log.Warnf("Running only 1 control plane VM not recommended for production clusters, use 3 or 5 for control plane redundancy")
}
if m.IsVirtualMachineScaleSets() && m.VnetSubnetID != "" && m.FirstConsecutiveStaticIP != "" {
return errors.New("when masterProfile's availabilityProfile is VirtualMachineScaleSets and a vnetSubnetID is specified, the firstConsecutiveStaticIP should be empty and will be determined by an offset from the first IP in the vnetCidr")
}
if m.ImageRef != nil {
if err := m.ImageRef.validateImageNameAndGroup(); err != nil {
return err
}
}
if m.IsVirtualMachineScaleSets() {
if !isUpdate {
log.Warnf("Clusters with a VMSS control plane are not upgradable! You will not be able to upgrade your cluster using `aks-engine-azurestack upgrade`")
}
e := validateVMSS(a.OrchestratorProfile, false, m.StorageProfile, a.HasWindows(), a.IsAzureStackCloud())
if e != nil {
return e
}
if !a.IsClusterAllVirtualMachineScaleSets() {
return errors.New("VirtualMachineScaleSets for master profile must be used together with virtualMachineScaleSets for agent profiles. Set \"availabilityProfile\" to \"VirtualMachineScaleSets\" for agent profiles")
}
if a.OrchestratorProfile.KubernetesConfig != nil && to.Bool(a.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) && a.OrchestratorProfile.KubernetesConfig.UserAssignedID == "" {
return errors.New("virtualMachineScaleSets for master profile can be used only with user assigned MSI ! Please specify \"userAssignedID\" in \"kubernetesConfig\"")
}
}
if m.SinglePlacementGroup != nil && m.AvailabilityProfile == AvailabilitySet {
return errors.New("singlePlacementGroup is only supported with VirtualMachineScaleSets")
}
if e := validateProximityPlacementGroupID(m.ProximityPlacementGroupID); e != nil {
return e
}
distroValues := DistroValues
if isUpdate {
distroValues = append(distroValues, AKSDockerEngine, AKS1604Deprecated, AKS1804Deprecated)
}
if !validateDistro(m.Distro, distroValues) {
switch m.Distro {
case AKSDockerEngine, AKS1604Deprecated:
return errors.Errorf("The %s distro is deprecated, please use %s instead", m.Distro, AKSUbuntu1604)
case AKS1804Deprecated:
return errors.Errorf("The %s distro is deprecated, please use %s instead", m.Distro, AKSUbuntu1804)
default:
return errors.Errorf("The %s distro is not supported", m.Distro)
}
}
if to.Bool(m.AuditDEnabled) {
if m.Distro != "" && !m.IsUbuntu() {
return errors.Errorf("auditd was enabled for master vms, but an Ubuntu-based distro was not selected")
}
} else {
if a.FeatureFlags.IsEnforceUbuntuDisaStigEnabled() && m.Distro != "" && m.IsUbuntu() {
return errors.New("AuditD should be enabled in all Ubuntu-based pools if feature flag 'EnforceUbuntu2004DisaStig' or 'EnforceUbuntu2204DisaStig' is set")
}
}
var validOSDiskCachingType bool
for _, valid := range cachingTypesValidValues {
if valid == m.OSDiskCachingType {
validOSDiskCachingType = true
}
}
if !validOSDiskCachingType {
return errors.Errorf("Invalid masterProfile osDiskCachingType value \"%s\", please use one of the following versions: %s", m.OSDiskCachingType, cachingTypesValidValues)
}
return common.ValidateDNSPrefix(m.DNSPrefix)
}
func (a *Properties) validateAgentPoolProfiles(isUpdate bool) error {
profileNames := make(map[string]bool)
for i, agentPoolProfile := range a.AgentPoolProfiles {
if e := validatePoolName(agentPoolProfile.Name); e != nil {
return e
}
// validate os type is linux if dual stack feature is enabled
if a.FeatureFlags.IsIPv6DualStackEnabled() || a.FeatureFlags.IsIPv6OnlyEnabled() {
if agentPoolProfile.OSType == Windows {
if a.FeatureFlags.IsIPv6DualStackEnabled() && !common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.19.0") {
return errors.Errorf("Dual stack IPv6 feature is supported on Windows only from Kubernetes version 1.19, but OrchestratorProfile.OrchestratorVersion is '%s'", a.OrchestratorProfile.OrchestratorVersion)
}
if a.FeatureFlags.IsIPv6OnlyEnabled() {
return errors.Errorf("Single stack IPv6 feature is supported only with Linux, but agent pool '%s' is of os type %s", agentPoolProfile.Name, agentPoolProfile.OSType)
}
}
if agentPoolProfile.Distro == Flatcar {
return errors.Errorf("Dual stack and single stack IPv6 feature is currently supported only with Ubuntu, but agent pool '%s' is of distro type %s", agentPoolProfile.Name, agentPoolProfile.Distro)
}
}
// validate that each AgentPoolProfile Name is unique
if _, ok := profileNames[agentPoolProfile.Name]; ok {
return errors.Errorf("profile name '%s' already exists, profile names must be unique across pools", agentPoolProfile.Name)
}
profileNames[agentPoolProfile.Name] = true
if e := validatePoolOSType(agentPoolProfile.OSType); e != nil {
return e
}
if to.Bool(agentPoolProfile.AcceleratedNetworkingEnabled) || to.Bool(agentPoolProfile.AcceleratedNetworkingEnabledWindows) {
if a.IsAzureStackCloud() {
return errors.Errorf("AcceleratedNetworkingEnabled or AcceleratedNetworkingEnabledWindows shouldn't be set to true as feature is not yet supported on Azure Stack")
} else if e := validatePoolAcceleratedNetworking(agentPoolProfile.VMSize); e != nil {
return e
}
}
if to.Bool(agentPoolProfile.VMSSOverProvisioningEnabled) {
if agentPoolProfile.AvailabilityProfile == AvailabilitySet {
return errors.Errorf("You have specified VMSS Overprovisioning in agent pool %s, but you did not specify VMSS", agentPoolProfile.Name)
}
}
if to.Bool(agentPoolProfile.AuditDEnabled) {
if agentPoolProfile.Distro != "" && !agentPoolProfile.IsUbuntu() {
return errors.Errorf("You have enabled auditd in agent pool %s, but you did not specify an Ubuntu-based distro", agentPoolProfile.Name)
}
} else {
if a.FeatureFlags.IsEnforceUbuntuDisaStigEnabled() && agentPoolProfile.IsUbuntu() {
return errors.New("AuditD should be enabled in all Ubuntu-based pools if feature flag 'EnforceUbuntu2004DisaStig' or 'EnforceUbuntu2204DisaStig' is set")
}
}
if to.Bool(agentPoolProfile.EnableVMSSNodePublicIP) {
if agentPoolProfile.AvailabilityProfile == AvailabilitySet {
return errors.Errorf("You have enabled VMSS node public IP in agent pool %s, but you did not specify VMSS", agentPoolProfile.Name)
}
if !strings.EqualFold(a.OrchestratorProfile.KubernetesConfig.LoadBalancerSku, BasicLoadBalancerSku) {
return errors.Errorf("You have enabled VMSS node public IP in agent pool %s, but you did not specify Basic Load Balancer SKU", agentPoolProfile.Name)
}
}
if e := agentPoolProfile.validateOrchestratorSpecificProperties(); e != nil {
return e
}
if agentPoolProfile.ImageRef != nil {
if e := agentPoolProfile.ImageRef.validateImageNameAndGroup(); e != nil {
return e
}
}
if e := agentPoolProfile.validateAvailabilityProfile(); e != nil {
return e
}
if e := agentPoolProfile.validateRoles(); e != nil {
return e
}
if e := agentPoolProfile.validateCustomNodeLabels(); e != nil {
return e
}
if agentPoolProfile.AvailabilityProfile != AvailabilitySet {
e := validateVMSS(a.OrchestratorProfile, isUpdate, agentPoolProfile.StorageProfile, a.HasWindows(), a.IsAzureStackCloud())
if e != nil {
return e
}
}
if a.AgentPoolProfiles[i].AvailabilityProfile != a.AgentPoolProfiles[0].AvailabilityProfile {
return errors.New("mixed mode availability profiles are not allowed. Please set either VirtualMachineScaleSets or AvailabilitySet in availabilityProfile for all agent pools")
}
if a.AgentPoolProfiles[i].SinglePlacementGroup != nil && a.AgentPoolProfiles[i].AvailabilityProfile == AvailabilitySet {
return errors.New("singlePlacementGroup is only supported with VirtualMachineScaleSets")
}
distroValues := DistroValues
if isUpdate {
distroValues = append(distroValues, AKSDockerEngine, AKS1604Deprecated, AKS1804Deprecated)
}
if !validateDistro(agentPoolProfile.Distro, distroValues) {
switch agentPoolProfile.Distro {
case AKSDockerEngine, AKS1604Deprecated:
return errors.Errorf("The %s distro is deprecated, please use %s instead", agentPoolProfile.Distro, AKSUbuntu1604)
case AKS1804Deprecated:
return errors.Errorf("The %s distro is deprecated, please use %s instead", agentPoolProfile.Distro, AKSUbuntu1804)
default:
return errors.Errorf("The %s distro is not supported", agentPoolProfile.Distro)
}
}
if e := agentPoolProfile.validateLoadBalancerBackendAddressPoolIDs(); e != nil {
return e
}
if agentPoolProfile.IsEphemeral() {
log.Warnf("Ephemeral disks are enabled for Agent Pool %s. This feature in AKS-Engine is experimental, and data could be lost in some cases.", agentPoolProfile.Name)
}
if e := validateProximityPlacementGroupID(agentPoolProfile.ProximityPlacementGroupID); e != nil {
return e
}
var validOSDiskCachingType, validDataDiskCachingType bool
for _, valid := range cachingTypesValidValues {
if valid == agentPoolProfile.OSDiskCachingType {
validOSDiskCachingType = true
}
if valid == agentPoolProfile.DataDiskCachingType {
validDataDiskCachingType = true
}
}
if !validOSDiskCachingType {
return errors.Errorf("Invalid osDiskCachingType value \"%s\" for agentPoolProfile \"%s\", please use one of the following versions: %s", agentPoolProfile.OSDiskCachingType, agentPoolProfile.Name, cachingTypesValidValues)
}
if !validDataDiskCachingType {
return errors.Errorf("Invalid dataDiskCachingType value \"%s\" for agentPoolProfile \"%s\", please use one of the following versions: %s", agentPoolProfile.DataDiskCachingType, agentPoolProfile.Name, cachingTypesValidValues)
}
if agentPoolProfile.IsEphemeral() {
if agentPoolProfile.OSDiskCachingType != "" && agentPoolProfile.OSDiskCachingType != string(compute.CachingTypesReadOnly) {
return errors.Errorf("Invalid osDiskCachingType value \"%s\" for agentPoolProfile \"%s\" using Ephemeral Disk, you must use: %s", agentPoolProfile.OSDiskCachingType, agentPoolProfile.Name, string(compute.CachingTypesReadOnly))
}
}
}
return nil
}
func (a *Properties) validateZones() error {
if a.HasAvailabilityZones() {
var poolsWithZones, poolsWithoutZones []string
for _, pool := range a.AgentPoolProfiles {
if pool.HasAvailabilityZones() {
poolsWithZones = append(poolsWithZones, pool.Name)
} else {
poolsWithoutZones = append(poolsWithoutZones, pool.Name)
}
}
if !a.MastersAndAgentsUseAvailabilityZones() {
poolsWithZonesPrefix := "pool"
poolsWithoutZonesPrefix := "pool"
if len(poolsWithZones) > 1 {
poolsWithZonesPrefix = "pools"
}
if len(poolsWithoutZones) > 1 {
poolsWithoutZonesPrefix = "pools"
}
poolsWithZonesString := helpers.GetEnglishOrderedQuotedListWithOxfordCommas(poolsWithZones)
poolsWithoutZonesString := helpers.GetEnglishOrderedQuotedListWithOxfordCommas(poolsWithoutZones)
if !a.MasterProfile.HasAvailabilityZones() {
if len(poolsWithZones) == len(a.AgentPoolProfiles) {
log.Warnf("This cluster is using Availability Zones for %s %s, but not for master VMs", poolsWithZonesPrefix, poolsWithZonesString)
} else {
log.Warnf("This cluster is using Availability Zones for %s %s, but not for %s %s, nor for master VMs", poolsWithZonesPrefix, poolsWithZonesString, poolsWithoutZonesPrefix, poolsWithoutZonesString)
}
} else {
if len(poolsWithoutZones) > 0 {
log.Warnf("This cluster is using Availability Zones for master VMs, but not for %s %s", poolsWithoutZonesPrefix, poolsWithoutZonesString)
}
}
} else {
// agent pool profiles
for _, agentPoolProfile := range a.AgentPoolProfiles {
if agentPoolProfile.AvailabilityProfile == AvailabilitySet {
return errors.New("Availability Zones are not supported with an AvailabilitySet. Please either remove availabilityProfile or set availabilityProfile to VirtualMachineScaleSets")
}
}
if a.OrchestratorProfile.KubernetesConfig != nil && a.OrchestratorProfile.KubernetesConfig.LoadBalancerSku != "" && !strings.EqualFold(a.OrchestratorProfile.KubernetesConfig.LoadBalancerSku, StandardLoadBalancerSku) {
return errors.New("Availability Zones requires Standard LoadBalancer. Please set KubernetesConfig \"LoadBalancerSku\" to \"Standard\"")
}
}
}
return nil
}
func (a *Properties) validateLinuxProfile() error {
var validEth0MTU bool
if a.LinuxProfile.Eth0MTU != 0 {
if a.OrchestratorProfile != nil &&
a.OrchestratorProfile.KubernetesConfig != nil &&
a.OrchestratorProfile.KubernetesConfig.NetworkPlugin == NetworkPluginKubenet {
return errors.Errorf("Custom linuxProfile eth0MTU value not allowed when using Kubenet")
}
for _, valid := range linuxEth0MTUAllowedValues {
if valid == a.LinuxProfile.Eth0MTU {
validEth0MTU = true
break
}
}
if !validEth0MTU {
allowedMTUs := ""
for _, mtu := range linuxEth0MTUAllowedValues {
allowedMTUs += strconv.Itoa(mtu) + ", "
}
allowedMTUs = strings.TrimRight(allowedMTUs, ", ")
return errors.Errorf("Invalid linuxProfile eth0MTU value \"%d\", please use one of the following values: %s", a.LinuxProfile.Eth0MTU, allowedMTUs)
}
}
for _, publicKey := range a.LinuxProfile.SSH.PublicKeys {
if e := validate.Var(publicKey.KeyData, "required"); e != nil {
return errors.New("KeyData in LinuxProfile.SSH.PublicKeys cannot be empty string")
}
}
if a.LinuxProfile.EnableUnattendedUpgrades == nil {
log.Warnf("linuxProfile.enableUnattendedUpgrades configuration was not declared, your cluster nodes will be configured to run unattended-upgrade by default")
}
return validateKeyVaultSecrets(a.LinuxProfile.Secrets, false)
}
func (a *Properties) validateAddons(isUpdate bool) error {
if a.OrchestratorProfile.KubernetesConfig != nil && a.OrchestratorProfile.KubernetesConfig.Addons != nil {
var isAvailabilitySets bool
var kubeDNSEnabled bool
var corednsEnabled bool
for _, agentPool := range a.AgentPoolProfiles {
if agentPool.IsAvailabilitySets() {
isAvailabilitySets = true
}
}
for _, addon := range a.OrchestratorProfile.KubernetesConfig.Addons {
if addon.Data != "" {
if len(addon.Config) > 0 || len(addon.Containers) > 0 {
return errors.New("Config and containers should be empty when addon.Data is specified")
}
if _, err := base64.StdEncoding.DecodeString(addon.Data); err != nil {
return errors.Errorf("Addon %s's data should be base64 encoded", addon.Name)
}
}
if addon.Mode != "" {
if addon.Mode != AddonModeEnsureExists && addon.Mode != AddonModeReconcile {
return errors.Errorf("addon %s has a mode configuration '%s', must be either %s or %s", addon.Name, addon.Mode, AddonModeEnsureExists, AddonModeReconcile)
}
}
// Validation for addons if they are enabled
if to.Bool(addon.Enabled) {
switch addon.Name {
case "cluster-autoscaler":
if isAvailabilitySets {
return errors.Errorf("cluster-autoscaler addon can only be used with VirtualMachineScaleSets. Please specify \"availabilityProfile\": \"%s\"", VirtualMachineScaleSets)
}
for _, pool := range addon.Pools {
if pool.Name == "" {
return errors.Errorf("cluster-autoscaler addon pools configuration must have a 'name' property that correlates with a pool name in the agentPoolProfiles array")
}
if a.GetAgentPoolByName(pool.Name) == nil {
return errors.Errorf("cluster-autoscaler addon pool 'name' %s does not match any agentPoolProfiles nodepool name", pool.Name)
}
if pool.Config != nil {
var min, max int
var err error
if pool.Config["min-nodes"] != "" {
min, err = strconv.Atoi(pool.Config["min-nodes"])
if err != nil {
return errors.Errorf("cluster-autoscaler addon pool 'name' %s has invalid 'min-nodes' config, must be a string int, got %s", pool.Name, pool.Config["min-nodes"])
}
}
if pool.Config["max-nodes"] != "" {
max, err = strconv.Atoi(pool.Config["max-nodes"])
if err != nil {
return errors.Errorf("cluster-autoscaler addon pool 'name' %s has invalid 'max-nodes' config, must be a string int, got %s", pool.Name, pool.Config["max-nodes"])
}
}
if min > max {
return errors.Errorf("cluster-autoscaler addon pool 'name' %s has invalid config, 'max-nodes' %d must be greater than or equal to 'min-nodes' %d", pool.Name, max, min)
}
}
}
case "aad":
if !a.HasAADAdminGroupID() {
return errors.New("aad addon can't be enabled without a valid aadProfile w/ adminGroupID")
}
case "appgw-ingress":
if (a.ServicePrincipalProfile == nil || len(a.ServicePrincipalProfile.ObjectID) == 0) &&
!to.Bool(a.OrchestratorProfile.KubernetesConfig.UseManagedIdentity) {
return errors.New("appgw-ingress add-ons requires 'objectID' to be specified or UseManagedIdentity to be true")
}
if a.OrchestratorProfile.KubernetesConfig.NetworkPlugin != "azure" {
return errors.New("appgw-ingress add-ons can only be used with Network Plugin as 'azure'")
}
if len(addon.Config["appgw-subnet"]) == 0 {
return errors.New("appgw-ingress add-ons requires 'appgw-subnet' in the Config. It is used to provision the subnet for Application Gateway in the vnet")
}
case "cloud-node-manager":
if !to.Bool(a.OrchestratorProfile.KubernetesConfig.UseCloudControllerManager) {
return errors.Errorf("%s add-on requires useCloudControllerManager to be true", addon.Name)
}
if !a.ShouldEnableAzureCloudAddon(addon.Name) {
minVersion := "1.16.0"
if a.HasWindows() {
minVersion = "1.18.0"
}
return errors.Errorf("%s add-on can only be used Kubernetes %s or above", addon.Name, minVersion)
}
case common.CiliumAddonName:
if !common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.16.0") {
if a.OrchestratorProfile.KubernetesConfig.NetworkPolicy != NetworkPolicyCilium {
return errors.Errorf("%s addon may only be enabled if the networkPolicy=%s", common.CiliumAddonName, NetworkPolicyCilium)
}
} else {
return errors.Errorf("%s addon is not supported on Kubernetes v1.16.0 or greater", common.CiliumAddonName)
}
case common.AntreaAddonName:
if a.OrchestratorProfile.KubernetesConfig.NetworkPolicy != NetworkPolicyAntrea {
return errors.Errorf("%s addon may only be enabled if the networkPolicy=%s", common.AntreaAddonName, NetworkPolicyAntrea)
}
case common.FlannelAddonName:
if isUpdate {
if a.OrchestratorProfile.KubernetesConfig.NetworkPolicy != "" {
return errors.Errorf("%s addon does not support NetworkPolicy, replace %s with \"\"", common.FlannelAddonName, a.OrchestratorProfile.KubernetesConfig.NetworkPolicy)
}
networkPlugin := a.OrchestratorProfile.KubernetesConfig.NetworkPlugin
if networkPlugin != "" {
if networkPlugin != NetworkPluginFlannel {
return errors.Errorf("%s addon is not supported with networkPlugin=%s, please use networkPlugin=%s", common.FlannelAddonName, networkPlugin, NetworkPluginFlannel)
}
}
if a.OrchestratorProfile.KubernetesConfig.ContainerRuntime != Containerd {
return errors.Errorf("%s addon is only supported with containerRuntime=%s", common.FlannelAddonName, Containerd)
}
} else {
return errors.Errorf("%s addon is deprecated for new clusters", common.FlannelAddonName)
}
case common.KubeDNSAddonName:
kubeDNSEnabled = true
case common.CoreDNSAddonName:
corednsEnabled = true
case common.SecretsStoreCSIDriverAddonName:
if !common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.16.0") {
return errors.Errorf("%s add-on can only be used in 1.16+", addon.Name)
}
case common.PodSecurityPolicyAddonName:
if common.ShouldDisablePodSecurityPolicyAddon(a.OrchestratorProfile.OrchestratorVersion) {
log.Warn("The PodSecurityPolicy admission was removed in Kubernetes v1.25+. " +
"The pod security standards will be enforced by the built-in PodSecurity admission controller instead. " +
"See https://github.com/Azure/aks-engine-azurestack/blob/master/docs/topics/pod-security.md")
}
case common.AzureArcOnboardingAddonName:
if err := addon.validateArcAddonConfig(); err != nil {
return err
}
case common.ReschedulerAddonName:
if isUpdate {
log.Warnf("The rescheduler addon has been deprecated and disabled, it will be removed during this update")
}
return errors.Errorf("The rescheduler addon has been deprecated and disabled, please remove it from your cluster configuration before creating a new cluster")
case common.ContainerMonitoringAddonName:
if isUpdate {
log.Warnf("The container monitoring addon has been deprecated and disabled, it will be removed during this update")
}
return errors.Errorf("The container monitoring addon has been deprecated and disabled, please remove it from your cluster configuration before creating a new cluster")
case common.DashboardAddonName:
log.Warnf("The kube-dashboard addon is deprecated, we recommend you install the dashboard yourself, see https://github.com/kubernetes/dashboard")
case common.AzureCNINetworkMonitorAddonName:
if isUpdate {
log.Warnf("The Azure CNI networkmonitor addon has been deprecated, it will be marked as disabled")
}
}
} else {
// Validation for addons if they are disabled
switch addon.Name {
case "cloud-node-manager":
if a.ShouldEnableAzureCloudAddon(addon.Name) && !a.IsAzureStackCloud() {
minVersion := "1.16.0"
if a.HasWindows() {
minVersion = "1.18.0"
}
return errors.Errorf("%s add-on is required when useCloudControllerManager is true in Kubernetes %s or above", addon.Name, minVersion)
}
case common.AzureCloudProviderAddonName:
return errors.Errorf("%s add-on is required, it cannot be disabled", addon.Name)
}
}
}
if kubeDNSEnabled && corednsEnabled {
return errors.New("Both kube-dns and coredns addons are enabled, only one of these may be enabled on a cluster")
}
}
return nil
}
func (a *Properties) validateExtensions() error {
for _, agentPool := range a.AgentPoolProfiles {
if len(agentPool.Extensions) != 0 && (len(agentPool.AvailabilityProfile) == 0 || agentPool.IsVirtualMachineScaleSets()) {
return errors.Errorf("Extensions are currently not supported with VirtualMachineScaleSets. Please specify \"availabilityProfile\": \"%s\"", AvailabilitySet)
}
if agentPool.OSType == Windows && len(agentPool.Extensions) != 0 {
for _, e := range agentPool.Extensions {
if e.Name == "prometheus-grafana-k8s" {
return errors.Errorf("prometheus-grafana-k8s extension is currently not supported for Windows agents")
}
}
}
}
for _, extension := range a.ExtensionProfiles {
if extension.ExtensionParametersKeyVaultRef != nil {
if e := validate.Var(extension.ExtensionParametersKeyVaultRef.VaultID, "required"); e != nil {
return errors.Errorf("the Keyvault ID must be specified for Extension %s", extension.Name)
}
if e := validate.Var(extension.ExtensionParametersKeyVaultRef.SecretName, "required"); e != nil {
return errors.Errorf("the Keyvault Secret must be specified for Extension %s", extension.Name)
}
if !keyvaultIDRegex.MatchString(extension.ExtensionParametersKeyVaultRef.VaultID) {
return errors.Errorf("Extension %s's keyvault secret reference is of incorrect format", extension.Name)
}
}
}
return nil
}
func (a *Properties) validateVNET() error {
isCustomVNET := a.MasterProfile.IsCustomVNET()
for _, agentPool := range a.AgentPoolProfiles {
if agentPool.IsCustomVNET() != isCustomVNET {
return errors.New("Multiple VNET Subnet configurations specified. The master profile and each agent pool profile must all specify a custom VNET Subnet, or none at all")
}
}
if isCustomVNET {
if a.MasterProfile.IsVirtualMachineScaleSets() && a.MasterProfile.AgentVnetSubnetID == "" {
return errors.New("when master profile is using VirtualMachineScaleSets and is custom vnet, set \"vnetsubnetid\" and \"agentVnetSubnetID\" for master profile")
}
subscription, resourcegroup, vnetname, _, e := common.GetVNETSubnetIDComponents(a.MasterProfile.VnetSubnetID)
if e != nil {
return e
}
for _, agentPool := range a.AgentPoolProfiles {
agentSubID, agentRG, agentVNET, _, err := common.GetVNETSubnetIDComponents(agentPool.VnetSubnetID)
if err != nil {
return err
}
if agentSubID != subscription ||
agentRG != resourcegroup ||
agentVNET != vnetname {
return errors.New("Multiple VNETS specified. The master profile and each agent pool must reference the same VNET (but it is ok to reference different subnets on that VNET)")
}
}
masterFirstIP := net.ParseIP(a.MasterProfile.FirstConsecutiveStaticIP)
if masterFirstIP == nil && !a.MasterProfile.IsVirtualMachineScaleSets() {
return errors.Errorf("MasterProfile.FirstConsecutiveStaticIP (with VNET Subnet specification) '%s' is an invalid IP address", a.MasterProfile.FirstConsecutiveStaticIP)
}
if a.MasterProfile.VnetCidr != "" {
_, _, err := net.ParseCIDR(a.MasterProfile.VnetCidr)
if err != nil {
return errors.Errorf("MasterProfile.VnetCidr '%s' contains invalid cidr notation", a.MasterProfile.VnetCidr)
}
}
}
return nil
}
func (a *Properties) validateServicePrincipalProfile() error {
useManagedIdentityDisabled := a.OrchestratorProfile.KubernetesConfig != nil &&
a.OrchestratorProfile.KubernetesConfig.UseManagedIdentity != nil && !to.Bool(a.OrchestratorProfile.KubernetesConfig.UseManagedIdentity)
if useManagedIdentityDisabled {
if a.ServicePrincipalProfile == nil {
return errors.Errorf("ServicePrincipalProfile must be specified")
}
if e := validate.Var(a.ServicePrincipalProfile.ClientID, "required"); e != nil {
return errors.Errorf("the service principal client ID must be specified")
}
if (len(a.ServicePrincipalProfile.Secret) == 0 && a.ServicePrincipalProfile.KeyvaultSecretRef == nil) ||
(len(a.ServicePrincipalProfile.Secret) != 0 && a.ServicePrincipalProfile.KeyvaultSecretRef != nil) {
return errors.Errorf("either the service principal client secret or keyvault secret reference must be specified")
}
if a.OrchestratorProfile.KubernetesConfig != nil && to.Bool(a.OrchestratorProfile.KubernetesConfig.EnableEncryptionWithExternalKms) && len(a.ServicePrincipalProfile.ObjectID) == 0 {
return errors.Errorf("the service principal object ID must be specified when enableEncryptionWithExternalKms is true")
}
if a.ServicePrincipalProfile.KeyvaultSecretRef != nil {
if e := validate.Var(a.ServicePrincipalProfile.KeyvaultSecretRef.VaultID, "required"); e != nil {
return errors.Errorf("the Keyvault ID must be specified for the Service Principle")
}
if e := validate.Var(a.ServicePrincipalProfile.KeyvaultSecretRef.SecretName, "required"); e != nil {
return errors.Errorf("the Keyvault Secret must be specified for the Service Principle")
}
if !keyvaultIDRegex.MatchString(a.ServicePrincipalProfile.KeyvaultSecretRef.VaultID) {
return errors.Errorf("service principal client keyvault secret reference is of incorrect format")
}
}
}
return nil
}
func (a *Properties) validateAADProfile() error {
if profile := a.AADProfile; profile != nil {
if _, err := uuid.Parse(profile.ClientAppID); err != nil {
return errors.Errorf("clientAppID '%v' is invalid", profile.ClientAppID)
}
if _, err := uuid.Parse(profile.ServerAppID); err != nil {
return errors.Errorf("serverAppID '%v' is invalid", profile.ServerAppID)
}
if len(profile.TenantID) > 0 {
if _, err := uuid.Parse(profile.TenantID); err != nil {
return errors.Errorf("tenantID '%v' is invalid", profile.TenantID)
}
}
if len(profile.AdminGroupID) > 0 {
if _, err := uuid.Parse(profile.AdminGroupID); err != nil {
return errors.Errorf("adminGroupID '%v' is invalid", profile.AdminGroupID)
}
}
}
return nil
}
func (a *AgentPoolProfile) validateAvailabilityProfile() error {
switch a.AvailabilityProfile {
case AvailabilitySet:
case VirtualMachineScaleSets:
case "":
default:
{
return errors.Errorf("unknown availability profile type '%s' for agent pool '%s'. Specify either %s, or %s", a.AvailabilityProfile, a.Name, AvailabilitySet, VirtualMachineScaleSets)
}
}
return nil
}
func (a *AgentPoolProfile) validateRoles() error {
validRoles := []AgentPoolProfileRole{AgentPoolProfileRoleEmpty}
var found bool
for _, validRole := range validRoles {
if a.Role == validRole {
found = true
break
}
}
if !found {
return errors.Errorf("Role %q is not supported", a.Role)
}
return nil
}
func (a *AgentPoolProfile) validateCustomNodeLabels() error {
if len(a.CustomNodeLabels) > 0 {
for k, v := range a.CustomNodeLabels {
if e := validateKubernetesLabelKey(k); e != nil {
return e
}
if e := validateKubernetesLabelValue(v); e != nil {
return e
}
}
}
return nil
}
func validateVMSS(o *OrchestratorProfile, isUpdate bool, storageProfile string, hasWindows bool, isAzureStackCloud bool) error {
version := common.RationalizeReleaseAndVersion(
o.OrchestratorType,
o.OrchestratorRelease,
o.OrchestratorVersion,
isUpdate,
hasWindows,
isAzureStackCloud)
if version == "" {
return errors.Errorf("the following OrchestratorProfile configuration is not supported: OrchestratorType: %s, OrchestratorRelease: %s, OrchestratorVersion: %s. Please check supported Release or Version for this build of aks-engine", o.OrchestratorType, o.OrchestratorRelease, o.OrchestratorVersion)
}
sv, err := semver.Make(version)
if err != nil {
return errors.Errorf("could not validate version %s", version)
}
minVersion, err := semver.Make("1.10.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.Errorf("VirtualMachineScaleSets are only available in Kubernetes version %s or greater. Please set \"orchestratorVersion\" to %s or above", minVersion.String(), minVersion.String())
}
// validation for instanceMetadata using VMSS with Kubernetes
minVersion, err = semver.Make("1.10.2")
if err != nil {
return errors.New("could not validate version")
}
if o.KubernetesConfig != nil && o.KubernetesConfig.UseInstanceMetadata != nil {
if *o.KubernetesConfig.UseInstanceMetadata && sv.LT(minVersion) {
return errors.Errorf("VirtualMachineScaleSets with instance metadata is supported for Kubernetes version %s or greater. Please set \"useInstanceMetadata\": false in \"kubernetesConfig\" or set \"orchestratorVersion\" to %s or above", minVersion.String(), minVersion.String())
}
}
if storageProfile == StorageAccount {
return errors.Errorf("VirtualMachineScaleSets does not support %s disks. Please specify \"storageProfile\": \"%s\" (recommended) or \"availabilityProfile\": \"%s\"", StorageAccount, ManagedDisks, AvailabilitySet)
}
return nil
}
func (a *Properties) validateWindowsProfile(isUpdate bool) error {
hasWindowsAgentPools := false
for _, profile := range a.AgentPoolProfiles {
if profile.OSType == Windows {
hasWindowsAgentPools = true
break
}
}
if !hasWindowsAgentPools {
return nil
}
o := a.OrchestratorProfile
version := common.RationalizeReleaseAndVersion(
o.OrchestratorType,
o.OrchestratorRelease,
o.OrchestratorVersion,
isUpdate,
hasWindowsAgentPools,
a.IsAzureStackCloud())
if version == "" {
return errors.Errorf("Orchestrator %s version %s does not support Windows", o.OrchestratorType, o.OrchestratorVersion)
}
w := a.WindowsProfile
if w == nil {
return errors.New("WindowsProfile is required when the cluster definition contains Windows agent pools")
}
if e := validate.Var(w.AdminUsername, "required"); e != nil {
return errors.New("WindowsProfile.AdminUsername is required, when agent pool specifies Windows")
}
if e := validate.Var(w.AdminPassword, "required"); e != nil {
return errors.New("WindowsProfile.AdminPassword is required, when agent pool specifies Windows")
}
if !validatePasswordComplexity(w.AdminUsername, w.AdminPassword) {
return errors.New("WindowsProfile.AdminPassword complexity not met. Windows password should contain 3 of the following categories - uppercase letters(A-Z), lowercase(a-z) letters, digits(0-9), special characters (~!@#$%^&*_-+=`|\\(){}[]:;<>,.?/')")
}
if e := validateKeyVaultSecrets(w.Secrets, true); e != nil {
return e
}
if e := validateCsiProxyWindowsProperties(w, version); e != nil {
return e
}
if e := validateWindowsRuntimes(w.WindowsRuntimes); e != nil {
return e
}
return nil
}
func validateCsiProxyWindowsProperties(w *WindowsProfile, k8sVersion string) error {
if w.IsCSIProxyEnabled() && !common.IsKubernetesVersionGe(k8sVersion, "1.18.0") {
return errors.New("CSI proxy for Windows is only available in Kubernetes versions 1.18.0 or greater")
}
return nil
}
func validateWindowsRuntimes(r *WindowsRuntimes) error {
if r == nil {
// can be blank defaults will be applied
return nil
}
if r.Default != "process" && r.Default != "hyperv" {
return errors.New("Default runtime types are process or hyperv")
}
if r.HypervRuntimes != nil {
handlersMap := make(map[string]bool)
for _, h := range r.HypervRuntimes {
if h.BuildNumber != "17763" && h.BuildNumber != "18362" && h.BuildNumber != "18363" && h.BuildNumber != "19041" {
return errors.New("Current hyper-v build id values supported are 17763, 18362, 18363, 19041")
}
if _, ok := handlersMap[h.BuildNumber]; ok {
return errors.Errorf("Hyper-v RuntimeHandlers have duplicate runtime with build number '%s', Windows Runtimes must be unique", h.BuildNumber)
}
handlersMap[h.BuildNumber] = true
}
}
return nil
}
func (a *AgentPoolProfile) validateOrchestratorSpecificProperties() error {
if e := validate.Var(a.DNSPrefix, "len=0"); e != nil {
return errors.New("AgentPoolProfile.DNSPrefix must be empty for Kubernetes")
}
if e := validate.Var(a.Ports, "len=0"); e != nil {
return errors.New("AgentPoolProfile.Ports must be empty for Kubernetes")
}
if validate.Var(a.ScaleSetPriority, "eq=Regular") == nil && validate.Var(a.ScaleSetEvictionPolicy, "len=0") != nil {
return errors.New("property 'AgentPoolProfile.ScaleSetEvictionPolicy' must be empty for AgentPoolProfile.Priority of Regular")
}
if a.DNSPrefix != "" {
if e := common.ValidateDNSPrefix(a.DNSPrefix); e != nil {
return e
}
if len(a.Ports) > 0 {
if e := validateUniquePorts(a.Ports, a.Name); e != nil {
return e
}
} else {
a.Ports = []int{80, 443, 8080}
}
} else if e := validate.Var(a.Ports, "len=0"); e != nil {
return errors.Errorf("AgentPoolProfile.Ports must be empty when AgentPoolProfile.DNSPrefix is empty")
}
if len(a.DiskSizesGB) > 0 {
if e := validate.Var(a.StorageProfile, "eq=StorageAccount|eq=ManagedDisks"); e != nil {
return errors.Errorf("property 'StorageProfile' must be set to either '%s' or '%s' when attaching disks", StorageAccount, ManagedDisks)
}
if e := validate.Var(a.AvailabilityProfile, "eq=VirtualMachineScaleSets|eq=AvailabilitySet"); e != nil {
return errors.Errorf("property 'AvailabilityProfile' must be set to either '%s' or '%s' when attaching disks", VirtualMachineScaleSets, AvailabilitySet)
}
if a.StorageProfile == StorageAccount && (a.AvailabilityProfile != AvailabilitySet) {
return errors.Errorf("VirtualMachineScaleSets does not support storage account attached disks. Instead specify 'StorageAccount': '%s' or specify AvailabilityProfile '%s'", ManagedDisks, AvailabilitySet)
}
}
if a.DiskEncryptionSetID != "" {
if !diskEncryptionSetIDRegex.MatchString(a.DiskEncryptionSetID) {
return errors.Errorf("DiskEncryptionSetID(%s) is of incorrect format, correct format: %s", a.DiskEncryptionSetID, diskEncryptionSetIDRegex.String())
}
}
return nil
}
func (a *AgentPoolProfile) validateLoadBalancerBackendAddressPoolIDs() error {
if a.LoadBalancerBackendAddressPoolIDs != nil {
for _, backendPoolID := range a.LoadBalancerBackendAddressPoolIDs {
if len(backendPoolID) == 0 {
return errors.Errorf("AgentPoolProfile.LoadBalancerBackendAddressPoolIDs can not contain empty string. Agent pool name: %s", a.Name)
}
}
}
return nil
}
func validateProximityPlacementGroupID(ppgID string) error {
if ppgID != "" {
if !proximityPlacementGroupIDRegex.MatchString(ppgID) {
return errors.Errorf("ProximityPlacementGroupID(%s) is of incorrect format, correct format: %s", ppgID, proximityPlacementGroupIDRegex.String())
}
}
return nil
}
func validateKeyVaultSecrets(secrets []KeyVaultSecrets, requireCertificateStore bool) error {
for _, s := range secrets {
if len(s.VaultCertificates) == 0 {
return errors.New("Valid KeyVaultSecrets must have no empty VaultCertificates")
}
if s.SourceVault == nil {
return errors.New("missing SourceVault in KeyVaultSecrets")
}
if s.SourceVault.ID == "" {
return errors.New("KeyVaultSecrets must have a SourceVault.ID")
}
for _, c := range s.VaultCertificates {
if _, e := url.Parse(c.CertificateURL); e != nil {
return errors.Errorf("Certificate url was invalid. received error %s", e)
}
if e := validateName(c.CertificateStore, "KeyVaultCertificate.CertificateStore"); requireCertificateStore && e != nil {
return errors.Errorf("%s for certificates in a WindowsProfile", e)
}
}
}
return nil
}
func validatePasswordComplexity(name string, password string) (out bool) {
if strings.EqualFold(name, password) {
return false
}
if len(password) == 0 {
return false
}
hits := 0
if regexp.MustCompile(`[0-9]+`).MatchString(password) {
hits++
}
if regexp.MustCompile(`[A-Z]+`).MatchString(password) {
hits++
}
if regexp.MustCompile(`[a-z]`).MatchString(password) {
hits++
}
if regexp.MustCompile(`[~!@#\$%\^&\*_\-\+=\x60\|\(\){}\[\]:;"'<>,\.\?/]+`).MatchString(password) {
hits++
}
return hits > 2
}
// Validate validates the KubernetesConfig
func (k *KubernetesConfig) Validate(k8sVersion string, hasWindows, ipv6DualStackEnabled, isIPv6, isUpdate bool) error {
// number of minimum retries allowed for kubelet to post node status
const minKubeletRetries = 4
// enableIPv6DualStack and enableIPv6Only are mutually exclusive feature flags
if ipv6DualStackEnabled && isIPv6 {
return errors.Errorf("featureFlags.EnableIPv6DualStack and featureFlags.EnableIPv6Only can't be enabled at the same time")
}
sv, err := semver.Make(k8sVersion)
if err != nil {
return errors.Errorf("could not validate version %s", k8sVersion)
}
if ipv6DualStackEnabled {
minVersion, err := semver.Make("1.16.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.Errorf("IPv6 dual stack not available in kubernetes version %s", k8sVersion)
}
// ipv6 dual stack feature is currently only supported with kubenet
if k.NetworkPlugin != "kubenet" && k.NetworkPlugin != "azure" {
return errors.Errorf("the OrchestratorProfile.KubernetesConfig.NetworkPlugin '%s' is invalid, IPv6 dual stack supported only with 'kubenet' and 'azure'", k.NetworkPlugin)
}
if k.NetworkPlugin == "azure" && k.NetworkPolicy != "" {
return errors.Errorf("Network policy %s is not supported for azure cni dualstack", k.NetworkPolicy)
}
}
if isIPv6 {
minVersion, err := semver.Make("1.18.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.Errorf("IPv6 single stack not available in kubernetes version %s", k8sVersion)
}
// single stack IPv6 feature is currently only supported with kubenet
if k.NetworkPlugin != "kubenet" {
return errors.Errorf("the OrchestratorProfile.KubernetesConfig.NetworkPlugin '%s' is invalid, IPv6 single stack supported only with kubenet", k.NetworkPlugin)
}
}
if k.ClusterSubnet != "" {
clusterSubnets := strings.Split(k.ClusterSubnet, ",")
if !ipv6DualStackEnabled && len(clusterSubnets) > 1 {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ClusterSubnet '%s' is an invalid subnet", k.ClusterSubnet)
}
if ipv6DualStackEnabled && len(clusterSubnets) > 2 {
return errors.Errorf("the OrchestratorProfile.KubernetesConfig.ClusterSubnet '%s' is an invalid subnet, not more than 2 subnets for ipv6 dual stack", k.ClusterSubnet)
}
for _, clusterSubnet := range clusterSubnets {
_, subnet, err := net.ParseCIDR(clusterSubnet)
if err != nil {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ClusterSubnet '%s' is an invalid subnet", clusterSubnet)
}
if k.NetworkPlugin == "azure" {
ones, bits := subnet.Mask.Size()
if bits-ones <= 8 {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ClusterSubnet '%s' must reserve at least 9 bits for nodes", clusterSubnet)
}
}
}
}
if k.DockerBridgeSubnet != "" {
_, _, err := net.ParseCIDR(k.DockerBridgeSubnet)
if err != nil {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.DockerBridgeSubnet '%s' is an invalid subnet", k.DockerBridgeSubnet)
}
}
if k.MaxPods != 0 {
if k.MaxPods < KubernetesMinMaxPods {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.MaxPods '%v' must be at least %v", k.MaxPods, KubernetesMinMaxPods)
}
}
if k.KubeletConfig != nil {
if _, ok := k.KubeletConfig["--node-status-update-frequency"]; ok {
val := k.KubeletConfig["--node-status-update-frequency"]
_, err := time.ParseDuration(val)
if err != nil {
return errors.Errorf("--node-status-update-frequency '%s' is not a valid duration", val)
}
}
}
if _, ok := k.ControllerManagerConfig["--node-monitor-grace-period"]; ok {
_, err := time.ParseDuration(k.ControllerManagerConfig["--node-monitor-grace-period"])
if err != nil {
return errors.Errorf("--node-monitor-grace-period '%s' is not a valid duration", k.ControllerManagerConfig["--node-monitor-grace-period"])
}
}
if k.KubeletConfig != nil {
if _, ok := k.KubeletConfig["--node-status-update-frequency"]; ok {
if _, ok := k.ControllerManagerConfig["--node-monitor-grace-period"]; ok {
nodeStatusUpdateFrequency, _ := time.ParseDuration(k.KubeletConfig["--node-status-update-frequency"])
ctrlMgrNodeMonitorGracePeriod, _ := time.ParseDuration(k.ControllerManagerConfig["--node-monitor-grace-period"])
kubeletRetries := ctrlMgrNodeMonitorGracePeriod.Seconds() / nodeStatusUpdateFrequency.Seconds()
if kubeletRetries < minKubeletRetries {
return errors.Errorf("aks-engine-azurestack requires that --node-monitor-grace-period(%f)s be larger than nodeStatusUpdateFrequency(%f)s by at least a factor of %d; ", ctrlMgrNodeMonitorGracePeriod.Seconds(), nodeStatusUpdateFrequency.Seconds(), minKubeletRetries)
}
}
}
// Re-enable this unit test if --non-masquerade-cidr is re-introduced
/*if _, ok := k.KubeletConfig["--non-masquerade-cidr"]; ok {
if _, _, err := net.ParseCIDR(k.KubeletConfig["--non-masquerade-cidr"]); err != nil {
return errors.Errorf("--non-masquerade-cidr kubelet config '%s' is an invalid CIDR string", k.KubeletConfig["--non-masquerade-cidr"])
}
}*/
}
if _, ok := k.ControllerManagerConfig["--pod-eviction-timeout"]; ok {
_, err := time.ParseDuration(k.ControllerManagerConfig["--pod-eviction-timeout"])
if err != nil {
return errors.Errorf("--pod-eviction-timeout '%s' is not a valid duration", k.ControllerManagerConfig["--pod-eviction-timeout"])
}
}
if _, ok := k.ControllerManagerConfig["--route-reconciliation-period"]; ok {
_, err := time.ParseDuration(k.ControllerManagerConfig["--route-reconciliation-period"])
if err != nil {
return errors.Errorf("--route-reconciliation-period '%s' is not a valid duration", k.ControllerManagerConfig["--route-reconciliation-period"])
}
}
if k.DNSServiceIP != "" || k.ServiceCidr != "" {
if k.DNSServiceIP == "" {
return errors.New("OrchestratorProfile.KubernetesConfig.DNSServiceIP must be specified when ServiceCidr is")
}
if k.ServiceCidr == "" {
return errors.New("OrchestratorProfile.KubernetesConfig.ServiceCidr must be specified when DNSServiceIP is")
}
dnsIP := net.ParseIP(k.DNSServiceIP)
if dnsIP == nil {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.DNSServiceIP '%s' is an invalid IP address", k.DNSServiceIP)
}
primaryServiceCIDR := k.ServiceCidr
if ipv6DualStackEnabled {
// split the service cidr to see if there are multiple cidrs
serviceCidrs := strings.Split(k.ServiceCidr, ",")
if len(serviceCidrs) > 2 {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ServiceCidr '%s' is an invalid CIDR subnet. More than 2 CIDRs not allowed for dualstack", k.ServiceCidr)
}
if len(serviceCidrs) == 2 {
firstServiceCIDR, secondServiceCIDR := serviceCidrs[0], serviceCidrs[1]
_, _, err := net.ParseCIDR(secondServiceCIDR)
if err != nil {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ServiceCidr '%s' is an invalid CIDR subnet", secondServiceCIDR)
}
// use the primary service cidr for further validation
primaryServiceCIDR = firstServiceCIDR
}
// if # of service cidrs is 1, then continues with the default validation
}
_, serviceCidr, err := net.ParseCIDR(primaryServiceCIDR)
if err != nil {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ServiceCidr '%s' is an invalid CIDR subnet", primaryServiceCIDR)
}
// Finally validate that the DNS ip is within the subnet
if !serviceCidr.Contains(dnsIP) {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.DNSServiceIP '%s' is not within the ServiceCidr '%s'", k.DNSServiceIP, primaryServiceCIDR)
}
// and that the DNS IP is _not_ the subnet broadcast address
broadcast := common.IP4BroadcastAddress(serviceCidr)
if dnsIP.Equal(broadcast) {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.DNSServiceIP '%s' cannot be the broadcast address of ServiceCidr '%s'", k.DNSServiceIP, primaryServiceCIDR)
}
// and that the DNS IP is _not_ the first IP in the service subnet
firstServiceIP := common.CidrFirstIP(serviceCidr.IP)
if firstServiceIP.Equal(dnsIP) {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.DNSServiceIP '%s' cannot be the first IP of ServiceCidr '%s'", k.DNSServiceIP, primaryServiceCIDR)
}
}
if k.ProxyMode != "" && k.ProxyMode != KubeProxyModeIPTables && k.ProxyMode != KubeProxyModeIPVS {
return errors.Errorf("Invalid KubeProxyMode %v. Allowed modes are %v and %v", k.ProxyMode, KubeProxyModeIPTables, KubeProxyModeIPVS)
}
// dualstack IPVS mode supported from 1.16+
// dualstack IPtables mode supported from 1.18+
if ipv6DualStackEnabled && k.ProxyMode == KubeProxyModeIPTables {
minVersion, err := semver.Make("1.18.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.Errorf("KubeProxyMode %v in dualstack not supported with %s version", k.ProxyMode, k8sVersion)
}
}
// Validate that we have a valid etcd version
if e := validateEtcdVersion(k.EtcdVersion); e != nil {
return e
}
// Validate containerd scenarios
if k.ContainerRuntime == Docker || k.ContainerRuntime == "" {
if k.MobyVersion != "" && k.ContainerdVersion != "" && versions.LessThan(k.MobyVersion, "19.03") {
return errors.Errorf("containerdVersion is only valid in a non-docker context, use %s containerRuntime value instead if you wish to provide a containerdVersion", Containerd)
}
}
if e := validateContainerdVersion(k.ContainerdVersion); e != nil {
return e
}
if to.Bool(k.UseCloudControllerManager) || k.CustomCcmImage != "" {
sv, err := semver.Make(k8sVersion)
if err != nil {
return errors.Errorf("could not validate version %s", k8sVersion)
}
minVersion, err := semver.Make("1.8.0")
if err != nil {
return errors.New("could not validate version")
}
if sv.LT(minVersion) {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.UseCloudControllerManager and OrchestratorProfile.KubernetesConfig.CustomCcmImage not available in kubernetes version %s", k8sVersion)
}
}
if e := k.validateNetworkPlugin(hasWindows, isUpdate); e != nil {
return e
}
if e := k.validateNetworkPolicy(k8sVersion, hasWindows); e != nil {
return e
}
if e := k.validateNetworkPluginPlusPolicy(); e != nil {
return e
}
if e := k.validateNetworkMode(); e != nil {
return e
}
if e := k.validateKubernetesImageBaseType(); e != nil {
return e
}
if to.Bool(k.EnableMultipleStandardLoadBalancers) && !common.IsKubernetesVersionGe(k8sVersion, "1.20.0-beta.1") {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.EnableMultipleStandardLoadBalancers is available since kubernetes version v1.20.0-beta.1, current version is %s", k8sVersion)
}
if k.Tags != "" && !common.IsKubernetesVersionGe(k8sVersion, "1.20.0-beta.1") {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.Tags is available since kubernetes version v1.20.0-beta.1, current version is %s", k8sVersion)
}
return k.validateContainerRuntimeConfig()
}
func (k *KubernetesConfig) validateContainerRuntimeConfig() error {
if val, ok := k.ContainerRuntimeConfig[common.ContainerDataDirKey]; ok {
if val == "" {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ContainerRuntimeConfig.DataDir '%s' is invalid: must not be empty", val)
}
if !strings.HasPrefix(val, "/") {
return errors.Errorf("OrchestratorProfile.KubernetesConfig.ContainerRuntimeConfig.DataDir '%s' is invalid: must be absolute path", val)
}
}
// Validate base config here, and only allow predefined mutations to ensure invariant.
if k.ContainerRuntime == Containerd {
_, err := common.GetContainerdConfig(k.ContainerRuntimeConfig, nil)
if err != nil {
return err
}
} else {
_, err := common.GetDockerConfig(k.ContainerRuntimeConfig, nil)
if err != nil {
return err
}
}
return nil
}
func (k *KubernetesConfig) validateNetworkPlugin(hasWindows, isUpdate bool) error {
networkPlugin := k.NetworkPlugin
// Check NetworkPlugin has a valid value.
valid := false
for _, plugin := range NetworkPluginValues {
if networkPlugin == plugin {
if plugin == NetworkPluginFlannel {
if isUpdate {
valid = true
}
} else {
valid = true
}
break
}
}
if !valid {
if networkPlugin == NetworkPluginFlannel {
return errors.Errorf("networkPlugin '%s' has been deprecated and is no longer supported for new cluster creation", networkPlugin)
}
return errors.Errorf("unknown networkPlugin '%s' specified", networkPlugin)
}
// Temporary safety check, to be removed when Windows support is added.
if (networkPlugin == NetworkPluginAntrea) && hasWindows {
return errors.Errorf("networkPlugin '%s' is not supporting windows agents", networkPlugin)
}
if networkPlugin == NetworkPluginKubenet && hasWindows {
log.Warnf("Windows + Kubenet is for development and testing only, not recommended for production")
}
return nil
}
func (k *KubernetesConfig) validateNetworkPolicy(k8sVersion string, hasWindows bool) error {
networkPolicy := k.NetworkPolicy
networkPlugin := k.NetworkPlugin
// Check NetworkPolicy has a valid value.
valid := false
for _, plugin := range NetworkPolicyValues {
if networkPolicy == plugin {
valid = true
break
}
}
if !valid {
return errors.Errorf("unknown networkPolicy '%s' specified", networkPolicy)
}
if networkPolicy == "azure" && networkPlugin == "azure" && !common.IsKubernetesVersionGe(k8sVersion, "1.8.0") {
return errors.New("networkPolicy azure requires kubernetes version of 1.8 or higher")
}
// Temporary safety check, to be removed when Windows support is added.
if (networkPolicy == "calico" || networkPolicy == NetworkPolicyCilium ||
networkPolicy == NetworkPolicyAntrea) && hasWindows {
return errors.Errorf("networkPolicy '%s' is not supporting windows agents", networkPolicy)
}
return nil
}
func (k *KubernetesConfig) validateNetworkPluginPlusPolicy() error {
var config k8sNetworkConfig
config.networkPlugin = k.NetworkPlugin
config.networkPolicy = k.NetworkPolicy
for _, c := range networkPluginPlusPolicyAllowed {
if c.networkPlugin == config.networkPlugin && c.networkPolicy == config.networkPolicy {
return nil
}
}
return errors.Errorf("networkPolicy '%s' is not supported with networkPlugin '%s'", config.networkPolicy, config.networkPlugin)
}
func (k *KubernetesConfig) validateNetworkMode() error {
networkPlugin := k.NetworkPlugin
networkPolicy := k.NetworkPolicy
networkMode := k.NetworkMode
// Check NetworkMode has a valid value.
valid := false
for _, mode := range NetworkModeValues {
if networkMode == mode {
valid = true
break
}
}
if !valid {
return errors.Errorf("unknown networkMode '%s' specified", networkMode)
}
if networkMode != "" {
if networkPlugin != "azure" {
return errors.New("networkMode requires network plugin to be 'azure'")
}
if networkPolicy == "calico" && networkMode != NetworkModeTransparent {
return errors.Errorf("networkMode '%s' is not supported by calico", networkMode)
}
}
return nil
}
func (k *KubernetesConfig) validateKubernetesImageBaseType() error {
for _, valid := range kubernetesImageBaseTypeValidVersions {
if valid == k.KubernetesImageBaseType {
return nil
}
}
return errors.Errorf("Invalid kubernetesImageBaseType value \"%s\", please use one of the following versions: %s", k.KubernetesImageBaseType, kubernetesImageBaseTypeValidVersions)
}
func (k *KubernetesConfig) isUsingCustomKubeComponent() bool {
return k.CustomKubeAPIServerImage != "" || k.CustomKubeControllerManagerImage != "" || k.CustomKubeSchedulerImage != "" || k.CustomKubeBinaryURL != ""
}
func (a *Properties) validateContainerRuntime(isUpdate bool) error {
var containerRuntime string
if a.OrchestratorProfile.KubernetesConfig != nil {
containerRuntime = a.OrchestratorProfile.KubernetesConfig.ContainerRuntime
}
// Check for deprecated, non-back-compat
if isUpdate && containerRuntime == KataContainers {
return errors.Errorf("%s containerRuntime has been deprecated, you will not be able to update this cluster with this version of aks-engine", KataContainers)
}
// Check ContainerRuntime has a valid value.
valid := false
for _, runtime := range ContainerRuntimeValues {
if containerRuntime == runtime {
valid = true
break
}
}
if !valid {
return errors.Errorf("unknown containerRuntime %q specified", containerRuntime)
}
// TODO: These validations should be relaxed once ContainerD and CNI plugins are more readily available
if containerRuntime == Containerd && a.HasWindows() {
if a.OrchestratorProfile.KubernetesConfig.NetworkPlugin == "kubenet" {
if a.OrchestratorProfile.KubernetesConfig.WindowsSdnPluginURL == "" {
return errors.Errorf("WindowsSdnPluginURL must be provided when using Windows with ContainerRuntime=containerd and networkPlugin=kubenet")
}
}
}
return nil
}
func (a *Properties) validateCustomKubeComponent() error {
k := a.OrchestratorProfile.KubernetesConfig
if k == nil {
return nil
}
if common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.17.0") {
if k.CustomHyperkubeImage != "" {
return errors.New("customHyperkubeImage has no effect in Kubernetes version 1.17.0 or above")
}
} else {
if k.isUsingCustomKubeComponent() {
return errors.New("customKubeAPIServerImage, customKubeControllerManagerImage, customKubeSchedulerImage or customKubeBinaryURL have no effect in Kubernetes version 1.16 or earlier")
}
}
if !common.IsKubernetesVersionGe(a.OrchestratorProfile.OrchestratorVersion, "1.16.0") {
if k.CustomKubeProxyImage != "" {
return errors.New("customKubeProxyImage has no effect in Kubernetes version 1.15 or earlier")
}
}
return nil
}
func validateName(name string, label string) error {
if name == "" {
return errors.Errorf("%s must be a non-empty value", label)
}
return nil
}
func validatePoolName(poolName string) error {
// we will cap at length of 12 and all lowercase letters since this makes up the VMName
poolNameRegex := `^([a-z][a-z0-9]{0,11})$`
re, err := regexp.Compile(poolNameRegex)
if err != nil {
return err
}
submatches := re.FindStringSubmatch(poolName)
if len(submatches) != 2 {
return errors.Errorf("pool name '%s' is invalid. A pool name must start with a lowercase letter, have max length of 12, and only have characters a-z0-9", poolName)
}
return nil
}
func validatePoolOSType(os OSType) error {
if os != Linux && os != Windows && os != "" {
return errors.New("AgentPoolProfile.osType must be either Linux or Windows")
}
return nil
}
func validatePoolAcceleratedNetworking(vmSize string) error {
if !helpers.AcceleratedNetworkingSupported(vmSize) {
return errors.Errorf("AgentPoolProfile.vmsize %s does not support AgentPoolProfile.acceleratedNetworking", vmSize)
}
return nil
}
func validateUniquePorts(ports []int, name string) error {
portMap := make(map[int]bool)
for _, port := range ports {
if _, ok := portMap[port]; ok {
return errors.Errorf("agent profile '%s' has duplicate port '%d', ports must be unique", name, port)
}
portMap[port] = true
}
return nil
}
func validateKubernetesLabelValue(v string) error {
if !(len(v) == 0) && !labelValueRegex.MatchString(v) {
return errors.Errorf("Label value '%s' is invalid. Valid label values must be 63 characters or less and must be empty or begin and end with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between", v)
}
return nil
}
func validateKubernetesLabelKey(k string) error {
if !labelKeyRegex.MatchString(k) {
return errors.Errorf("Label key '%s' is invalid. Valid label keys have two segments: an optional prefix and name, separated by a slash (/). The name segment is required and must be 63 characters or less, beginning and ending with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_), dots (.), and alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain: a series of DNS labels separated by dots (.), not longer than 253 characters in total, followed by a slash (/)", k)
}
prefix := strings.Split(k, "/")
if len(prefix) != 1 && len(prefix[0]) > labelKeyPrefixMaxLength {
return errors.Errorf("Label key prefix '%s' is invalid. If specified, the prefix must be no longer than 253 characters in total", k)
}
return nil
}
func validateEtcdVersion(etcdVersion string) error {
// "" is a valid etcdVersion that maps to DefaultEtcdVersion
if etcdVersion == "" {
return nil
}
for _, ver := range etcdValidVersions {
if ver == etcdVersion {
return nil
}
}
return errors.Errorf("Invalid etcd version \"%s\", please use one of the following versions: %s", etcdVersion, etcdValidVersions)
}
func validateContainerdVersion(containerdVersion string) error {
// "" is a valid containerd that maps to DefaultContainerdVersion
if containerdVersion == "" {
return nil
}
for _, ver := range containerdValidVersions {
if ver == containerdVersion {
return nil
}
}
return errors.Errorf("Invalid containerd version \"%s\", please use one of the following versions: %s", containerdVersion, containerdValidVersions)
}
// Check that distro has a valid value
func validateDistro(distro Distro, distroValues []Distro) bool {
var ret bool
for _, d := range distroValues {
if distro == d {
ret = true
}
}
switch distro {
case AKSUbuntu1604, Ubuntu:
log.Warnf("The '%s' distro uses Ubuntu 16.04-LTS, which is End of Life (EOL) and will no longer receive security updates", distro)
}
return ret
}
func (i *ImageReference) validateImageNameAndGroup() error {
if i.Name == "" && i.ResourceGroup != "" {
return errors.New("imageName needs to be specified when imageResourceGroup is provided")
}
if i.Name != "" && i.ResourceGroup == "" {
return errors.New("imageResourceGroup needs to be specified when imageName is provided")
}
return nil
}
func (cs *ContainerService) validateCustomCloudProfile() error {
a := cs.Properties
if a.IsCustomCloudProfile() {
if a.IsAzureStackCloud() {
if a.CustomCloudProfile.PortalURL == "" {
return errors.New("portalURL needs to be specified when AzureStackCloud CustomCloudProfile is provided")
}
if !strings.HasPrefix(a.CustomCloudProfile.PortalURL, fmt.Sprintf("https://portal.%s.", cs.Location)) {
return errors.Errorf("portalURL needs to start with https://portal.%s. ", cs.Location)
}
if a.CustomCloudProfile.AuthenticationMethod != "" && !(a.CustomCloudProfile.AuthenticationMethod == ClientSecretAuthMethod || a.CustomCloudProfile.AuthenticationMethod == ClientCertificateAuthMethod) {
return errors.Errorf("authenticationMethod allowed values are '%s' and '%s'", ClientCertificateAuthMethod, ClientSecretAuthMethod)
}
if a.CustomCloudProfile.IdentitySystem != "" && !(a.CustomCloudProfile.IdentitySystem == AzureADIdentitySystem || a.CustomCloudProfile.IdentitySystem == ADFSIdentitySystem) {
return errors.Errorf("identitySystem allowed values are '%s' and '%s'", AzureADIdentitySystem, ADFSIdentitySystem)
}
}
dependenciesLocationValues := DependenciesLocationValues
if !validateDependenciesLocation(a.CustomCloudProfile.DependenciesLocation, dependenciesLocationValues) {
return errors.Errorf("The %s dependenciesLocation is not supported. The supported vaules are %s", a.CustomCloudProfile.DependenciesLocation, dependenciesLocationValues)
}
}
return nil
}
// Validate implements validation for ContainerService
func (cs *ContainerService) Validate(isUpdate bool) error {
if e := cs.validateProperties(); e != nil {
return e
}
if e := cs.validateLocation(); e != nil {
return e
}
if e := cs.validateCustomCloudProfile(); e != nil {
return e
}
if e := cs.Properties.validate(isUpdate); e != nil {
return e
}
return nil
}
func (cs *ContainerService) validateLocation() error {
if cs.Properties != nil && cs.Properties.IsCustomCloudProfile() && cs.Location == "" {
return errors.New("missing ContainerService Location")
}
if cs.Location == "" {
log.Warnf("No \"location\" value was specified, AKS Engine will generate an ARM template configuration valid for regions in public cloud only")
}
return nil
}
func (cs *ContainerService) validateProperties() error {
if cs.Properties == nil {
return errors.New("missing ContainerService Properties")
}
return nil
}
// Check that dependenciesLocation has a valid value
func validateDependenciesLocation(dependenciesLocation DependenciesLocation, dependenciesLocationValues []DependenciesLocation) bool {
for _, d := range dependenciesLocationValues {
if dependenciesLocation == d {
return true
}
}
return false
}
// validateAzureStackSupport logs a warning if apimodel contains preview features and returns an error if a property is not supported on Azure Stack clouds
func (a *Properties) validateAzureStackSupport() error {
if a.IsAzureStackCloud() {
networkPlugin := a.OrchestratorProfile.KubernetesConfig.NetworkPlugin
if networkPlugin != "azure" && networkPlugin != "kubenet" && networkPlugin != "" {
return errors.Errorf("kubernetesConfig.networkPlugin '%s' is not supported on Azure Stack clouds", networkPlugin)
}
if a.MasterProfile.AvailabilityProfile == VirtualMachineScaleSets {
return errors.Errorf("masterProfile.availabilityProfile should be set to '%s' on Azure Stack clouds", AvailabilitySet)
}
for _, agentPool := range a.AgentPoolProfiles {
pool := agentPool
if pool.AvailabilityProfile != AvailabilitySet {
return errors.Errorf("agentPoolProfiles[%s].availabilityProfile should be set to '%s' on Azure Stack clouds", pool.Name, AvailabilitySet)
}
}
}
return nil
}
func (a *KubernetesAddon) validateArcAddonConfig() error {
if a.Config == nil {
a.Config = make(map[string]string)
}
err := []string{}
if a.Config["location"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'location' property")
}
if a.Config["tenantID"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'tenantID' property")
}
if a.Config["subscriptionID"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'subscriptionID' property")
}
if a.Config["resourceGroup"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'resourceGroup' property")
}
if a.Config["clusterName"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'clusterName' property")
}
if a.Config["clientID"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'clientID' property")
}
if a.Config["clientSecret"] == "" {
err = append(err, "azure-arc-onboarding addon configuration must have a 'clientSecret' property")
}
if len(err) > 0 {
return errors.New(strings.Join(err, "; "))
}
return nil
}