VYPR
Critical severity9.9OSV Advisory· Published Sep 25, 2025· Updated Apr 15, 2026

CVE-2025-59823

CVE-2025-59823

Description

Project Gardener implements the automated management and operation of Kubernetes clusters as a service. Code injection may be possible in Gardener Extensions for AWS providers prior to version 1.64.0, Azure providers prior to version 1.55.0, OpenStack providers prior to version 1.49.0, and GCP providers prior to version 1.46.0. This vulnerability could allow a user with administrative privileges for a Gardener project to obtain control over the seed cluster where the shoot cluster is managed. This affects all Gardener installations where Terraformer is used/can be enabled for infrastructure provisioning with any of the affected components. This issue has been patched in Gardener Extensions for AWS providers version 1.64.0, Azure providers version 1.55.0, OpenStack providers version 1.49.0, and GCP providers version 1.46.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/gardener/gardener-extension-provider-awsGo
< 1.64.01.64.0
github.com/gardener/gardener-extension-provider-gcpGo
< 1.46.01.46.0
github.com/gardener/gardener-extension-provider-azureGo
< 1.55.01.55.0
github.com/gardener/gardener-extension-provider-openstackGo
< 1.49.01.49.0

Affected products

1

Patches

8
b64320a701a3

release v1.49.0

4 files changed · +8 8
  • example/controller-registration.yaml+1 1 modified
    @@ -7,7 +7,7 @@ helm:
       rawChart: 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
       values:
         image:
    -      tag: v1.49.0-dev
    +      tag: v1.49.0
     ---
     apiVersion: core.gardener.cloud/v1beta1
     kind: ControllerRegistration
    
  • example/extension/base/extension.yaml+3 3 modified
    @@ -8,15 +8,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-runtime:v1.49.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-runtime:v1.49.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-application:v1.49.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-application:v1.49.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-openstack:v1.49.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-openstack:v1.49.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • example/extension.yaml+3 3 modified
    @@ -10,15 +10,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-runtime:v1.49.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-runtime:v1.49.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-application:v1.49.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-openstack-application:v1.49.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-openstack:v1.49.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-openstack:v1.49.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • VERSION+1 1 modified
    @@ -1 +1 @@
    -v1.49.0-dev
    +v1.49.0
    
27fcec2a9fd2

release v1.46.0

4 files changed · +8 8
  • example/controller-registration.yaml+1 1 modified
    @@ -7,7 +7,7 @@ helm:
       rawChart: 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
       values:
         image:
    -      tag: v1.46.0-dev
    +      tag: v1.46.0
     ---
     apiVersion: core.gardener.cloud/v1beta1
     kind: ControllerRegistration
    
  • example/extension/base/extension.yaml+3 3 modified
    @@ -8,15 +8,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-runtime:v1.46.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-runtime:v1.46.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-application:v1.46.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-application:v1.46.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-gcp:v1.46.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-gcp:v1.46.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • example/extension.yaml+3 3 modified
    @@ -10,15 +10,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-runtime:v1.46.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-runtime:v1.46.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-application:v1.46.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-gcp-application:v1.46.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-gcp:v1.46.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-gcp:v1.46.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • VERSION+1 1 modified
    @@ -1 +1 @@
    -v1.46.0-dev
    +v1.46.0
    
30e3c13c8967

release v1.55.0

4 files changed · +8 8
  • example/controller-registration.yaml+1 1 modified
    @@ -7,7 +7,7 @@ helm:
       rawChart: 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
       values:
         image:
    -      tag: v1.55.0-dev
    +      tag: v1.55.0
     ---
     apiVersion: core.gardener.cloud/v1beta1
     kind: ControllerRegistration
    
  • example/extension/base/extension.yaml+3 3 modified
    @@ -8,15 +8,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-runtime:v1.55.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-runtime:v1.55.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-application:v1.55.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-application:v1.55.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-azure:v1.55.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-azure:v1.55.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • example/extension.yaml+3 3 modified
    @@ -10,15 +10,15 @@ spec:
           runtimeCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-runtime:v1.55.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-runtime:v1.55.0
           virtualCluster:
             helm:
               ociRepository:
    -            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-application:v1.55.0-dev
    +            ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/admission-azure-application:v1.55.0
         extension:
           helm:
             ociRepository:
    -          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-azure:v1.55.0-dev
    +          ref: europe-docker.pkg.dev/gardener-project/public/charts/gardener/extensions/provider-azure:v1.55.0
           injectGardenKubeconfig: true
       resources:
       - kind: BackupBucket
    
  • VERSION+1 1 modified
    @@ -1 +1 @@
    -v1.55.0-dev
    +v1.55.0
    
06113421518c

Release v1.46.0

2 files changed · +2 2
  • example/controller-registration.yaml+1 1 modified
    @@ -8,7 +8,7 @@ providerConfig:
       chart: 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
       values:
         image:
    -      tag: v1.46.0-dev
    +      tag: v1.46.0
     ---
     apiVersion: core.gardener.cloud/v1beta1
     kind: ControllerRegistration
    
  • VERSION+1 1 modified
    @@ -1 +1 @@
    -v1.46.0-dev
    +v1.46.0
    
cb5045fc1462

Shoot input validation (#1479)

https://github.com/gardener/gardener-extension-provider-awsKonstantinos AngelopoulosSep 18, 2025via ghsa
7 files changed · +285 67
  • pkg/apis/aws/validation/controlplane.go+9 3 modified
    @@ -12,11 +12,17 @@ import (
     )
     
     // ValidateControlPlaneConfig validates a ControlPlaneConfig object.
    -func ValidateControlPlaneConfig(controlPlaneConfig *apisaws.ControlPlaneConfig, version string, fldPath *field.Path) field.ErrorList {
    +func ValidateControlPlaneConfig(cpConfig *apisaws.ControlPlaneConfig, version string, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	if controlPlaneConfig.CloudControllerManager != nil {
    -		allErrs = append(allErrs, featurevalidation.ValidateFeatureGates(controlPlaneConfig.CloudControllerManager.FeatureGates, version, fldPath.Child("cloudControllerManager", "featureGates"))...)
    +	if cpConfig.CloudControllerManager != nil {
    +		allErrs = append(allErrs, featurevalidation.ValidateFeatureGates(cpConfig.CloudControllerManager.FeatureGates, version, fldPath.Child("cloudControllerManager", "featureGates"))...)
    +	}
    +
    +	if cpConfig.LoadBalancerController != nil && cpConfig.LoadBalancerController.IngressClassName != nil {
    +		ingressClassName := *cpConfig.LoadBalancerController.IngressClassName
    +		ingressPath := fldPath.Child("loadBalancerController", "ingressClassName")
    +		allErrs = append(allErrs, validateK8sResourceName(ingressClassName, ingressPath)...)
     	}
     
     	return allErrs
    
  • pkg/apis/aws/validation/controlplane_test.go+22 0 modified
    @@ -9,6 +9,7 @@ import (
     	. "github.com/onsi/gomega"
     	. "github.com/onsi/gomega/gstruct"
     	"k8s.io/apimachinery/pkg/util/validation/field"
    +	"k8s.io/utils/ptr"
     
     	apisaws "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws"
     	. "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws/validation"
    @@ -46,5 +47,26 @@ var _ = Describe("ControlPlaneConfig validation", func() {
     				})),
     			))
     		})
    +
    +		Context("LoadBalancerController", func() {
    +			It("should pass for valid ingress class name", func() {
    +				controlPlane.LoadBalancerController = &apisaws.LoadBalancerControllerConfig{
    +					IngressClassName: ptr.To("valid-ingress-class"),
    +				}
    +				Expect(ValidateControlPlaneConfig(controlPlane, "", fldPath)).To(BeEmpty())
    +			})
    +
    +			It("should fail for invalid ingress class name", func() {
    +				controlPlane.LoadBalancerController = &apisaws.LoadBalancerControllerConfig{
    +					IngressClassName: ptr.To("NoUpperCaseAllowed"),
    +				}
    +				errorList := ValidateControlPlaneConfig(controlPlane, "", fldPath)
    +				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("loadBalancerController.ingressClassName"),
    +					"Detail": Equal("does not match expected regex ^[a-z0-9.-]+$"),
    +				}))))
    +			})
    +		})
     	})
     })
    
  • pkg/apis/aws/validation/filter.go+90 0 added
    @@ -0,0 +1,90 @@
    +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
    +//
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package validation
    +
    +import (
    +	"fmt"
    +	"regexp"
    +	"unicode/utf8"
    +
    +	"k8s.io/apimachinery/pkg/util/validation/field"
    +)
    +
    +var (
    +	// k8s resource names have max 253 characters, consist of lower case alphanumeric characters, '-' or '.'
    +	k8sResourceNameRegex = `^[a-z0-9.-]+$`
    +	// VpcIDRegex matches e.g. vpc-064b5b7771f6331aa
    +	VpcIDRegex = `^vpc-[a-z0-9]+$`
    +	// EipAllocationIDRegex matches e.g. eipalloc-0676786f3e288044c
    +	EipAllocationIDRegex = `^eipalloc-[a-z0-9]+$`
    +	// SnapshotIDRegex matches e.g. snap-0676786f3e288044c
    +	SnapshotIDRegex = `^snap-[a-z0-9]+$`
    +	// IamInstanceProfileNameRegex matches https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-iam-instanceprofile.html#:~:text=Properties-,InstanceProfileName,-The%20name%20of
    +	IamInstanceProfileNameRegex = `^[\w+=,.@-]+$`
    +	// IamInstanceProfileArnRegex matches arn:aws:iam::<account-id>:instance-profile/<path>/<profile-name>
    +	// Note: for china landscapes it's arn:aws-cn:iam::<account-id>:instance-profile/<path>/<profile-name>
    +	IamInstanceProfileArnRegex = `^arn:[\w +=,.@\-/:]+$`
    +	// ZoneNameRegex matches e.g. us-east-1a
    +	ZoneNameRegex = `^[a-z0-9-]+$`
    +	// TagKeyRegex matches Letters (a–z, A–Z), numbers (0–9), spaces, and the following symbols: + - = . _ : / @
    +	TagKeyRegex = `^[\w +\-=\.:/@]+$`
    +	// GatewayEndpointRegex matches one or more word characters, optionally followed by dot-separated word segments
    +	GatewayEndpointRegex = `^\w+(\.\w+)*$`
    +
    +	validateK8sResourceName        = combineValidationFuncs(regex(k8sResourceNameRegex), notEmpty, maxLength(253))
    +	validateVpcID                  = combineValidationFuncs(regex(VpcIDRegex), notEmpty, maxLength(255))
    +	validateEipAllocationID        = combineValidationFuncs(regex(EipAllocationIDRegex), maxLength(255))
    +	validateSnapshotID             = combineValidationFuncs(regex(SnapshotIDRegex), maxLength(255))
    +	validateIamInstanceProfileName = combineValidationFuncs(regex(IamInstanceProfileNameRegex), notEmpty, maxLength(128))
    +	validateIamInstanceProfileArn  = combineValidationFuncs(regex(IamInstanceProfileArnRegex), maxLength(255))
    +	validateZoneName               = combineValidationFuncs(regex(ZoneNameRegex), maxLength(255))
    +	validateTagKey                 = combineValidationFuncs(regex(TagKeyRegex), notEmpty, maxLength(128))
    +	validateGatewayEndpointName    = combineValidationFuncs(regex(GatewayEndpointRegex), maxLength(255))
    +)
    +
    +type validateFunc[T any] func(T, *field.Path) field.ErrorList
    +
    +// combineValidationFuncs validates a value against a list of filters.
    +func combineValidationFuncs[T any](filters ...validateFunc[T]) validateFunc[T] {
    +	return func(t T, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		for _, f := range filters {
    +			allErrs = append(allErrs, f(t, fld)...)
    +		}
    +		return allErrs
    +	}
    +}
    +
    +// regex returns a filterFunc that validates a string against a regular expression.
    +func regex(regex string) validateFunc[string] {
    +	compiled := regexp.MustCompile(regex)
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if name == "" {
    +			return allErrs // Allow empty strings to pass through
    +		}
    +		if !compiled.MatchString(name) {
    +			allErrs = append(allErrs, field.Invalid(fld, name, fmt.Sprintf("does not match expected regex %s", compiled.String())))
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func notEmpty(name string, fld *field.Path) field.ErrorList {
    +	if utf8.RuneCountInString(name) == 0 {
    +		return field.ErrorList{field.Required(fld, "cannot be empty")}
    +	}
    +	return nil
    +}
    +
    +func maxLength(max int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if l := utf8.RuneCountInString(name); l > max {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be more than %d characters, got %d", max, l))}
    +		}
    +		return allErrs
    +	}
    +}
    
  • pkg/apis/aws/validation/infrastructure.go+17 16 modified
    @@ -6,7 +6,6 @@ package validation
     
     import (
     	"fmt"
    -	"regexp"
     	"slices"
     	"strings"
     
    @@ -20,9 +19,6 @@ import (
     	apisaws "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws"
     )
     
    -// valid values for networks.vpc.gatewayEndpoints
    -var gatewayEndpointPattern = regexp.MustCompile(`^[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$`)
    -
     // ValidateInfrastructureConfigAgainstCloudProfile validates the given `InfrastructureConfig` against the given `CloudProfile`.
     func ValidateInfrastructureConfigAgainstCloudProfile(oldInfra, infra *apisaws.InfrastructureConfig, shoot *core.Shoot, cloudProfileSpec *gardencorev1beta1.CloudProfileSpec, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    @@ -95,9 +91,7 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
     	if len(infra.Networks.VPC.GatewayEndpoints) > 0 {
     		epsPath := networksPath.Child("vpc", "gatewayEndpoints")
     		for i, svc := range infra.Networks.VPC.GatewayEndpoints {
    -			if !gatewayEndpointPattern.MatchString(svc) {
    -				allErrs = append(allErrs, field.Invalid(epsPath.Index(i), svc, "must be a valid domain name"))
    -			}
    +			allErrs = append(allErrs, validateGatewayEndpointName(svc, epsPath.Index(i))...)
     		}
     	}
     
    @@ -110,6 +104,8 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
     	for i, zone := range infra.Networks.Zones {
     		zonePath := networksPath.Child("zones").Index(i)
     
    +		allErrs = append(allErrs, validateZoneName(zone.Name, zonePath.Child("name"))...)
    +
     		publicPath := zonePath.Child("public")
     		cidrs = append(cidrs, cidrvalidation.NewCIDR(zone.Public, publicPath))
     		allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(publicPath, zone.Public)...)
    @@ -134,9 +130,7 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
     			}
     			referencedElasticIPAllocationIDs = append(referencedElasticIPAllocationIDs, *zone.ElasticIPAllocationID)
     
    -			if !strings.HasPrefix(*zone.ElasticIPAllocationID, "eipalloc-") {
    -				allErrs = append(allErrs, field.Invalid(zonePath.Child("elasticIPAllocationID"), *zone.ElasticIPAllocationID, "must start with eipalloc-"))
    -			}
    +			allErrs = append(allErrs, validateEipAllocationID(*zone.ElasticIPAllocationID, zonePath.Child("elasticIPAllocationID"))...)
     		}
     	}
     
    @@ -146,16 +140,23 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
     		allErrs = append(allErrs, nodes.ValidateSubset(workerCIDRs...)...)
     	}
     
    -	if (infra.Networks.VPC.ID == nil && infra.Networks.VPC.CIDR == nil) || (infra.Networks.VPC.ID != nil && infra.Networks.VPC.CIDR != nil) {
    +	idProvided := infra.Networks.VPC.ID != nil
    +	cidrProvided := infra.Networks.VPC.CIDR != nil
    +	switch {
    +	case !idProvided && !cidrProvided:
     		allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc"), infra.Networks.VPC, "must specify either a vpc id or a cidr"))
    -	} else if infra.Networks.VPC.CIDR != nil && infra.Networks.VPC.ID == nil && !slices.Contains(ipFamilies, core.IPFamilyIPv6) {
    +	case idProvided && cidrProvided:
    +		allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc"), infra.Networks.VPC, "cannot specify both vpc id and cidr"))
    +	case cidrProvided && !idProvided && !slices.Contains(ipFamilies, core.IPFamilyIPv6):
     		cidrPath := networksPath.Child("vpc", "cidr")
     		vpcCIDR := cidrvalidation.NewCIDR(*infra.Networks.VPC.CIDR, cidrPath)
     		allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(cidrPath, *infra.Networks.VPC.CIDR)...)
     		allErrs = append(allErrs, vpcCIDR.ValidateParse()...)
     		allErrs = append(allErrs, vpcCIDR.ValidateSubset(nodes)...)
     		allErrs = append(allErrs, vpcCIDR.ValidateSubset(cidrs...)...)
     		allErrs = append(allErrs, vpcCIDR.ValidateNotOverlap(pods, services)...)
    +	case idProvided && !cidrProvided:
    +		allErrs = append(allErrs, validateVpcID(*infra.Networks.VPC.ID, networksPath.Child("vpc", "id"))...)
     	}
     
     	// make sure that VPC cidrs don't overlap with each other
    @@ -259,8 +260,8 @@ func ValidateIgnoreTags(fldPath *field.Path, ignoreTags *apisaws.IgnoreTags) fie
     	keysPath := fldPath.Child("keys")
     	for i, key := range ignoreTags.Keys {
     		idxPath := keysPath.Index(i)
    -		if key == "" {
    -			allErrs = append(allErrs, field.Invalid(idxPath, key, "ignored key must not be empty"))
    +		if errs := validateTagKey(key, idxPath); errs != nil {
    +			allErrs = append(allErrs, errs...)
     			continue
     		}
     		allErrs = append(allErrs, validateKeyIsReserved(idxPath, key)...)
    @@ -270,8 +271,8 @@ func ValidateIgnoreTags(fldPath *field.Path, ignoreTags *apisaws.IgnoreTags) fie
     	prefixesPath := fldPath.Child("keyPrefixes")
     	for i, prefix := range ignoreTags.KeyPrefixes {
     		idxPath := prefixesPath.Index(i)
    -		if prefix == "" {
    -			allErrs = append(allErrs, field.Invalid(idxPath, prefix, "ignored key prefix must not be empty"))
    +		if errs := validateTagKey(prefix, idxPath); errs != nil {
    +			allErrs = append(allErrs, errs...)
     			continue
     		}
     		allErrs = append(allErrs, validatePrefixIncludesReservedKey(idxPath, prefix)...)
    
  • pkg/apis/aws/validation/infrastructure_test.go+104 32 modified
    @@ -5,6 +5,8 @@
     package validation_test
     
     import (
    +	"fmt"
    +
     	"github.com/gardener/gardener/pkg/apis/core"
     	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
     	. "github.com/gardener/gardener/pkg/utils/test/matchers"
    @@ -27,10 +29,10 @@ var _ = Describe("InfrastructureConfig validation", func() {
     		pods        = "100.96.0.0/11"
     		services    = "100.64.0.0/13"
     		nodes       = "10.250.0.0/16"
    -		vpc         = "10.0.0.0/8"
    +		vpcCIDR     = "10.0.0.0/8"
     		invalidCIDR = "invalid-cidr"
    -		zone        = "zone1"
    -		zone2       = "zone2"
    +		zone        = "eu-central-1c"
    +		zone2       = "us-east-1a"
     
     		awsZone2 = apisaws.Zone{
     			Name:     zone2,
    @@ -49,7 +51,7 @@ var _ = Describe("InfrastructureConfig validation", func() {
     		infrastructureConfig = &apisaws.InfrastructureConfig{
     			Networks: apisaws.Networks{
     				VPC: apisaws.VPC{
    -					CIDR: &vpc,
    +					CIDR: &vpcCIDR,
     				},
     				Zones: []apisaws.Zone{
     					{
    @@ -169,7 +171,87 @@ var _ = Describe("InfrastructureConfig validation", func() {
     	})
     
     	Describe("#ValidateInfrastructureConfig", func() {
    +		Context("VPC", func() {
    +			Context("ID", func() {
    +				It("should pass with ID", func() {
    +					infrastructureConfig.Networks.VPC.ID = ptr.To("vpc-064b5b7771f63317c")
    +					infrastructureConfig.Networks.VPC.CIDR = nil
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(BeEmpty())
    +				})
    +
    +				It("should reject empty ID string", func() {
    +					infrastructureConfig.Networks.VPC.ID = ptr.To("")
    +					infrastructureConfig.Networks.VPC.CIDR = nil
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(ConsistOfFields(Fields{
    +						"Type":   Equal(field.ErrorTypeRequired),
    +						"Field":  Equal("networks.vpc.id"),
    +						"Detail": Equal("cannot be empty"),
    +					}))
    +				})
    +
    +				It("should reject setting both ID and CIDR", func() {
    +					infrastructureConfig.Networks.VPC.ID = ptr.To("vpc-123456")
    +					infrastructureConfig.Networks.VPC.CIDR = &vpcCIDR
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(ConsistOfFields(Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.vpc"),
    +						"Detail": Equal("cannot specify both vpc id and cidr"),
    +					}))
    +				})
    +
    +				It("should reject invalid format", func() {
    +					infrastructureConfig.Networks.VPC.ID = ptr.To("no-vpc-prefix-1234")
    +					infrastructureConfig.Networks.VPC.CIDR = nil
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(ConsistOfFields(Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.vpc.id"),
    +						"Detail": Equal(fmt.Sprintf("does not match expected regex %s", VpcIDRegex)),
    +					}))
    +				})
    +			})
    +
    +			Context("gatewayEndpoints", func() {
    +				It("should accept empty list", func() {
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(BeEmpty())
    +				})
    +
    +				It("should reject non-alphanumeric endpoints", func() {
    +					infrastructureConfig.Networks.VPC.GatewayEndpoints = []string{"s3", "my-endpoint"}
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(ConsistOfFields(Fields{
    +						"Type":     Equal(field.ErrorTypeInvalid),
    +						"Field":    Equal("networks.vpc.gatewayEndpoints[1]"),
    +						"BadValue": Equal("my-endpoint"),
    +						"Detail":   Equal(fmt.Sprintf("does not match expected regex %s", GatewayEndpointRegex)),
    +					}))
    +				})
    +
    +				It("should accept all-valid lists", func() {
    +					infrastructureConfig.Networks.VPC.GatewayEndpoints = []string{"myservice", "s3", "my.other.service"}
    +					errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +					Expect(errorList).To(BeEmpty())
    +				})
    +			})
    +		})
    +
     		Context("Zones", func() {
    +			It("should reject invalid zone name", func() {
    +				infrastructureConfig.Networks.Zones[0].Name = "US-East-1a"
    +
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    +
    +				Expect(errorList).To(ConsistOfFields(Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.zones[0].name"),
    +					"Detail": Equal(fmt.Sprintf("does not match expected regex %s", ZoneNameRegex)),
    +				}))
    +			})
    +
     			It("should forbid empty zones", func() {
     				infrastructureConfig.Networks.Zones = nil
     
    @@ -369,8 +451,9 @@ var _ = Describe("InfrastructureConfig validation", func() {
     				infrastructureConfig.Networks.Zones[0].ElasticIPAllocationID = ptr.To("foo")
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
     				Expect(errorList).To(ConsistOfFields(Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("networks.zones[0].elasticIPAllocationID"),
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.zones[0].elasticIPAllocationID"),
    +					"Detail": Equal(fmt.Sprintf("does not match expected regex %s", EipAllocationIDRegex)),
     				}))
     
     				infrastructureConfig.Networks.Zones[0].ElasticIPAllocationID = ptr.To("eipalloc-123456")
    @@ -395,30 +478,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			})
     		})
     
    -		Context("gatewayEndpoints", func() {
    -			It("should accept empty list", func() {
    -				errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    -				Expect(errorList).To(BeEmpty())
    -			})
    -
    -			It("should reject non-domain name endpoints", func() {
    -				infrastructureConfig.Networks.VPC.GatewayEndpoints = []string{"s3", "my_endpoint", "guardduty-data"}
    -				errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    -				Expect(errorList).To(ConsistOfFields(Fields{
    -					"Type":     Equal(field.ErrorTypeInvalid),
    -					"Field":    Equal("networks.vpc.gatewayEndpoints[1]"),
    -					"BadValue": Equal("my_endpoint"),
    -					"Detail":   Equal("must be a valid domain name"),
    -				}))
    -			})
    -
    -			It("should accept all-valid lists", func() {
    -				infrastructureConfig.Networks.VPC.GatewayEndpoints = []string{"myservice", "s3", "my.other.service"}
    -				errorList := ValidateInfrastructureConfig(infrastructureConfig, familyIPv4, &nodes, &pods, &services)
    -				Expect(errorList).To(BeEmpty())
    -			})
    -		})
    -
     		Context("ignoreTags", func() {
     			It("should forbid ignoring reserved tags", func() {
     				infrastructureConfig.IgnoreTags = &apisaws.IgnoreTags{
    @@ -723,16 +782,29 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			})
     			Expect(errorList).To(ConsistOf(
     				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Type":  Equal(field.ErrorTypeRequired),
     					"Field": Equal("ignoreTags.keys[1]"),
     				})),
     				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Type":  Equal(field.ErrorTypeRequired),
     					"Field": Equal("ignoreTags.keyPrefixes[1]"),
     				})),
     			))
     		})
     
    +		It("should forbid invalid values", func() {
    +			errorList := ValidateIgnoreTags(fldPath, &apisaws.IgnoreTags{
    +				Keys: []string{"notAllowedChar{}"},
    +			})
    +			Expect(errorList).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("ignoreTags.keys[0]"),
    +					"Detail": Equal(fmt.Sprintf("does not match expected regex %s", TagKeyRegex)),
    +				})),
    +			))
    +		})
    +
     		It("should forbid ignoring Name tag", func() {
     			errorList := ValidateIgnoreTags(fldPath, &apisaws.IgnoreTags{
     				Keys:        []string{"Name"},
    
  • pkg/apis/aws/validation/worker.go+10 5 modified
    @@ -59,17 +59,22 @@ func ValidateWorkerConfig(workerConfig *apisaws.WorkerConfig, volume *core.Volum
     		} else {
     			dataVolumeConfigNames.Insert(dv.Name)
     		}
    +
    +		if id := dv.SnapshotID; id != nil {
    +			allErrs = append(allErrs, validateSnapshotID(*id, idxPath.Child("snapshotID"))...)
    +		}
     	}
     
     	if iam := workerConfig.IAMInstanceProfile; iam != nil {
     		if (iam.Name == nil && iam.ARN == nil) || (iam.Name != nil && iam.ARN != nil) {
    -			allErrs = append(allErrs, field.Invalid(fldPath.Child("iamInstanceProfile"), iam, "either <name> or <arn> must be provided"))
    +			allErrs = append(allErrs, field.Invalid(fldPath.Child("iamInstanceProfile"), iam,
    +				"exactly one of 'name' or 'arn' must be specified"))
     		}
    -		if iam.Name != nil && len(*iam.Name) == 0 {
    -			allErrs = append(allErrs, field.Required(fldPath.Child("iamInstanceProfile", "name"), "name must not be empty"))
    +		if iam.Name != nil {
    +			allErrs = append(allErrs, validateIamInstanceProfileName(*iam.Name, fldPath.Child("iamInstanceProfile", "name"))...)
     		}
    -		if iam.ARN != nil && len(*iam.ARN) == 0 {
    -			allErrs = append(allErrs, field.Required(fldPath.Child("iamInstanceProfile", "arn"), "arn must not be empty"))
    +		if iam.ARN != nil {
    +			allErrs = append(allErrs, validateIamInstanceProfileArn(*iam.ARN, fldPath.Child("iamInstanceProfile", "arn"))...)
     		}
     	}
     
    
  • pkg/apis/aws/validation/worker_test.go+33 11 modified
    @@ -5,6 +5,8 @@
     package validation_test
     
     import (
    +	"fmt"
    +
     	"github.com/gardener/gardener/pkg/apis/core"
     	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
     	. "github.com/onsi/ginkgo/v2"
    @@ -41,7 +43,7 @@ var _ = Describe("ValidateWorkerConfig", func() {
     			nodeTemplate    *extensionsv1alpha1.NodeTemplate
     
     			iamInstanceProfileName = "name"
    -			iamInstanceProfileARN  = "arn"
    +			iamInstanceProfileARN  = "arn:aws:iam::123456789012:instance-profile/path/to/profile-name"
     
     			worker  *apisaws.WorkerConfig
     			fldPath = field.NewPath("config")
    @@ -220,6 +222,7 @@ var _ = Describe("ValidateWorkerConfig", func() {
     				})),
     			))
     		})
    +
     		It("should enforce that the IOPS is positive", func() {
     			var negative int64 = -100
     			worker.Volume.IOPS = &negative
    @@ -238,6 +241,7 @@ var _ = Describe("ValidateWorkerConfig", func() {
     				})),
     			))
     		})
    +
     		It("should prevent duplicate entries for data volumes in workerconfig", func() {
     			worker.DataVolumes = append(worker.DataVolumes, apisaws.DataVolume{Name: dataVolume1Name})
     
    @@ -248,6 +252,7 @@ var _ = Describe("ValidateWorkerConfig", func() {
     				"Field": Equal("config.dataVolumes[1].name"),
     			}))))
     		})
    +
     		It("should enforce that the throughput is positive", func() {
     			var negative int64 = -100
     			worker.Volume.Throughput = &negative
    @@ -268,6 +273,7 @@ var _ = Describe("ValidateWorkerConfig", func() {
     				})),
     			))
     		})
    +
     		It("should prevent data volume entries in workerconfig for non-existing data volumes shoot", func() {
     			worker.DataVolumes = append(worker.DataVolumes, apisaws.DataVolume{Name: "broken"})
     
    @@ -279,15 +285,28 @@ var _ = Describe("ValidateWorkerConfig", func() {
     			}))))
     		})
     
    +		It("should reject invalid snapshot ID", func() {
    +			worker.DataVolumes[0].SnapshotID = ptr.To("must-start-with-snap")
    +
    +			errorList := ValidateWorkerConfig(worker, rootVolumeIO1, dataVolumes, fldPath)
    +
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("config.dataVolumes[0].snapshotID"),
    +				"Detail": Equal(fmt.Sprintf("does not match expected regex %s", SnapshotIDRegex)),
    +			}))))
    +		})
    +
     		Context("iamInstanceProfile", func() {
     			It("should prevent not specifying both IAM name and arn", func() {
     				worker.IAMInstanceProfile = &apisaws.IAMInstanceProfile{}
     
     				errorList := ValidateWorkerConfig(worker, rootVolumeIO1, dataVolumes, fldPath)
     
     				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("config.iamInstanceProfile"),
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.iamInstanceProfile"),
    +					"Detail": Equal("exactly one of 'name' or 'arn' must be specified"),
     				}))))
     			})
     
    @@ -300,34 +319,37 @@ var _ = Describe("ValidateWorkerConfig", func() {
     				errorList := ValidateWorkerConfig(worker, rootVolumeIO1, dataVolumes, fldPath)
     
     				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("config.iamInstanceProfile"),
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.iamInstanceProfile"),
    +					"Detail": Equal("exactly one of 'name' or 'arn' must be specified"),
     				}))))
     			})
     
     			It("should forbid specifying an invalid IAM name", func() {
     				worker.IAMInstanceProfile = &apisaws.IAMInstanceProfile{
    -					Name: ptr.To(""),
    +					Name: ptr.To("invalidChar{"),
     				}
     
     				errorList := ValidateWorkerConfig(worker, rootVolumeIO1, dataVolumes, fldPath)
     
     				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeRequired),
    -					"Field": Equal("config.iamInstanceProfile.name"),
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.iamInstanceProfile.name"),
    +					"Detail": Equal(fmt.Sprintf("does not match expected regex %s", IamInstanceProfileNameRegex)),
     				}))))
     			})
     
     			It("should forbid specifying an invalid IAM arn", func() {
     				worker.IAMInstanceProfile = &apisaws.IAMInstanceProfile{
    -					ARN: ptr.To(""),
    +					ARN: ptr.To("must-start-with-arn:aws:iam::"),
     				}
     
     				errorList := ValidateWorkerConfig(worker, rootVolumeIO1, dataVolumes, fldPath)
     
     				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeRequired),
    -					"Field": Equal("config.iamInstanceProfile.arn"),
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.iamInstanceProfile.arn"),
    +					"Detail": Equal(fmt.Sprintf("does not match expected regex %s", IamInstanceProfileArnRegex)),
     				}))))
     			})
     
    
2ed6f0fe1be9

add shoot input validation (#1155)

https://github.com/gardener/gardener-extension-provider-openstackKonstantinos AngelopoulosSep 18, 2025via ghsa
9 files changed · +193 24
  • pkg/apis/openstack/helper/helper.go+2 2 modified
    @@ -140,7 +140,7 @@ func FindFloatingPool(floatingPools []api.FloatingPool, floatingPoolNamePattern,
     	for _, f := range floatingPools {
     		var fip = f
     
    -		// Check non constraining floating pools with second priority
    +		// Check non-constraining floating pools with second priority
     		// which means only when no other floating pool is matching.
     		if fip.NonConstraining != nil && *fip.NonConstraining {
     			nonConstrainingFloatingPools = append(nonConstrainingFloatingPools, fip)
    @@ -158,7 +158,7 @@ func FindFloatingPool(floatingPools []api.FloatingPool, floatingPoolNamePattern,
     	}
     
     	// So far no floating pool was matching to the `floatingPoolNamePattern`
    -	// therefore try now if there is a non contraining floating pool matching.
    +	// therefore try now if there is a non-constraining floating pool matching.
     	for _, f := range nonConstrainingFloatingPools {
     		var fip = f
     		if candidate, score := checkFloatingPoolCandidate(&fip, floatingPoolNamePattern, region, domain); candidate != nil && score > maxCandidateScore {
    
  • pkg/apis/openstack/validation/cloudprofile.go+14 0 modified
    @@ -269,6 +269,20 @@ func validateLoadBalancerClass(lbClass api.LoadBalancerClass, fldPath *field.Pat
     	if lbClass.Purpose != nil && *lbClass.Purpose != api.DefaultLoadBalancerClass && *lbClass.Purpose != api.PrivateLoadBalancerClass && *lbClass.Purpose != api.VPNLoadBalancerClass {
     		allErrs = append(allErrs, field.Invalid(fldPath, *lbClass.Purpose, fmt.Sprintf("invalid LoadBalancerClass purpose. Valid values are %q or %q", api.DefaultLoadBalancerClass, api.PrivateLoadBalancerClass)))
     	}
    +	if lbClass.FloatingNetworkID != nil {
    +		allErrs = append(allErrs, uuid(*lbClass.FloatingNetworkID, fldPath.Child("floatingNetworkID"))...)
    +	}
    +	if lbClass.FloatingSubnetID != nil {
    +		allErrs = append(allErrs, uuid(*lbClass.FloatingSubnetID, fldPath.Child("floatingSubnetID"))...)
    +	}
    +
    +	allErrs = append(allErrs, validateResourceName(lbClass.Name, fldPath.Child("name"))...)
    +	if lbClass.FloatingSubnetName != nil {
    +		allErrs = append(allErrs, validateResourceName(*lbClass.FloatingSubnetName, fldPath.Child("floatingSubnetName"))...)
    +	}
    +	if lbClass.FloatingSubnetTags != nil {
    +		allErrs = append(allErrs, validateResourceName(*lbClass.FloatingSubnetTags, fldPath.Child("floatingSubnetTags"))...)
    +	}
     
     	if lbClass.FloatingSubnetID != nil && lbClass.FloatingSubnetName != nil && lbClass.FloatingSubnetTags != nil {
     		return append(allErrs, field.Forbidden(fldPath, "cannot select floating subnet by id, name and tags in parallel"))
    
  • pkg/apis/openstack/validation/cloudprofile_test.go+5 3 modified
    @@ -546,11 +546,13 @@ var _ = Describe("LoadBalancerClass validation", func() {
     	var (
     		loadBalancerClasses []api.LoadBalancerClass
     		fieldPath           *field.Path
    +		testUUID            string
     	)
     
     	BeforeEach(func() {
     		fieldPath = field.NewPath("loadBalancerClasses")
     
    +		testUUID = "123e4567-e89b-12d3-a456-426614174000"
     		loadBalancerClasses = []api.LoadBalancerClass{{
     			Name: "test1",
     		}}
    @@ -573,7 +575,7 @@ var _ = Describe("LoadBalancerClass validation", func() {
     		})
     
     		It("should fail as LoadBalancerClass specifies floating subnet by id, name and tags in parallel", func() {
    -			loadBalancerClasses[0].FloatingSubnetID = ptr.To("floating-subnet-id")
    +			loadBalancerClasses[0].FloatingSubnetID = ptr.To(testUUID)
     			loadBalancerClasses[0].FloatingSubnetName = ptr.To("floating-subnet-name")
     			loadBalancerClasses[0].FloatingSubnetTags = ptr.To("floating-subnet-tags")
     
    @@ -585,7 +587,7 @@ var _ = Describe("LoadBalancerClass validation", func() {
     		})
     
     		It("should fail as LoadBalancerClass specifies floating subnet by id and name", func() {
    -			loadBalancerClasses[0].FloatingSubnetID = ptr.To("floating-subnet-id")
    +			loadBalancerClasses[0].FloatingSubnetID = ptr.To(testUUID)
     			loadBalancerClasses[0].FloatingSubnetName = ptr.To("floating-subnet-name")
     
     			errorList := ValidateLoadBalancerClasses(loadBalancerClasses, fieldPath)
    @@ -596,7 +598,7 @@ var _ = Describe("LoadBalancerClass validation", func() {
     		})
     
     		It("should fail as LoadBalancerClass specifies floating subnet by id and tags", func() {
    -			loadBalancerClasses[0].FloatingSubnetID = ptr.To("floating-subnet-id")
    +			loadBalancerClasses[0].FloatingSubnetID = ptr.To(testUUID)
     			loadBalancerClasses[0].FloatingSubnetTags = ptr.To("floating-subnet-tags")
     
     			errorList := ValidateLoadBalancerClasses(loadBalancerClasses, fieldPath)
    
  • pkg/apis/openstack/validation/controlplane_test.go+25 0 modified
    @@ -37,6 +37,31 @@ var _ = Describe("ControlPlaneConfig validation", func() {
     			Expect(ValidateControlPlaneConfig(controlPlane, infraConfig, "", nilPath)).To(BeEmpty())
     		})
     
    +		It("should fail if the network does not have valid uuid", func() {
    +			controlPlane.LoadBalancerClasses = []api.LoadBalancerClass{
    +				{
    +					Name:              "foo",
    +					FloatingNetworkID: ptr.To("invalid-uuid"),
    +				},
    +			}
    +			errorList := ValidateControlPlaneConfig(controlPlane, infraConfig, "", nilPath)
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":  Equal(field.ErrorTypeInvalid),
    +				"Field": Equal("loadBalancerClasses[0].floatingNetworkID"),
    +			}))))
    +		})
    +
    +		It("should succeed for valid LB classes", func() {
    +			controlPlane.LoadBalancerClasses = []api.LoadBalancerClass{
    +				{
    +					Name:              "foo",
    +					FloatingNetworkID: ptr.To("123e4567-e89b-12d3-a456-426614174000"),
    +				},
    +			}
    +			errorList := ValidateControlPlaneConfig(controlPlane, infraConfig, "", nilPath)
    +			Expect(errorList).To(BeEmpty())
    +		})
    +
     		It("should require the name of a load balancer provider", func() {
     			controlPlane.LoadBalancerProvider = ""
     
    
  • pkg/apis/openstack/validation/filter.go+75 0 added
    @@ -0,0 +1,75 @@
    +//  SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
    +//
    +//  SPDX-License-Identifier: Apache-2.0
    +
    +package validation
    +
    +import (
    +	"fmt"
    +	"regexp"
    +	"unicode/utf8"
    +
    +	guuid "github.com/google/uuid"
    +	"k8s.io/apimachinery/pkg/util/validation/field"
    +)
    +
    +var (
    +	// genericNameRegex is used to validate openstack resource names. There are not many restrictions on the character set, but we will constrain brackets,braces and newlines.
    +	genericNameRegex     = `^[^{}\[\]\n]+$`
    +	validateResourceName = combineValidationFuncs(regex(genericNameRegex), notEmpty, maxLength(255))
    +)
    +
    +type validateFunc[T any] func(T, *field.Path) field.ErrorList
    +
    +// combineValidationFuncs validates a value against a list of filters.
    +func combineValidationFuncs[T any](filters ...validateFunc[T]) validateFunc[T] {
    +	return func(t T, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		for _, f := range filters {
    +			allErrs = append(allErrs, f(t, fld)...)
    +		}
    +		return allErrs
    +	}
    +}
    +
    +// regex returns a filterFunc that validates a string against a regular expression.
    +// regex allows empty strings to pass through to allow combining with other checks for clearer error messages. Use
    +// notEmpty() to check for emptiness.
    +func regex(regex string) validateFunc[string] {
    +	compiled := regexp.MustCompile(regex)
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if name == "" {
    +			// allow empty strings to pass through, use notEmpty() to check for emptiness.
    +			return allErrs
    +		}
    +		if !compiled.MatchString(name) {
    +			allErrs = append(allErrs, field.Invalid(fld, name, fmt.Sprintf("does not match expected regex %q", compiled.String())))
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func notEmpty(name string, fld *field.Path) field.ErrorList {
    +	if utf8.RuneCountInString(name) == 0 {
    +		return field.ErrorList{field.Required(fld, "cannot be empty")}
    +	}
    +	return nil
    +}
    +
    +func maxLength(max int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if l := utf8.RuneCountInString(name); l > max {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be more than %d characters, got %d", max, l))}
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func uuid(u string, fld *field.Path) field.ErrorList {
    +	if _, err := guuid.Parse(u); err != nil {
    +		return field.ErrorList{field.Invalid(fld, u, fmt.Sprintf("must be a valid UUID: %v", err))}
    +	}
    +	return nil
    +}
    
  • pkg/apis/openstack/validation/infrastructure.go+13 12 modified
    @@ -9,7 +9,6 @@ import (
     	"sort"
     
     	cidrvalidation "github.com/gardener/gardener/pkg/utils/validation/cidr"
    -	"github.com/google/uuid"
     	apivalidation "k8s.io/apimachinery/pkg/api/validation"
     	"k8s.io/apimachinery/pkg/util/sets"
     	"k8s.io/apimachinery/pkg/util/validation/field"
    @@ -21,10 +20,7 @@ import (
     // ValidateInfrastructureConfig validates a InfrastructureConfig object.
     func ValidateInfrastructureConfig(infra *api.InfrastructureConfig, nodesCIDR *string, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    -
    -	if len(infra.FloatingPoolName) == 0 {
    -		allErrs = append(allErrs, field.Required(fldPath.Child("floatingPoolName"), "must provide the name of a floating pool"))
    -	}
    +	allErrs = append(allErrs, validateResourceName(infra.FloatingPoolName, fldPath.Child("floatingPoolName"))...)
     
     	networkingPath := field.NewPath("networking")
     	var nodes cidrvalidation.CIDR
    @@ -54,17 +50,22 @@ func ValidateInfrastructureConfig(infra *api.InfrastructureConfig, nodesCIDR *st
     	}
     
     	if infra.Networks.ID != nil {
    -		if _, err := uuid.Parse(*infra.Networks.ID); err != nil {
    -			allErrs = append(allErrs, field.Invalid(networksPath.Child("id"), infra.Networks.ID, "if network ID is provided it must be a valid OpenStack UUID"))
    +		allErrs = append(allErrs, uuid(*infra.Networks.ID, networksPath.Child("id"))...)
    +	}
    +	if infra.Networks.Router != nil {
    +		if infra.Networks.Router.ID == "" {
    +			allErrs = append(allErrs, field.Invalid(networksPath.Child("router", "id"), infra.Networks.Router.ID, "router id must not be empty when router key is provided"))
    +		} else {
    +			allErrs = append(allErrs, uuid(infra.Networks.Router.ID, networksPath.Child("router").Child("id"))...)
     		}
     	}
     
    -	if infra.Networks.Router != nil && len(infra.Networks.Router.ID) == 0 {
    -		allErrs = append(allErrs, field.Invalid(networksPath.Child("router", "id"), infra.Networks.Router.ID, "router id must not be empty when router key is provided"))
    -	}
    +	if infra.FloatingPoolSubnetName != nil {
    +		allErrs = append(allErrs, validateResourceName(*infra.FloatingPoolSubnetName, fldPath.Child("floatingPoolSubnetName"))...)
     
    -	if infra.FloatingPoolSubnetName != nil && infra.Networks.Router != nil && len(infra.Networks.Router.ID) > 0 {
    -		allErrs = append(allErrs, field.Invalid(fldPath.Child("floatingPoolSubnetName"), infra.FloatingPoolSubnetName, "router id must be empty when a floating subnet name is provided"))
    +		if infra.Networks.Router != nil {
    +			allErrs = append(allErrs, field.Forbidden(fldPath.Child("router"), "router cannot be set when a floating subnet name is provided"))
    +		}
     	}
     
     	return allErrs
    
  • pkg/apis/openstack/validation/infrastructure_test.go+28 5 modified
    @@ -23,6 +23,7 @@ var _ = Describe("InfrastructureConfig validation", func() {
     	var (
     		nilPath *field.Path
     
    +		routerID          = "123e4567-e89b-12d3-a456-426614174000"
     		floatingPoolName1 = "foo"
     
     		infrastructureConfig *api.InfrastructureConfig
    @@ -36,7 +37,8 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			FloatingPoolName: floatingPoolName1,
     			Networks: api.Networks{
     				Router: &api.Router{
    -					ID: "hugo",
    +
    +					ID: routerID,
     				},
     				Workers: "10.250.0.0/16",
     			},
    @@ -45,8 +47,18 @@ var _ = Describe("InfrastructureConfig validation", func() {
     
     	Describe("#ValidateInfrastructureConfig", func() {
     		It("should forbid invalid floating pool name configuration", func() {
    -			infrastructureConfig.FloatingPoolName = ""
    +			infrastructureConfig.FloatingPoolName = "not-a-valid-{}-name"
    +			errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, nilPath)
    +			Expect(errorList).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Field": Equal("floatingPoolName"),
    +				})),
    +			))
    +		})
     
    +		It("should forbid empty floating pool name configuration", func() {
    +			infrastructureConfig.FloatingPoolName = ""
     			errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, nilPath)
     
     			Expect(errorList).To(ConsistOfFields(Fields{
    @@ -56,6 +68,17 @@ var _ = Describe("InfrastructureConfig validation", func() {
     		})
     
     		It("should forbid invalid router id configuration", func() {
    +			infrastructureConfig.Networks.Router = &api.Router{ID: "foo bar"}
    +
    +			errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, nilPath)
    +
    +			Expect(errorList).To(ConsistOfFields(Fields{
    +				"Type":  Equal(field.ErrorTypeInvalid),
    +				"Field": Equal("networks.router.id"),
    +			}))
    +		})
    +
    +		It("should forbid empty router id configuration", func() {
     			infrastructureConfig.Networks.Router = &api.Router{ID: ""}
     
     			errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, nilPath)
    @@ -67,14 +90,14 @@ var _ = Describe("InfrastructureConfig validation", func() {
     		})
     
     		It("should forbid floating ip subnet when router is specified", func() {
    -			infrastructureConfig.Networks.Router = &api.Router{ID: "sample-router-id"}
     			infrastructureConfig.FloatingPoolSubnetName = ptr.To("sample-floating-pool-subnet-id")
     
     			errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, nilPath)
     
     			Expect(errorList).To(ConsistOfFields(Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("floatingPoolSubnetName"),
    +				"Type":   Equal(field.ErrorTypeForbidden),
    +				"Field":  Equal("router"),
    +				"Detail": Equal("router cannot be set when a floating subnet name is provided"),
     			}))
     		})
     	})
    
  • pkg/apis/openstack/validation/shoot.go+2 1 modified
    @@ -187,7 +187,8 @@ func validateMachineLabels(worker *core.Worker, workerConfig *api.WorkerConfig,
     	machineLabelNames := sets.New[string]()
     	for i, ml := range workerConfig.MachineLabels {
     		idxPath := fldPath.Index(i)
    -
    +		allErrs = append(allErrs, validateResourceName(ml.Name, idxPath.Child("name"))...)
    +		allErrs = append(allErrs, validateResourceName(ml.Value, idxPath.Child("value"))...)
     		if machineLabelNames.Has(ml.Name) {
     			allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ml.Name))
     		} else if _, found := worker.Labels[ml.Name]; found {
    
  • pkg/apis/openstack/validation/shoot_test.go+29 1 modified
    @@ -239,10 +239,38 @@ var _ = Describe("Shoot validation", func() {
     					}
     
     					errorList := ValidateWorkers(workers, nil, nilPath)
    -
     					Expect(errorList).To(BeEmpty())
     				})
     
    +				It("should fail for invalid machine labels", func() {
    +					workers[0].ProviderConfig = &runtime.RawExtension{
    +						Object: &apiv1alpha1.WorkerConfig{
    +							TypeMeta: metav1.TypeMeta{
    +								Kind:       "WorkerConfig",
    +								APIVersion: apiv1alpha1.SchemeGroupVersion.String(),
    +							},
    +							MachineLabels: []apiv1alpha1.MachineLabel{
    +								{Name: "foo\nbar", Value: "bar"},
    +								{Name: "m2", Value: "bar\nbaz"},
    +							},
    +						},
    +					}
    +
    +					errorList := ValidateWorkers(workers, nil, nilPath)
    +					Expect(errorList).To(ConsistOf(
    +						PointTo(MatchFields(IgnoreExtras, Fields{
    +							"Type":     Equal(field.ErrorTypeInvalid),
    +							"BadValue": Equal("foo\nbar"),
    +							"Field":    Equal("[0].providerConfig.machineLabels[0].name"),
    +						})),
    +						PointTo(MatchFields(IgnoreExtras, Fields{
    +							"Type":     Equal(field.ErrorTypeInvalid),
    +							"Field":    Equal("[0].providerConfig.machineLabels[1].value"),
    +							"BadValue": Equal("bar\nbaz"),
    +						})),
    +					))
    +
    +				})
     				It("should fail on duplicate labels", func() {
     					workers[0].Labels = map[string]string{"l1": "x", "l2": "x"}
     					workers[0].ProviderConfig = &runtime.RawExtension{
    
4573a4404969

Shoot input validation (#1295)

https://github.com/gardener/gardener-extension-provider-azureKonstantinos AngelopoulosSep 18, 2025via ghsa
6 files changed · +544 152
  • pkg/apis/azure/types_controlplane.go+1 1 modified
    @@ -20,7 +20,7 @@ type ControlPlaneConfig struct {
     
     	// Storage contains configuration for storage in the cluster.
     	// +optional
    -	Storage *Storage `json:"storage,omitempty"`
    +	Storage *Storage
     }
     
     // CloudControllerManagerConfig contains configuration settings for the cloud-controller-manager.
    
  • pkg/apis/azure/validation/filter.go+123 0 added
    @@ -0,0 +1,123 @@
    +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
    +//
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package validation
    +
    +import (
    +	"fmt"
    +	"net/url"
    +	"regexp"
    +	"unicode/utf8"
    +
    +	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    +	"github.com/google/uuid"
    +	"k8s.io/apimachinery/pkg/util/validation/field"
    +)
    +
    +// There is no enum for Azure Service Endpoints, so we use a regex to validate the names.
    +var (
    +	resourceGroupNameRegex       = `^[A-Za-z0-9_().-]{1,89}[A-Za-z0-9_()-]$`
    +	vnetNameRegex                = `^[A-Za-z0-9][\w.-]*[\w]$`
    +	genericAzureNameRegex        = `^[A-Za-z0-9][\w-]*$`
    +	serviceEndpointsRegex        = `^Microsoft\.[A-Za-z0-9.]+$`
    +	storageURIRegex              = `^https://[a-z0-9-]{3,24}\.blob\.core[^\s]+/[^\s]+$`
    +	urnRegex                     = `^[\w-]+:[\w-]+:[\w.-]+:[\w.-]+$`
    +	sharedGalleryImageIDRegex    = `^/SharedGalleries/[\w-]+/Images/[\w-]+/Versions/[\w.-]+$`
    +	communityGalleryImageIDRegex = `^/CommunityGalleries/[\w-]+/Images/[\w-]+/Versions/[\w.-]+$`
    +
    +	validateServiceEndpoint           = combineValidationFuncs(regex(serviceEndpointsRegex), minLength(9), maxLength(120))
    +	validateResourceGroup             = combineValidationFuncs(regex(resourceGroupNameRegex), maxLength(90))
    +	validateVnetName                  = combineValidationFuncs(regex(vnetNameRegex), minLength(2), maxLength(64))
    +	validateGenericName               = combineValidationFuncs(regex(genericAzureNameRegex), minLength(3), maxLength(120))
    +	validatePublicIP                  = combineValidationFuncs(regex(genericAzureNameRegex), maxLength(80))
    +	storageURIValidation              = combineValidationFuncs(urlFilter, regex(storageURIRegex), notEmpty)
    +	urnValidation                     = combineValidationFuncs(regex(urnRegex), notEmpty, maxLength(256))
    +	sharedGalleryImageIDValidation    = combineValidationFuncs(regex(sharedGalleryImageIDRegex), notEmpty, maxLength(512))
    +	communityGalleryImageIDValidation = combineValidationFuncs(regex(communityGalleryImageIDRegex), notEmpty, maxLength(512))
    +)
    +
    +type validateFunc[T any] func(T, *field.Path) field.ErrorList
    +
    +// combineValidationFuncs validates a value against a list of filters.
    +func combineValidationFuncs[T any](filters ...validateFunc[T]) validateFunc[T] {
    +	return func(t T, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		for _, f := range filters {
    +			allErrs = append(allErrs, f(t, fld)...)
    +		}
    +		return allErrs
    +	}
    +}
    +
    +// regex returns a filterFunc that validates a string against a regular expression.
    +func regex(regex string) validateFunc[string] {
    +	compiled := regexp.MustCompile(regex)
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if name == "" {
    +			return allErrs // Allow empty strings to pass through
    +		}
    +		if !compiled.MatchString(name) {
    +			allErrs = append(allErrs, field.Invalid(fld, name, fmt.Sprintf("does not match expected regex %s", compiled.String())))
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func minLength(min int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if l := utf8.RuneCountInString(name); l < min {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be fewer than %d characters, got %d", min, l))}
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func notEmpty(name string, fld *field.Path) field.ErrorList {
    +	if utf8.RuneCountInString(name) == 0 {
    +		return field.ErrorList{field.Required(fld, name)}
    +	}
    +	return nil
    +}
    +
    +func maxLength(max int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if l := utf8.RuneCountInString(name); l > max {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be more than %d characters, got %d", max, l))}
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func urlFilter(u string, fld *field.Path) field.ErrorList {
    +	_, err := url.Parse(u)
    +	if err != nil {
    +		return field.ErrorList{field.Invalid(fld, u, fmt.Sprintf("must be a valid URL: %v", err))}
    +	}
    +	return nil
    +}
    +
    +func validateResourceID(rid *arm.ResourceID, resourceType *string, fld *field.Path) field.ErrorList {
    +	var allErrs field.ErrorList
    +	if rid == nil {
    +		return allErrs
    +	}
    +
    +	if rid.SubscriptionID != "" {
    +		if _, err := uuid.Parse(rid.SubscriptionID); err != nil {
    +			allErrs = append(allErrs, field.Invalid(fld, rid.SubscriptionID, fmt.Sprintf("must be a valid Azure subscription ID: %v", err)))
    +		}
    +	}
    +
    +	if rid.ResourceGroupName != "" {
    +		allErrs = append(allErrs, validateResourceGroup(rid.ResourceGroupName, fld.Child("resourceGroupName"))...)
    +	}
    +	if resourceType != nil {
    +		allErrs = append(allErrs, validateGenericName(*resourceType, fld.Child("resourceType"))...)
    +	}
    +
    +	return allErrs
    +}
    
  • pkg/apis/azure/validation/infrastructure.go+58 16 modified
    @@ -7,6 +7,7 @@ package validation
     import (
     	"fmt"
     
    +	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
     	"github.com/gardener/gardener/pkg/apis/core"
     	gardencorehelper "github.com/gardener/gardener/pkg/apis/core/helper"
     	gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
    @@ -15,6 +16,7 @@ import (
     	apivalidation "k8s.io/apimachinery/pkg/api/validation"
     	"k8s.io/apimachinery/pkg/util/sets"
     	"k8s.io/apimachinery/pkg/util/validation/field"
    +	"k8s.io/utils/ptr"
     
     	apisazure "github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure"
     	"github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure/helper"
    @@ -53,7 +55,7 @@ func validateInfrastructureConfigZones(oldInfra, infra *apisazure.Infrastructure
     	}
     
     	for i, zone := range infra.Networks.Zones {
    -		// Search if the current zones were alrady present and valid in the old infrastructureConfig and if so, skip further checks.
    +		// Search if the current zones were already present and valid in the old infrastructureConfig and if so, skip further checks.
     		// This safeguards from breaking existing shoots in case a zone has been removed from the CloudProfile.
     		if oldInfra != nil && len(oldInfra.Networks.Zones) > 0 {
     			matchFound := false
    @@ -105,13 +107,19 @@ func ValidateInfrastructureConfig(infra *apisazure.InfrastructureConfig, shoot *
     	// validation here will fail for those cases.
     	// TODO: remove the following block and uncomment below blocks once deployment into existing resource groups works properly.
     	if infra.ResourceGroup != nil {
    +		allErrs = append(allErrs, validateResourceGroup(infra.ResourceGroup.Name, fldPath.Child("resourceGroup.name"))...)
     		allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceGroup"), infra.ResourceGroup, "specifying an existing resource group is not supported yet"))
     	}
     
     	allErrs = append(allErrs, validateNetworkConfig(shoot, infra, nodes, pods, services, fldPath)...)
     
    -	if infra.Identity != nil && (infra.Identity.Name == "" || infra.Identity.ResourceGroup == "") {
    -		allErrs = append(allErrs, field.Invalid(fldPath.Child("identity"), infra.Identity, "specifying an identity requires the name of the identity and the resource group which hosts the identity"))
    +	if infra.Identity != nil {
    +		path := fldPath.Child("identity")
    +		if infra.Identity.Name == "" || infra.Identity.ResourceGroup == "" {
    +			allErrs = append(allErrs, field.Invalid(fldPath.Child("identity"), infra.Identity, "specifying an identity requires the name of the identity and the resource group which hosts the identity"))
    +		}
    +		allErrs = append(allErrs, validateResourceGroup(infra.Identity.ResourceGroup, path.Child("resourceGroup"))...)
    +		allErrs = append(allErrs, validateGenericName(infra.Identity.Name, path.Child("name"))...)
     	}
     
     	return allErrs
    @@ -163,6 +171,10 @@ func validateNetworkConfig(
     		}
     
     		allErrs = append(allErrs, validateNatGatewayConfig(config.NatGateway, helper.HasShootVmoMigrationAnnotation(shoot.GetAnnotations()), networksPath.Child("natGateway"))...)
    +
    +		for idx := range config.ServiceEndpoints {
    +			allErrs = append(allErrs, validateServiceEndpoint(config.ServiceEndpoints[idx], networksPath.Child("serviceEndpoints").Index(idx))...)
    +		}
     		return allErrs
     	}
     
    @@ -188,19 +200,34 @@ func validateVnetConfig(networkConfig *apisazure.NetworkConfig, resourceGroupCon
     		allErrs    = field.ErrorList{}
     		vnetConfig = networkConfig.VNet
     	)
    -
     	// Validate that just vnet name or vnet resource group is specified.
    -	if (networkConfig.VNet.Name != nil && vnetConfig.ResourceGroup == nil) || (vnetConfig.Name == nil && vnetConfig.ResourceGroup != nil) {
    +	if (vnetConfig.Name != nil && vnetConfig.ResourceGroup == nil) || (vnetConfig.Name == nil && vnetConfig.ResourceGroup != nil) {
     		return append(allErrs, field.Invalid(vNetPath, vnetConfig, "a vnet cidr or vnet name and resource group need to be specified"))
     	}
     
    +	if vnetConfig.DDosProtectionPlanID != nil {
    +		fldPath := vNetPath.Child("ddosProtectionPlanID")
    +		resourceID, err := arm.ParseResourceID(*vnetConfig.DDosProtectionPlanID)
    +		if err != nil {
    +			return append(allErrs, field.Invalid(fldPath, vnetConfig.DDosProtectionPlanID, fmt.Sprintf("invalid DDoS protection plan ID: %v", err)))
    +		}
    +
    +		allErrs = append(allErrs, validateResourceID(resourceID, ptr.To("ddosProtectionPlans"), fldPath)...)
    +		allErrs = append(validateGenericName(resourceID.Name, fldPath), allErrs...)
    +	}
    +
     	if isExternalVnetUsed(&networkConfig.VNet) {
     		if *networkConfig.VNet.Name == "" {
    -			allErrs = append(allErrs, field.Required(vNetPath.Child("name"), "the vnet name must not be empty"))
    +			allErrs = append(allErrs, field.Required(vNetPath.Child("name"), "the vnet name must not be empty if specifying an existing vnet"))
     		}
    +
     		if *networkConfig.VNet.ResourceGroup == "" {
    -			allErrs = append(allErrs, field.Required(vNetPath.Child("resourceGroup"), "the vnet resource group must not be empty"))
    +			allErrs = append(allErrs, field.Required(vNetPath.Child("resourceGroup"), "the vnet resource group must not be empty if specifying an existing vnet"))
     		}
    +
    +		allErrs = append(allErrs, validateResourceGroup(*vnetConfig.ResourceGroup, vNetPath.Child("resourceGroup"))...)
    +		allErrs = append(allErrs, validateVnetName(*vnetConfig.Name, vNetPath.Child("name"))...)
    +
     		if networkConfig.VNet.CIDR != nil {
     			allErrs = append(allErrs, field.Invalid(vNetPath.Child("cidr"), vnetConfig, "specifying a cidr for an existing vnet is not possible"))
     		}
    @@ -261,6 +288,11 @@ func validateZones(zones []apisazure.Zone, nodes, pods, services cidrvalidation.
     		zoneCIDRs = append(zoneCIDRs, zoneCIDR)
     		allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(zoneCIDR.GetFieldPath(), zoneCIDR.GetCIDR())...)
     
    +		// service endpoint validation
    +		for idx, se := range zone.ServiceEndpoints {
    +			allErrs = append(allErrs, validateServiceEndpoint(se, fld.Child("serviceEndpoints").Index(idx))...)
    +		}
    +
     		// NAT validation
     		allErrs = append(allErrs, validateZonedNatGatewayConfig(zone.NatGateway, zonePath.Child("natGateway"))...)
     	}
    @@ -299,7 +331,12 @@ func validateNatGatewayConfig(natGatewayConfig *apisazure.NatGatewayConfig, hasS
     	}
     
     	if natGatewayConfig.IdleConnectionTimeoutMinutes != nil && (*natGatewayConfig.IdleConnectionTimeoutMinutes < natGatewayMinTimeoutInMinutes || *natGatewayConfig.IdleConnectionTimeoutMinutes > natGatewayMaxTimeoutInMinutes) {
    -		allErrs = append(allErrs, field.Invalid(natGatewayPath.Child("idleConnectionTimeoutMinutes"), *natGatewayConfig.IdleConnectionTimeoutMinutes, "idleConnectionTimeoutMinutes values must range between 4 and 120"))
    +		allErrs = append(allErrs, field.Invalid(natGatewayPath.Child("idleConnectionTimeoutMinutes"), *natGatewayConfig.IdleConnectionTimeoutMinutes, fmt.Sprintf("idleConnectionTimeoutMinutes values must range between %d and %d", natGatewayMinTimeoutInMinutes, natGatewayMaxTimeoutInMinutes)))
    +	}
    +
    +	for idx := range natGatewayConfig.IPAddresses {
    +		allErrs = append(allErrs, validateResourceGroup(natGatewayConfig.IPAddresses[idx].ResourceGroup, natGatewayPath.Child("ipAddresses").Index(idx).Child("resourceGroup"))...)
    +		allErrs = append(allErrs, validatePublicIP(natGatewayConfig.IPAddresses[idx].Name, natGatewayPath.Child("ipAddresses").Index(idx).Child("name"))...)
     	}
     
     	if natGatewayConfig.Zone == nil {
    @@ -322,18 +359,20 @@ func validateNatGatewayIPReference(publicIPReferences []apisazure.PublicIPRefere
     			allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("name"), "Name for NatGateway public ip resource is required"))
     		}
     		if publicIPRef.ResourceGroup == "" {
    -			allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("resourceGroup"), "ResourceGroup for NatGateway public ip resouce is required"))
    +			allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("resourceGroup"), "ResourceGroup for NatGateway public ip resource is required"))
     		}
     	}
     	return allErrs
     }
     
     func validateZonedNatGatewayConfig(natGatewayConfig *apisazure.ZonedNatGatewayConfig, natGatewayPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    -
     	if natGatewayConfig == nil {
     		return nil
     	}
    +	if natGatewayConfig.IdleConnectionTimeoutMinutes != nil && (*natGatewayConfig.IdleConnectionTimeoutMinutes < natGatewayMinTimeoutInMinutes || *natGatewayConfig.IdleConnectionTimeoutMinutes > natGatewayMaxTimeoutInMinutes) {
    +		allErrs = append(allErrs, field.Invalid(natGatewayPath.Child("idleConnectionTimeoutMinutes"), *natGatewayConfig.IdleConnectionTimeoutMinutes, fmt.Sprintf("idleConnectionTimeoutMinutes values must range between %d and %d", natGatewayMinTimeoutInMinutes, natGatewayMaxTimeoutInMinutes)))
    +	}
     
     	if !natGatewayConfig.Enabled {
     		if natGatewayConfig.IdleConnectionTimeoutMinutes != nil || natGatewayConfig.IPAddresses != nil {
    @@ -348,13 +387,16 @@ func validateZonedNatGatewayConfig(natGatewayConfig *apisazure.ZonedNatGatewayCo
     
     func validateZonedPublicIPReference(publicIPReferences []apisazure.ZonedPublicIPReference, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    -	for i, publicIPRef := range publicIPReferences {
    -		if publicIPRef.Name == "" {
    -			allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("name"), "Name for NatGateway public ip resource is required"))
    +	for idx, ipRef := range publicIPReferences {
    +		ipFld := fldPath.Index(idx)
    +		if ipRef.ResourceGroup == "" {
    +			allErrs = append(allErrs, field.Required(ipFld.Child("resourceGroup"), "ResourceGroup for NatGateway public ip resource is required"))
     		}
    -		if publicIPRef.ResourceGroup == "" {
    -			allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("resourceGroup"), "ResourceGroup for NatGateway public ip resouce is required"))
    +		allErrs = append(allErrs, validateResourceGroup(ipRef.ResourceGroup, ipFld.Child("resourceGroup"))...)
    +		if ipRef.Name == "" {
    +			allErrs = append(allErrs, field.Required(fldPath.Index(idx).Child("name"), "Name for NatGateway public ip resource is required"))
     		}
    +		allErrs = append(allErrs, validatePublicIP(ipRef.Name, ipFld.Child("name"))...)
     	}
     	return allErrs
     }
    @@ -477,7 +519,7 @@ func isExternalVnetUsed(vnetConfig *apisazure.VNet) bool {
     	if vnetConfig == nil {
     		return false
     	}
    -	if vnetConfig.Name != nil && vnetConfig.ResourceGroup != nil {
    +	if vnetConfig.Name != nil || vnetConfig.ResourceGroup != nil {
     		return true
     	}
     	return false
    
  • pkg/apis/azure/validation/infrastructure_test.go+113 20 modified
    @@ -53,19 +53,115 @@ var _ = Describe("InfrastructureConfig validation", func() {
     				},
     			},
     		}
    -		ddosProtectionPlanID = "/subscriptions/test/resourceGroups/test/providers/Microsoft.Network/ddosProtectionPlans/test-ddos-protection-plan"
    +		ddosProtectionPlanID = "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/test/providers/Microsoft.Network/ddosProtectionPlans/test-ddos-protection-plan"
     	})
     
     	Describe("#ValidateInfrastructureConfig", func() {
    +		Describe("ValidateInput", func() {
    +			It("should forbid specifying an invalid resource group name", func() {
    +				infrastructureConfig.ResourceGroup = &apisazure.ResourceGroup{
    +					Name: "invalid-resource-group-name[[]]",
    +				}
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ContainElement(PointTo(MatchFields(IgnoreExtras,
    +					Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("resourceGroup.name"),
    +						"Detail": ContainSubstring("does not match expected regex"),
    +					},
    +				))))
    +			})
    +			It("should forbid specifying an invalid vnet input", func() {
    +				infrastructureConfig.Networks.VNet = apisazure.VNet{
    +					Name:          ptr.To("invalid-vnet-name[[]]"),
    +					ResourceGroup: ptr.To("invalid-resource-group-name[[]]"),
    +				}
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.vnet.resourceGroup"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				})), PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.vnet.name"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				}))))
    +			})
    +			It("should forbid specifying a vnet with less characters", func() {
    +				infrastructureConfig.Networks.VNet = apisazure.VNet{
    +					Name:          ptr.To("v"),
    +					ResourceGroup: ptr.To("resource-group"),
    +				}
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.vnet.name"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				})), PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.vnet.name"),
    +					"Detail": ContainSubstring("must not be fewer"),
    +				}))))
    +			})
    +			It("should forbid specifying an invalid vnet ddosProtectionPlan", func() {
    +				infrastructureConfig.Networks.VNet.DDosProtectionPlanID = ptr.To("invalid-ddos")
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ConsistOfFields(
    +					Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.vnet.ddosProtectionPlanID"),
    +						"Detail": ContainSubstring("invalid resource ID"),
    +					}))
    +			})
    +			It("should forbid specifying an invalid managed identity", func() {
    +				infrastructureConfig.Identity = &apisazure.IdentityConfig{
    +					Name:          "invalid-name{{}}",
    +					ResourceGroup: "invalid resource group name",
    +				}
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ConsistOfFields(Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("identity.resourceGroup"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				}, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("identity.name"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				}))
    +			})
    +			It("should forbid specifying invalid service endpoints", func() {
    +				infrastructureConfig.Networks.ServiceEndpoints = []string{
    +					"foo",
    +					"Microsoft.Storage",
    +					"GCP.Storage",
    +				}
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    +				Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.serviceEndpoints[0]"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				})), PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.serviceEndpoints[0]"),
    +					"Detail": ContainSubstring("must not be fewer"),
    +				})), PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("networks.serviceEndpoints[2]"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				}))))
    +			})
    +		})
    +
     		It("should forbid specifying a resource group configuration", func() {
     			infrastructureConfig.ResourceGroup = &apisazure.ResourceGroup{}
     
     			errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    -
    -			Expect(errorList).To(ConsistOfFields(Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("resourceGroup"),
    -			}))
    +			Expect(errorList).To(ConsistOfFields(
    +				Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("resourceGroup"),
    +					"Detail": ContainSubstring("specifying an existing resource group is not supported yet"),
    +				}))
     		})
     
     		Context("vnet", func() {
    @@ -90,7 +186,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     					ResourceGroup: &vnetGroup,
     				}
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
    -
     				Expect(errorList).To(ConsistOfFields(
     					Fields{
     						"Type":   Equal(field.ErrorTypeInvalid),
    @@ -106,17 +201,15 @@ var _ = Describe("InfrastructureConfig validation", func() {
     				}
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
     
    -				Expect(errorList).To(ConsistOfFields(
    -					Fields{
    -						"Type":   Equal(field.ErrorTypeRequired),
    -						"Field":  Equal("networks.vnet.name"),
    -						"Detail": Equal("the vnet name must not be empty"),
    -					},
    -					Fields{
    -						"Type":   Equal(field.ErrorTypeRequired),
    -						"Field":  Equal("networks.vnet.resourceGroup"),
    -						"Detail": Equal("the vnet resource group must not be empty"),
    -					}))
    +				Expect(errorList).To(ContainElements(PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeRequired),
    +					"Field":  Equal("networks.vnet.name"),
    +					"Detail": Equal("the vnet name must not be empty if specifying an existing vnet"),
    +				})), PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeRequired),
    +					"Field":  Equal("networks.vnet.resourceGroup"),
    +					"Detail": Equal("the vnet resource group must not be empty if specifying an existing vnet"),
    +				}))))
     			})
     
     			It("should forbid specifying existing vnet plus a vnet cidr", func() {
    @@ -322,7 +415,7 @@ var _ = Describe("InfrastructureConfig validation", func() {
     
     			It("should return errors because no name or resource group is given", func() {
     				infrastructureConfig.Identity = &apisazure.IdentityConfig{
    -					Name: "test-identiy",
    +					Name: "test-identity",
     				}
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &shoot, providerPath)
     				Expect(errorList).To(ConsistOfFields(Fields{
    @@ -411,7 +504,7 @@ var _ = Describe("InfrastructureConfig validation", func() {
     					Expect(errorList).To(ConsistOfFields(Fields{
     						"Type":   Equal(field.ErrorTypeRequired),
     						"Field":  Equal("networks.natGateway.ipAddresses[0].resourceGroup"),
    -						"Detail": Equal("ResourceGroup for NatGateway public ip resouce is required"),
    +						"Detail": Equal("ResourceGroup for NatGateway public ip resource is required"),
     					}))
     				})
     			})
    
  • pkg/apis/azure/validation/worker.go+53 15 modified
    @@ -8,11 +8,14 @@ import (
     	"fmt"
     	"slices"
     
    +	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
     	"github.com/gardener/gardener/pkg/apis/core"
     	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
     	corev1 "k8s.io/api/core/v1"
     	"k8s.io/apimachinery/pkg/api/resource"
    +	"k8s.io/apimachinery/pkg/util/sets"
     	"k8s.io/apimachinery/pkg/util/validation/field"
    +	"k8s.io/utils/ptr"
     
     	apiazure "github.com/gardener/gardener-extension-provider-azure/pkg/apis/azure"
     )
    @@ -21,11 +24,19 @@ import (
     func ValidateWorkerConfig(workerConfig *apiazure.WorkerConfig, dataVolumes []core.DataVolume, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	if workerConfig != nil {
    -		allErrs = append(allErrs, validateNodeTemplate(workerConfig.NodeTemplate, fldPath)...)
    -		allErrs = append(allErrs, validateDataVolumeConf(workerConfig.DataVolumes, dataVolumes, fldPath)...)
    +	if workerConfig == nil {
    +		return allErrs
     	}
     
    +	if diagnosticsProfile := workerConfig.DiagnosticsProfile; diagnosticsProfile != nil {
    +		if diagnosticsProfile.StorageURI != nil {
    +			allErrs = append(allErrs, storageURIValidation(*diagnosticsProfile.StorageURI, fldPath.Child("diagnosticsProfile").Child("storageURI"))...)
    +		}
    +	}
    +
    +	allErrs = append(allErrs, validateNodeTemplate(workerConfig.NodeTemplate, fldPath.Child("nodeTemplate"))...)
    +	allErrs = append(allErrs, validateDataVolumeConf(workerConfig.DataVolumes, dataVolumes, fldPath)...)
    +
     	return allErrs
     }
     
    @@ -35,37 +46,64 @@ func validateNodeTemplate(nodeTemplate *extensionsv1alpha1.NodeTemplate, fldPath
     	if nodeTemplate == nil {
     		return nil
     	}
    -	for _, capacityAttribute := range []corev1.ResourceName{corev1.ResourceCPU, "gpu", corev1.ResourceMemory} {
    +	resources := []corev1.ResourceName{corev1.ResourceCPU, "gpu", corev1.ResourceMemory, corev1.ResourceStorage, corev1.ResourceEphemeralStorage}
    +	resourceSet := sets.New[corev1.ResourceName](resources...)
    +	for resourceName := range nodeTemplate.Capacity {
    +		if !resourceSet.Has(resourceName) {
    +			allErrs = append(allErrs, field.Invalid(fldPath.Child("capacity").Child(string(resourceName)), resourceName, fmt.Sprintf("%s is an unsupported resource name. Valid values are: %v", resourceName, resourceSet.UnsortedList())))
    +		}
    +	}
    +
    +	for _, capacityAttribute := range resources {
     		value, ok := nodeTemplate.Capacity[capacityAttribute]
     		if !ok {
    -			allErrs = append(allErrs, field.Required(fldPath.Child("nodeTemplate").Child("capacity"), fmt.Sprintf("%s is a mandatory field", capacityAttribute)))
     			continue
     		}
    -		allErrs = append(allErrs, validateResourceQuantityValue(capacityAttribute, value, fldPath.Child("nodeTemplate").Child("capacity").Child(string(capacityAttribute)))...)
    +		allErrs = append(allErrs, validateResourceQuantityValue(capacityAttribute, value, fldPath.Child("capacity").Child(string(capacityAttribute)))...)
     	}
     
     	return allErrs
     }
     
     func validateDataVolumeConf(dataVolumeConfigs []apiazure.DataVolume, dataVolumes []core.DataVolume, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    -	imageRefPath := fldPath.Child("dataVolumes").Child("ImageRef")
    -	namePath := fldPath.Child("dataVolumes").Child("Name")
     	var dataVolumeNames []string
     
     	for _, dataVolume := range dataVolumes {
     		dataVolumeNames = append(dataVolumeNames, dataVolume.Name)
     	}
     
    -	for _, dataVolumeConf := range dataVolumeConfigs {
    -		if dataVolumeConf.ImageRef != nil && *dataVolumeConf.ImageRef == (apiazure.Image{}) {
    -			allErrs = append(allErrs, field.Invalid(imageRefPath, dataVolumeConf.ImageRef, "imageRef is defined but empty"))
    -		}
    -		if !slices.Contains(dataVolumeNames, dataVolumeConf.Name) {
    -			allErrs = append(allErrs, field.Invalid(namePath, dataVolumeConf.Name, "no dataVolume with this name exists"))
    +	for idx, dataVolumeConf := range dataVolumeConfigs {
    +		dvPath := fldPath.Child("dataVolumes").Index(idx)
    +		imgRefPath := dvPath.Child("imageRef")
    +
    +		if imgRef := dataVolumeConf.ImageRef; imgRef != nil {
    +			if !slices.Contains(dataVolumeNames, dataVolumeConf.Name) {
    +				allErrs = append(allErrs, field.Invalid(dvPath.Child("name"), dataVolumeConf.Name, "no dataVolume with this name exists"))
    +			}
    +
    +			if *imgRef == (apiazure.Image{}) {
    +				allErrs = append(allErrs, field.Invalid(imgRefPath, dataVolumeConf.ImageRef, "imageRef is defined but empty"))
    +			}
    +			if imgRef.URN != nil {
    +				allErrs = append(allErrs, urnValidation(*imgRef.URN, imgRefPath.Child("urn"))...)
    +			}
    +			if imgRef.CommunityGalleryImageID != nil {
    +				allErrs = append(allErrs, communityGalleryImageIDValidation(*imgRef.CommunityGalleryImageID, imgRefPath.Child("communityGalleryImageID"))...)
    +			}
    +			if imgRef.SharedGalleryImageID != nil {
    +				allErrs = append(allErrs, sharedGalleryImageIDValidation(*imgRef.SharedGalleryImageID, imgRefPath.Child("sharedGalleryImageID"))...)
    +			}
    +			if imgRef.ID != nil {
    +				resourceID, err := arm.ParseResourceID(*imgRef.ID)
    +				if err != nil {
    +					return append(allErrs, field.Invalid(imgRefPath.Child("id"), *imgRef.ID, fmt.Sprintf("invalid image ID: %v", err)))
    +				}
    +
    +				allErrs = append(allErrs, validateResourceID(resourceID, ptr.To("images"), imgRefPath.Child("images"))...)
    +			}
     		}
     	}
    -
     	return allErrs
     }
     
    
  • pkg/apis/azure/validation/worker_test.go+196 100 modified
    @@ -19,109 +19,205 @@ import (
     )
     
     var _ = Describe("ValidateWorkerConfig", func() {
    -	Describe("#ValidateWorkerConfig", func() {
    -		var (
    -			fldPath = field.NewPath("config")
    -		)
    -
    -		Describe("NodeTemplate", func() {
    -			It("should return no errors for a valid nodetemplate configuration", func() {
    -				nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{
    -						corev1.ResourceCPU:    resource.MustParse("1"),
    -						corev1.ResourceMemory: resource.MustParse("50Gi"),
    -						"gpu":                 resource.MustParse("0"),
    -					},
    -				}
    -				Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(BeEmpty())
    -			})
    -
    -			It("should return error when all resources not specified", func() {
    -				nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{
    -						"gpu": resource.MustParse("0"),
    -					},
    -				}
    -
    -				Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(ConsistOf(
    -					PointTo(MatchFields(IgnoreExtras, Fields{
    -						"Type":  Equal(field.ErrorTypeRequired),
    -						"Field": Equal("config.nodeTemplate.capacity"),
    -					})),
    -					PointTo(MatchFields(IgnoreExtras, Fields{
    -						"Type":  Equal(field.ErrorTypeRequired),
    -						"Field": Equal("config.nodeTemplate.capacity"),
    -					})),
    -				))
    -			})
    -
    -			It("should return error when resource value is negative", func() {
    -				nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{
    -						corev1.ResourceCPU:    resource.MustParse("1"),
    -						corev1.ResourceMemory: resource.MustParse("-50Gi"),
    -						"gpu":                 resource.MustParse("0"),
    -					},
    -				}
    -
    -				Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(ConsistOf(
    -					PointTo(MatchFields(IgnoreExtras, Fields{
    -						"Type":  Equal(field.ErrorTypeInvalid),
    -						"Field": Equal("config.nodeTemplate.capacity.memory"),
    -					})),
    -				))
    -			})
    +	var (
    +		fldPath   *field.Path
    +		workerCfg *apisazure.WorkerConfig
    +	)
    +
    +	BeforeEach(func() {
    +		workerCfg = &apisazure.WorkerConfig{}
    +		fldPath = field.NewPath("config")
    +	})
    +
    +	Describe("#DiagnosticsProfile", func() {
    +		It("should accept valid input", func() {
    +			workerCfg.DiagnosticsProfile = &apisazure.DiagnosticsProfile{
    +				Enabled:    true,
    +				StorageURI: ptr.To("https://mystorageaccount.blob.core.windows.net/mycontainer"),
    +			}
    +			Expect(ValidateWorkerConfig(workerCfg, nil, fldPath)).To(BeEmpty())
    +		})
    +
    +		It("should reject invalid input", func() {
    +			workerCfg.DiagnosticsProfile = &apisazure.DiagnosticsProfile{
    +				Enabled:    true,
    +				StorageURI: ptr.To("foobar"),
    +			}
    +			errorList := ValidateWorkerConfig(workerCfg, nil, fldPath)
    +			Expect(errorList).To(ContainElement(PointTo(MatchFields(IgnoreExtras,
    +				Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.diagnosticsProfile.storageURI"),
    +					"Detail": ContainSubstring("does not match expected regex"),
    +				}))))
    +		})
    +	})
    +
    +	Describe("NodeTemplate", func() {
    +		It("should return no errors for a valid nodetemplate configuration", func() {
    +			nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{
    +					corev1.ResourceCPU:    resource.MustParse("1"),
    +					corev1.ResourceMemory: resource.MustParse("50Gi"),
    +					"gpu":                 resource.MustParse("0"),
    +				},
    +			}
    +			Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(BeEmpty())
    +		})
    +
    +		It("should return error when invalid resources are specified", func() {
    +			nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{
    +					"foo":                 resource.MustParse("0"),
    +					corev1.ResourceCPU:    resource.MustParse("1"),
    +					corev1.ResourceMemory: resource.MustParse("50Gi"),
    +					"gpu":                 resource.MustParse("0"),
    +				},
    +			}
    +
    +			Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.capacity.foo"),
    +					"Detail": ContainSubstring("foo is an unsupported resource name."),
    +				})),
    +			))
     		})
     
    -		Describe("DataVolumes", func() {
    -			It("should allow valid DataVolumes config", func() {
    -				dataVolumes := []core.DataVolume{{
    -					Name: "test-disk",
    -				}}
    -				dataVolumeConfigs := []apisazure.DataVolume{{
    -					Name: "test-disk",
    -					ImageRef: &apisazure.Image{
    -						URN: ptr.To("sap:gardenlinux:greatest:1312.0.0"),
    -					},
    -				}}
    -
    -				Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(BeEmpty())
    -			})
    -
    -			It("should forbid config of none existing DataVolume", func() {
    -				var dataVolumes []core.DataVolume
    -				dataVolumeConfigs := []apisazure.DataVolume{{
    -					Name: "does not exist",
    -					ImageRef: &apisazure.Image{
    -						URN: ptr.To("sap:gardenlinux:greatest:1312.0.0"),
    -					},
    -				}}
    -
    -				Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    -					PointTo(MatchFields(IgnoreExtras, Fields{
    -						"Type":  Equal(field.ErrorTypeInvalid),
    -						"Field": Equal("config.dataVolumes.Name"),
    -					})),
    -				))
    -			})
    -
    -			It("should forbid empty DataVolume ImageRef", func() {
    -				dataVolumes := []core.DataVolume{{
    -					Name: "test-disk",
    -				}}
    -				dataVolumeConfigs := []apisazure.DataVolume{{
    -					Name:     "test-disk",
    -					ImageRef: &apisazure.Image{},
    -				}}
    -
    -				Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    -					PointTo(MatchFields(IgnoreExtras, Fields{
    -						"Type":  Equal(field.ErrorTypeInvalid),
    -						"Field": Equal("config.dataVolumes.ImageRef"),
    -					})),
    -				))
    -			})
    +		It("should return error when resource value is negative", func() {
    +			nodeTemplate := &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{
    +					corev1.ResourceCPU:    resource.MustParse("1"),
    +					corev1.ResourceMemory: resource.MustParse("-50Gi"),
    +					"gpu":                 resource.MustParse("0"),
    +				},
    +			}
    +
    +			Expect(validateNodeTemplate(nodeTemplate, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Field": Equal("config.capacity.memory"),
    +				})),
    +			))
     		})
    +
     	})
     
    +	Describe("DataVolumes", func() {
    +		It("should allow valid DataVolumes config", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "test-disk",
    +				ImageRef: &apisazure.Image{
    +					URN: ptr.To("sap:gardenlinux:greatest:1312.0.0"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(BeEmpty())
    +		})
    +
    +		It("should forbid config of none existing DataVolume", func() {
    +			var dataVolumes []core.DataVolume
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "does not exist",
    +				ImageRef: &apisazure.Image{
    +					URN: ptr.To("sap:gardenlinux:greatest:1312.0.0"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.dataVolumes[0].name"),
    +					"Detail": Equal("no dataVolume with this name exists"),
    +				})),
    +			))
    +		})
    +
    +		It("should forbid empty DataVolume ImageRef", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name:     "test-disk",
    +				ImageRef: &apisazure.Image{},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("config.dataVolumes[0].imageRef"),
    +					"Detail": Equal("imageRef is defined but empty"),
    +				})),
    +			))
    +		})
    +
    +		It("should forbid invalid DataVolume ImageRef URN", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "test-disk",
    +				ImageRef: &apisazure.Image{
    +					URN: ptr.To("invalid-urn"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Field": Equal("config.dataVolumes[0].imageRef.urn"),
    +				})),
    +			))
    +		})
    +
    +		It("should allow DataVolume ImageRef URN", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "test-disk",
    +				ImageRef: &apisazure.Image{
    +					URN: ptr.To("sap:gardenlinux:greatest:1312.0.0"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(BeEmpty())
    +		})
    +
    +		It("should forbid invalid DataVolume ImageRef with Community Gallery", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "test-disk",
    +				ImageRef: &apisazure.Image{
    +					CommunityGalleryImageID: ptr.To("invalid-gallery-image-id"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":  Equal(field.ErrorTypeInvalid),
    +					"Field": Equal("config.dataVolumes[0].imageRef.communityGalleryImageID"),
    +				})),
    +			))
    +		})
    +
    +		It("should allow DataVolume ImageRef with Community Gallery", func() {
    +			dataVolumes := []core.DataVolume{{
    +				Name: "test-disk",
    +			}}
    +			dataVolumeConfigs := []apisazure.DataVolume{{
    +				Name: "test-disk",
    +				ImageRef: &apisazure.Image{
    +					CommunityGalleryImageID: ptr.To("/CommunityGalleries/gardenlinux-13e998fe-534d-4b0a-8a27-f16a73aef620/Images/gardenlinux/Versions/1443.15.0"),
    +				},
    +			}}
    +
    +			Expect(validateDataVolumeConf(dataVolumeConfigs, dataVolumes, fldPath)).To(BeEmpty())
    +		})
    +	})
     })
    
51111b4f60c3

Shoot user input validation (#1173)

https://github.com/gardener/gardener-extension-provider-gcpKonstantinos AngelopoulosSep 17, 2025via ghsa
6 files changed · +805 542
  • pkg/admission/validator/shoot.go+4 2 modified
    @@ -132,8 +132,10 @@ func (s *shoot) validateContext(valContext *validationContext) field.ErrorList {
     		workerConfig, err := admission.DecodeWorkerConfig(s.decoder, worker.ProviderConfig)
     		if err != nil {
     			allErrors = append(allErrors, field.Invalid(workerFldPath.Child("providerConfig"), err, "invalid providerConfig"))
    -		} else {
    -			allErrors = append(allErrors, gcpvalidation.ValidateWorkerConfig(workerConfig, worker.DataVolumes)...)
    +			continue
    +		}
    +		if workerConfig != nil {
    +			allErrors = append(allErrors, gcpvalidation.ValidateWorkerConfig(*workerConfig, worker.DataVolumes, providerPath.Child("providerConfig"))...)
     		}
     	}
     
    
  • pkg/apis/gcp/validation/filter.go+88 0 added
    @@ -0,0 +1,88 @@
    +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
    +//
    +// SPDX-License-Identifier: Apache-2.0
    +
    +package validation
    +
    +import (
    +	"fmt"
    +	"regexp"
    +	"unicode/utf8"
    +
    +	"k8s.io/apimachinery/pkg/util/validation/field"
    +)
    +
    +var (
    +	// Rfc1035Regex matches strings that comply with RFC1035.
    +	Rfc1035Regex = `^[a-z]([-a-z0-9]*[a-z0-9])?$`
    +	// GpuAcceleratorTypeRegex matches e.g. nvidia-tesla-p100
    +	GpuAcceleratorTypeRegex = `^[a-z0-9\-]+$`
    +	// MinCPUsPlatformRegex matches e.g. "Intel Haswell" or "Intel Sandy Bridge"
    +	MinCPUsPlatformRegex = `^[A-Za-z0-9\s]+$`
    +	// VolumeSourceImageRegex matches e.g. family/debian-9 or projects/debian-cloud/global/images/family/debian-9
    +	VolumeSourceImageRegex = `^[a-z0-9\-/]+$`
    +	// VolumeKmsKeyNameRegex matches e.g. projects/projectId/locations/<zoneName>/keyRings/<keyRingName>/cryptoKeys/alpha
    +	VolumeKmsKeyNameRegex = `^[a-zA-Z0-9\-_/]+$`
    +	// ServiceAccountRegex matches e.g. user@projectId.iam.gserviceaccount.com
    +	ServiceAccountRegex = `^[a-zA-Z0-9\-_]+@[a-z0-9\-]+\.iam\.gserviceaccount\.com$`
    +	// ServiceAccountScopeRegex matches e.g.https://www.googleapis.com/auth/cloud-platform
    +	ServiceAccountScopeRegex = `^https:\/\/www\.googleapis\.com\/[a-z0-9\-\.\/_]+$`
    +
    +	validateGcpResourceName            = combineValidationFuncs(regex(Rfc1035Regex), minLength(1), maxLength(63))
    +	validateGpuAcceleratorType         = combineValidationFuncs(regex(GpuAcceleratorTypeRegex), minLength(1), maxLength(250))
    +	validateMinCPUsPlatform            = combineValidationFuncs(regex(MinCPUsPlatformRegex), minLength(1), maxLength(250))
    +	validateVolumeSourceImage          = combineValidationFuncs(regex(VolumeSourceImageRegex), minLength(1), maxLength(250))
    +	validateVolumeKmsKeyName           = combineValidationFuncs(regex(VolumeKmsKeyNameRegex), minLength(1), maxLength(250))
    +	validateVolumeKmsKeyServiceAccount = combineValidationFuncs(regex(ServiceAccountRegex), minLength(1), maxLength(250))
    +	validateServiceAccountEmail        = combineValidationFuncs(regex(ServiceAccountRegex), minLength(1), maxLength(250))
    +	validateServiceAccountScopeName    = combineValidationFuncs(regex(ServiceAccountScopeRegex), minLength(1), maxLength(250))
    +)
    +
    +type validateFunc[T any] func(T, *field.Path) field.ErrorList
    +
    +// combineValidationFuncs validates a value against a list of filters.
    +func combineValidationFuncs[T any](filters ...validateFunc[T]) validateFunc[T] {
    +	return func(t T, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		for _, f := range filters {
    +			allErrs = append(allErrs, f(t, fld)...)
    +		}
    +		return allErrs
    +	}
    +}
    +
    +// regex returns a filterFunc that validates a string against a regular expression.
    +func regex(regex string) validateFunc[string] {
    +	compiled := regexp.MustCompile(regex)
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if name == "" {
    +			return allErrs // Allow empty strings to pass through
    +		}
    +		if !compiled.MatchString(name) {
    +			allErrs = append(allErrs, field.Invalid(fld, name, fmt.Sprintf("does not match expected regex %s", compiled.String())))
    +		}
    +		return allErrs
    +	}
    +}
    +
    +// nolint:unparam
    +func minLength(min int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if utf8.RuneCountInString(name) < min {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be fewer than %d characters, got %d", min, len(name)))}
    +		}
    +		return allErrs
    +	}
    +}
    +
    +func maxLength(max int) validateFunc[string] {
    +	return func(name string, fld *field.Path) field.ErrorList {
    +		var allErrs field.ErrorList
    +		if utf8.RuneCountInString(name) > max {
    +			return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be more than %d characters, got %d", max, len(name)))}
    +		}
    +		return allErrs
    +	}
    +}
    
  • pkg/apis/gcp/validation/infrastructure.go+56 43 modified
    @@ -5,7 +5,7 @@
     package validation
     
     import (
    -	"reflect"
    +	"slices"
     
     	cidrvalidation "github.com/gardener/gardener/pkg/utils/validation/cidr"
     	apivalidation "k8s.io/apimachinery/pkg/api/validation"
    @@ -14,16 +14,31 @@ import (
     	apisgcp "github.com/gardener/gardener-extension-provider-gcp/pkg/apis/gcp"
     )
     
    +var (
    +	// validSubnetLogConfigIntervals contain the valid SubnetworkLogConfig AggregationIntervals
    +	validSubnetLogConfigIntervals = []string{
    +		"INTERVAL_5_SEC",
    +		"INTERVAL_30_SEC",
    +		"INTERVAL_1_MIN",
    +		"INTERVAL_5_MIN",
    +		"INTERVAL_10_MIN",
    +		"INTERVAL_15_MIN",
    +	}
    +	validSubnetLogConfigMetadata = []string{
    +		"CUSTOM_METADATA",
    +		"EXCLUDE_ALL_METADATA",
    +		"INCLUDE_ALL_METADATA",
    +	}
    +)
    +
     // ValidateInfrastructureConfig validates a InfrastructureConfig object.
     func ValidateInfrastructureConfig(infra *apisgcp.InfrastructureConfig, nodesCIDR, podsCIDR, servicesCIDR *string, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
     	var (
    -		nodes                    cidrvalidation.CIDR
    -		pods                     cidrvalidation.CIDR
    -		services                 cidrvalidation.CIDR
    -		aggregationIntervalArray = []string{"INTERVAL_5_SEC", "INTERVAL_30_SEC", "INTERVAL_1_MIN", "INTERVAL_5_MIN", "INTERVAL_15_MIN"}
    -		metadata                 = []string{"INCLUDE_ALL_METADATA"}
    +		nodes    cidrvalidation.CIDR
    +		pods     cidrvalidation.CIDR
    +		services cidrvalidation.CIDR
     	)
     
     	networkingPath := field.NewPath("networking")
    @@ -83,36 +98,23 @@ func ValidateInfrastructureConfig(infra *apisgcp.InfrastructureConfig, nodesCIDR
     	}
     
     	if infra.Networks.VPC != nil && len(infra.Networks.VPC.Name) > 0 {
    +		allErrs = append(allErrs, validateGcpResourceName(infra.Networks.VPC.Name, networksPath.Child("vpc", "name"))...)
    +
     		if infra.Networks.VPC.CloudRouter == nil {
     			allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc", "cloudRouter"), infra.Networks.VPC.CloudRouter, "cloud router must be defined when reusing a VPC"))
     		}
     
    -		if infra.Networks.VPC.CloudRouter != nil && len(infra.Networks.VPC.CloudRouter.Name) == 0 {
    -			allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc", "cloudRouter", "name"), infra.Networks.VPC.CloudRouter, "cloud router name must be specified when reusing a VPC"))
    +		if infra.Networks.VPC.CloudRouter != nil {
    +			if len(infra.Networks.VPC.CloudRouter.Name) == 0 {
    +				allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc", "cloudRouter", "name"), infra.Networks.VPC.CloudRouter, "cloud router name must be specified when reusing a VPC"))
    +			} else {
    +				allErrs = append(allErrs, validateGcpResourceName(infra.Networks.VPC.CloudRouter.Name, networksPath.Child("vpc", "cloudRouter", "name"))...)
    +			}
     		}
     	}
     
     	if infra.Networks.FlowLogs != nil {
    -		if infra.Networks.FlowLogs.AggregationInterval == nil && infra.Networks.FlowLogs.FlowSampling == nil && infra.Networks.FlowLogs.Metadata == nil {
    -			allErrs = append(allErrs, field.Required(networksPath.Child("flowLogs"), "at least one VPC flow log parameter must be specified when VPC flow log section is provided"))
    -		}
    -		if infra.Networks.FlowLogs.AggregationInterval != nil {
    -			validValue := findElement(aggregationIntervalArray, *infra.Networks.FlowLogs.AggregationInterval)
    -			if !validValue {
    -				allErrs = append(allErrs, field.NotSupported(networksPath.Child("flowLogs", "aggregationInterval"), infra.Networks.FlowLogs.AggregationInterval, aggregationIntervalArray))
    -			}
    -		}
    -		if infra.Networks.FlowLogs.Metadata != nil {
    -			validValue := findElement(metadata, *infra.Networks.FlowLogs.Metadata)
    -			if !validValue {
    -				allErrs = append(allErrs, field.NotSupported(networksPath.Child("flowLogs", "metadata"), infra.Networks.FlowLogs.Metadata, metadata))
    -			}
    -		}
    -		if infra.Networks.FlowLogs.FlowSampling != nil {
    -			if *infra.Networks.FlowLogs.FlowSampling < 0 || *infra.Networks.FlowLogs.FlowSampling > 1 {
    -				allErrs = append(allErrs, field.Invalid(networksPath.Child("flowLogs", "flowSampling"), infra.Networks.FlowLogs.FlowSampling, "must contain a valid value"))
    -			}
    -		}
    +		allErrs = append(allErrs, validateNetworkFlowLogs(*infra.Networks.FlowLogs, networksPath.Child("flowLogs"))...)
     	}
     
     	if infra.Networks.CloudNAT != nil {
    @@ -122,6 +124,28 @@ func ValidateInfrastructureConfig(infra *apisgcp.InfrastructureConfig, nodesCIDR
     	return allErrs
     }
     
    +func validateNetworkFlowLogs(flowLogs apisgcp.FlowLogs, fldPath *field.Path) field.ErrorList {
    +	allErrs := field.ErrorList{}
    +
    +	if flowLogs.AggregationInterval == nil && flowLogs.FlowSampling == nil && flowLogs.Metadata == nil {
    +		allErrs = append(allErrs, field.Required(fldPath, "at least one VPC flow log parameter must be specified when VPC flow log section is provided"))
    +	}
    +
    +	if flowLogs.AggregationInterval != nil && !slices.Contains(validSubnetLogConfigIntervals, *flowLogs.AggregationInterval) {
    +		allErrs = append(allErrs, field.NotSupported(fldPath.Child("aggregationInterval"), *flowLogs.AggregationInterval, validSubnetLogConfigIntervals))
    +	}
    +
    +	if flowLogs.Metadata != nil && !slices.Contains(validSubnetLogConfigMetadata, *flowLogs.Metadata) {
    +		allErrs = append(allErrs, field.NotSupported(fldPath.Child("metadata"), flowLogs.Metadata, validSubnetLogConfigMetadata))
    +	}
    +
    +	if flowLogs.FlowSampling != nil && (*flowLogs.FlowSampling < 0 || *flowLogs.FlowSampling > 1) {
    +		allErrs = append(allErrs, field.Invalid(fldPath.Child("flowSampling"), flowLogs.FlowSampling, "must be between 0 and 1"))
    +	}
    +
    +	return allErrs
    +}
    +
     // ValidateCloudNatConfig validates the config of the CloudNat. We intentionally keep the validation light, only
     // checking for gotchas (e.g. the port counts having to be powers of two) and obvious errors.
     func ValidateCloudNatConfig(config *apisgcp.CloudNAT, fldPath *field.Path) field.ErrorList {
    @@ -132,6 +156,10 @@ func ValidateCloudNatConfig(config *apisgcp.CloudNAT, fldPath *field.Path) field
     		allErrs = append(allErrs, field.Invalid(cloudNatPath.Child("natIPNames"), config.NatIPNames, "nat IP names cannot be empty."))
     	}
     
    +	for idx, natIPName := range config.NatIPNames {
    +		allErrs = append(allErrs, validateGcpResourceName(natIPName.Name, cloudNatPath.Child("natIPNames").Index(idx).Child("name"))...)
    +	}
    +
     	if config.EnableDynamicPortAllocation {
     		if config.EndpointIndependentMapping != nil && config.EndpointIndependentMapping.Enabled {
     			// There is no more fitting field.Error (e.g. field.MutuallyExclusive) so we put the blame on 'enableDynamicPortAllocation' and use the error msg
    @@ -206,18 +234,3 @@ func ValidateInfrastructureConfigUpdate(oldConfig, newConfig *apisgcp.Infrastruc
     
     	return allErrs
     }
    -
    -// FindElement takes a slice and an item and tries to find the item in the slice.
    -// if item is found, true is returned.
    -func findElement(slice interface{}, item interface{}) bool {
    -	s := reflect.ValueOf(slice)
    -	if s.Kind() != reflect.Slice {
    -		panic("Invalid data type")
    -	}
    -	for i := 0; i < s.Len(); i++ {
    -		if s.Index(i).Interface() == item {
    -			return true
    -		}
    -	}
    -	return false
    -}
    
  • pkg/apis/gcp/validation/infrastructure_test.go+117 78 modified
    @@ -5,6 +5,8 @@
     package validation_test
     
     import (
    +	"fmt"
    +
     	. "github.com/gardener/gardener/pkg/utils/test/matchers"
     	. "github.com/onsi/ginkgo/v2"
     	. "github.com/onsi/gomega"
    @@ -60,8 +62,8 @@ var _ = Describe("InfrastructureConfig validation", func() {
     		Context("CIDR", func() {
     			It("should forbid invalid worker CIDRs", func() {
     				infrastructureConfig.Networks.Workers = invalidCIDR
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.workers"),
    @@ -72,7 +74,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			It("should forbid invalid internal CIDR", func() {
     				invalidCIDR = "invalid-cidr"
     				infrastructureConfig.Networks.Internal = &invalidCIDR
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
     
     				Expect(errorList).To(ConsistOfFields(Fields{
    @@ -84,7 +85,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     
     			It("should forbid workers CIDR which are not in Nodes CIDR", func() {
     				infrastructureConfig.Networks.Workers = "1.1.1.1/32"
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
     
     				Expect(errorList).To(ConsistOfFields(Fields{
    @@ -98,7 +98,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     				overlappingCIDR := "10.250.1.0/30"
     				infrastructureConfig.Networks.Internal = &overlappingCIDR
     				infrastructureConfig.Networks.Workers = overlappingCIDR
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &overlappingCIDR, &pods, &services, fldPath)
     
     				Expect(errorList).To(ConsistOfFields(Fields{
    @@ -119,7 +118,6 @@ var _ = Describe("InfrastructureConfig validation", func() {
     				internal := "10.10.0.4/24"
     				infrastructureConfig.Networks.Internal = &internal
     				infrastructureConfig.Networks.Workers = "10.250.3.8/24"
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodeCIDR, &podCIDR, &serviceCIDR, fldPath)
     
     				Expect(errorList).To(HaveLen(2))
    @@ -151,11 +149,12 @@ var _ = Describe("InfrastructureConfig validation", func() {
     					Workers:  "10.250.0.0/16",
     				},
     			}
    +
     			It("should forbid configuring CloudRouter if VPC name is not set", func() {
     				testInfrastructureConfig.Networks.VPC = &apisgcp.VPC{}
     				testInfrastructureConfig.Networks.VPC.CloudRouter = &apisgcp.CloudRouter{}
    -
     				errorList := ValidateInfrastructureConfig(testInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.vpc.cloudRouter"),
    @@ -166,81 +165,65 @@ var _ = Describe("InfrastructureConfig validation", func() {
     					"Detail": Equal("vpc name must not be empty when vpc key is provided"),
     				}))
     			})
    +
     			It("should forbid empty VPC flow log config", func() {
     				infrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{}
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeRequired),
     					"Field":  Equal("networks.flowLogs"),
     					"Detail": Equal("at least one VPC flow log parameter must be specified when VPC flow log section is provided"),
     				}))
     			})
    -			It("should forbid wrong VPC flow log config", func() {
    -				aggregationInterval := "foo"
    -				flowSampling := float64(1.2)
    -				metadata := "foo"
    -				infrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{AggregationInterval: &aggregationInterval, FlowSampling: &flowSampling, Metadata: &metadata}
     
    -				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    -				Expect(errorList).To(ConsistOfFields(Fields{
    -					"Type":   Equal(field.ErrorTypeNotSupported),
    -					"Field":  Equal("networks.flowLogs.aggregationInterval"),
    -					"Detail": Equal("supported values: \"INTERVAL_5_SEC\", \"INTERVAL_30_SEC\", \"INTERVAL_1_MIN\", \"INTERVAL_5_MIN\", \"INTERVAL_15_MIN\""),
    -				}, Fields{
    -					"Type":   Equal(field.ErrorTypeNotSupported),
    -					"Field":  Equal("networks.flowLogs.metadata"),
    -					"Detail": Equal("supported values: \"INCLUDE_ALL_METADATA\""),
    -				}, Fields{
    -					"Type":   Equal(field.ErrorTypeInvalid),
    -					"Field":  Equal("networks.flowLogs.flowSampling"),
    -					"Detail": Equal("must contain a valid value"),
    -				}))
    -			})
     			It("should forbid reusing a VPC without specifying a CloudRouter", func() {
     				testInfrastructureConfig.Networks.VPC = &apisgcp.VPC{
     					Name: "test-vpc",
     				}
    -
     				errorList := ValidateInfrastructureConfig(testInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.vpc.cloudRouter"),
     					"Detail": Equal("cloud router must be defined when reusing a VPC"),
     				}))
     			})
    +
     			It("should forbid reusing a VPC without specifying a CloudRouter name", func() {
     				testInfrastructureConfig.Networks.VPC = &apisgcp.VPC{
     					Name:        "test-vpc",
     					CloudRouter: &apisgcp.CloudRouter{},
     				}
    -
     				errorList := ValidateInfrastructureConfig(testInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.vpc.cloudRouter.name"),
     					"Detail": Equal("cloud router name must be specified when reusing a VPC"),
     				}))
     			})
    +
     			It("should forbid reusing a VPC without specifying a CloudRouter", func() {
     				testInfrastructureConfig.Networks.VPC = &apisgcp.VPC{
     					Name: "test-vpc",
     				}
    -
     				errorList := ValidateInfrastructureConfig(testInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.vpc.cloudRouter"),
     					"Detail": Equal("cloud router must be defined when reusing a VPC"),
     				}))
     			})
    +
     			It("should forbid reusing a VPC without specifying a CloudRouter name", func() {
     				testInfrastructureConfig.Networks.VPC = &apisgcp.VPC{
     					Name:        "test-vpc",
     					CloudRouter: &apisgcp.CloudRouter{},
     				}
    -
     				errorList := ValidateInfrastructureConfig(testInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.vpc.cloudRouter.name"),
    @@ -250,45 +233,42 @@ var _ = Describe("InfrastructureConfig validation", func() {
     
     			It("should forbid empty VPC flow log config", func() {
     				infrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{}
    -
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeRequired),
     					"Field":  Equal("networks.flowLogs"),
     					"Detail": Equal("at least one VPC flow log parameter must be specified when VPC flow log section is provided"),
     				}))
     			})
    -			It("should forbid wrong VPC flow log config", func() {
    -				aggregationInterval := "foo"
    -				flowSampling := float64(1.2)
    -				metadata := "foo"
    -				infrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{AggregationInterval: &aggregationInterval, FlowSampling: &flowSampling, Metadata: &metadata}
     
    +			It("should forbid invalid VPC name", func() {
    +				infrastructureConfig.Networks.VPC.Name = "invalid-VPC-name"
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    -				Expect(errorList).To(ConsistOfFields(Fields{
    -					"Type":   Equal(field.ErrorTypeNotSupported),
    -					"Field":  Equal("networks.flowLogs.aggregationInterval"),
    -					"Detail": Equal("supported values: \"INTERVAL_5_SEC\", \"INTERVAL_30_SEC\", \"INTERVAL_1_MIN\", \"INTERVAL_5_MIN\", \"INTERVAL_15_MIN\""),
    -				}, Fields{
    -					"Type":   Equal(field.ErrorTypeNotSupported),
    -					"Field":  Equal("networks.flowLogs.metadata"),
    -					"Detail": Equal("supported values: \"INCLUDE_ALL_METADATA\""),
    -				}, Fields{
    -					"Type":   Equal(field.ErrorTypeInvalid),
    -					"Field":  Equal("networks.flowLogs.flowSampling"),
    -					"Detail": Equal("must contain a valid value"),
    -				}))
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.vpc.name"),
    +						"Detail": Equal(fmt.Sprintf("does not match expected regex %s", Rfc1035Regex)),
    +					})),
    +				))
     			})
    -			It("should allow correct VPC flow log config", func() {
    -				aggregationInterval := "INTERVAL_1_MIN"
    -				flowSampling := float64(0.5)
    -				metadata := "INCLUDE_ALL_METADATA"
    -				infrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{AggregationInterval: &aggregationInterval, FlowSampling: &flowSampling, Metadata: &metadata}
     
    +			It("should forbid invalid VPC cloud router name", func() {
    +				infrastructureConfig.Networks.VPC.CloudRouter.Name = "invalid-CLOUD-ROUTER-name"
     				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    -				Expect(errorList).To(BeEmpty())
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.vpc.cloudRouter.name"),
    +						"Detail": Equal(fmt.Sprintf("does not match expected regex %s", Rfc1035Regex)),
    +					})),
    +				))
     			})
     		})
    +
     		Context("CloudNAT and Flowlogs", func() {
     			It("should allow correct flowlogs config", func() {
     				newInfrastructureConfig := infrastructureConfig.DeepCopy()
    @@ -297,41 +277,94 @@ var _ = Describe("InfrastructureConfig validation", func() {
     					Metadata:            ptr.To("INCLUDE_ALL_METADATA"),
     					FlowSampling:        ptr.To[float64](0.5),
     				}
    -
     				errorList := ValidateInfrastructureConfig(newInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(BeEmpty())
     			})
    +
     			It("should allow CloudNAT config with valid NatIPNames", func() {
     				newInfrastructureConfig := infrastructureConfig.DeepCopy()
     				newInfrastructureConfig.Networks.CloudNAT = &apisgcp.CloudNAT{
     					NatIPNames: []apisgcp.NatIPName{
     						{Name: "test"},
     					},
     				}
    -
     				errorList := ValidateInfrastructureConfig(newInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(BeEmpty())
     			})
    +
     			It("should allow CloudNAT config without NatIPNames present", func() {
     				newInfrastructureConfig := infrastructureConfig.DeepCopy()
     				newInfrastructureConfig.Networks.CloudNAT = &apisgcp.CloudNAT{}
    -
     				errorList := ValidateInfrastructureConfig(newInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(BeEmpty())
     			})
    +
     			It("should forbid empty array for NAT IP names when CloudNAT is present", func() {
     				newInfrastructureConfig := infrastructureConfig.DeepCopy()
     				newInfrastructureConfig.Networks.CloudNAT = &apisgcp.CloudNAT{
     					NatIPNames: []apisgcp.NatIPName{},
     				}
    -
     				errorList := ValidateInfrastructureConfig(newInfrastructureConfig, &nodes, &pods, &services, fldPath)
    +
     				Expect(errorList).To(ConsistOfFields(Fields{
     					"Type":   Equal(field.ErrorTypeInvalid),
     					"Field":  Equal("networks.cloudNAT.natIPNames"),
     					"Detail": Equal("nat IP names cannot be empty."),
     				}))
     			})
    +
    +			It("should forbid invalid cloud nat IP name", func() {
    +				infrastructureConfig.Networks.CloudNAT.NatIPNames[0].Name = "invalid-CLOUD-NAT-ip-name"
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.cloudNAT.natIPNames[0].name"),
    +						"Detail": Equal(fmt.Sprintf("does not match expected regex %s", Rfc1035Regex)),
    +					})),
    +				))
    +			})
    +
    +			It("should forbid invalid flow logs aggregation interval", func() {
    +				infrastructureConfig.Networks.FlowLogs.AggregationInterval = ptr.To("INTERVAL_25_SEC")
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":  Equal(field.ErrorTypeNotSupported),
    +						"Field": Equal("networks.flowLogs.aggregationInterval"),
    +					})),
    +				))
    +			})
    +
    +			It("should forbid invalid flow logs sampling rate", func() {
    +				infrastructureConfig.Networks.FlowLogs.FlowSampling = ptr.To(1.2)
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":   Equal(field.ErrorTypeInvalid),
    +						"Field":  Equal("networks.flowLogs.flowSampling"),
    +						"Detail": Equal("must be between 0 and 1"),
    +					})),
    +				))
    +			})
    +
    +			It("should forbid invalid flow logs metadata", func() {
    +				infrastructureConfig.Networks.FlowLogs.Metadata = ptr.To("foo")
    +				errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services, fldPath)
    +
    +				Expect(errorList).To(ConsistOf(
    +					PointTo(MatchFields(IgnoreExtras, Fields{
    +						"Type":  Equal(field.ErrorTypeNotSupported),
    +						"Field": Equal("networks.flowLogs.metadata"),
    +					})),
    +				))
    +			})
     		})
     	})
     
    @@ -351,8 +384,8 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			newInfrastructureConfig.Networks.FlowLogs = &apisgcp.FlowLogs{
     				AggregationInterval: ptr.To("INTERVAL_30_SEC"),
     			}
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(BeEmpty())
     		})
     
    @@ -367,20 +400,24 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			newInfrastructureConfig.Networks.Workers = "10.96.0.0/16"
     			newInfrastructureConfig.Networks.Worker = "10.96.0.0/16"
     			newInfrastructureConfig.Networks.Internal = ptr.To("10.96.0.0/16")
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(ConsistOfFields(Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.vpc.name"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.vpc.name"),
    +				"Detail": Equal("field is immutable"),
     			}, Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.vpc.cloudRouter"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.vpc.cloudRouter"),
    +				"Detail": Equal("field is immutable"),
     			}, Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.internal"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.internal"),
    +				"Detail": Equal("field is immutable"),
     			}, Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.workers"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.workers"),
    +				"Detail": Equal("worker CIDR blocks can only be expanded"),
     			}))
     		})
     
    @@ -389,38 +426,40 @@ var _ = Describe("InfrastructureConfig validation", func() {
     			// delete the internal subnet to simulate it was not set before
     			infrastructureConfig.Networks.Internal = nil
     			newInfrastructureConfig.Networks.Internal = ptr.To("10.96.0.0/16")
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(BeEmpty())
     		})
     
     		It("should forbid updating VPC value to nil", func() {
     			newInfrastructureConfig := infrastructureConfig.DeepCopy()
     			newInfrastructureConfig.Networks.VPC = nil
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(ConsistOfFields(Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.vpc"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.vpc"),
    +				"Detail": Equal("field is immutable"),
     			}))
     		})
     
     		It("should allow expanding the worker subnet", func() {
     			newInfrastructureConfig := infrastructureConfig.DeepCopy()
     			newInfrastructureConfig.Networks.Workers = "10.250.0.0/15"
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(BeEmpty())
     		})
     
     		It("should forbid shrinking the worker subnet", func() {
     			newInfrastructureConfig := infrastructureConfig.DeepCopy()
     			newInfrastructureConfig.Networks.Workers = "10.250.0.0/17"
    -
     			errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, fldPath)
    +
     			Expect(errorList).To(ConsistOfFields(Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("networks.workers"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("networks.workers"),
    +				"Detail": Equal("worker CIDR blocks can only be expanded"),
     			}))
     		})
     	})
    
  • pkg/apis/gcp/validation/worker.go+77 77 modified
    @@ -22,31 +22,35 @@ import (
     
     var (
     	validVolumeLocalSSDInterfacesTypes = sets.New("NVME", "SCSI")
    -
    -	providerFldPath   = field.NewPath("providerConfig")
    -	volumeFldPath     = providerFldPath.Child("volume")
    -	dataVolumeFldPath = providerFldPath.Child("dataVolume")
     )
     
     // ValidateWorkerConfig validates a WorkerConfig object.
    -func ValidateWorkerConfig(workerConfig *gcp.WorkerConfig, dataVolumes []core.DataVolume) field.ErrorList {
    +func ValidateWorkerConfig(workerConfig gcp.WorkerConfig, dataVolumes []core.DataVolume, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	for i, dataVolume := range dataVolumes {
    -		dataVolumeFldPath := field.NewPath("dataVolumes").Index(i)
    -		allErrs = append(allErrs, validateDataVolume(workerConfig, dataVolume, dataVolumeFldPath)...)
    +	for idx, dataVolume := range dataVolumes {
    +		if dataVolume.Type == nil {
    +			allErrs = append(allErrs, field.Required(field.NewPath("dataVolumes").Index(idx).Child("type"), "must not be empty"))
    +			continue
    +		}
    +
    +		allErrs = append(allErrs, validateScratchDisk(*dataVolume.Type, workerConfig, fldPath.Child("volume"))...)
     	}
     
    -	if workerConfig != nil {
    -		allErrs = append(allErrs, validateGPU(workerConfig.GPU, providerFldPath.Child("gpu"))...)
    -		allErrs = append(allErrs, validateServiceAccount(workerConfig.ServiceAccount, providerFldPath.Child("serviceAccount"))...)
    -		if workerConfig.Volume != nil {
    -			allErrs = append(allErrs, validateDiskEncryption(workerConfig.Volume.Encryption, volumeFldPath.Child("encryption"))...)
    -		}
    -		allErrs = append(allErrs, validateNodeTemplate(workerConfig.NodeTemplate, providerFldPath.Child("nodeTemplate"))...)
    -		if workerConfig.DataVolumes != nil {
    -			allErrs = append(allErrs, validateDataVolumeConfigs(dataVolumes, workerConfig.DataVolumes)...)
    -		}
    +	allErrs = append(allErrs, validateGPU(workerConfig.GPU, fldPath.Child("gpu"))...)
    +	if workerConfig.ServiceAccount != nil {
    +		allErrs = append(allErrs, validateServiceAccount(*workerConfig.ServiceAccount, fldPath.Child("serviceAccount"))...)
    +	}
    +	if workerConfig.Volume != nil {
    +		allErrs = append(allErrs, validateVolumeConfig(*workerConfig.Volume, fldPath.Child("volume"))...)
    +	}
    +	allErrs = append(allErrs, validateNodeTemplate(workerConfig.NodeTemplate, fldPath.Child("nodeTemplate"))...)
    +	if workerConfig.DataVolumes != nil {
    +		allErrs = append(allErrs, validateDataVolumeConfigs(dataVolumes, workerConfig.DataVolumes, fldPath.Child("dataVolumes"))...)
    +	}
    +
    +	if workerConfig.MinCpuPlatform != nil {
    +		allErrs = append(allErrs, validateMinCPUsPlatform(*workerConfig.MinCpuPlatform, fldPath.Child("minCpuPlatform"))...)
     	}
     
     	return allErrs
    @@ -59,9 +63,7 @@ func validateGPU(gpu *gcp.GPU, fldPath *field.Path) field.ErrorList {
     		return allErrs
     	}
     
    -	if gpu.AcceleratorType == "" {
    -		allErrs = append(allErrs, field.Required(fldPath.Child("acceleratorType"), "must be set when providing gpu"))
    -	}
    +	allErrs = append(allErrs, validateGpuAcceleratorType(gpu.AcceleratorType, fldPath.Child("acceleratorType"))...)
     
     	if gpu.Count <= 0 {
     		allErrs = append(allErrs, field.Forbidden(fldPath.Child("count"), "must be > 0 when providing gpu"))
    @@ -70,28 +72,24 @@ func validateGPU(gpu *gcp.GPU, fldPath *field.Path) field.ErrorList {
     	return allErrs
     }
     
    -func validateServiceAccount(sa *gcp.ServiceAccount, fldPath *field.Path) field.ErrorList {
    +func validateServiceAccount(sa gcp.ServiceAccount, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	if sa == nil {
    -		return allErrs
    -	}
    -
    -	if sa.Email == "" {
    -		allErrs = append(allErrs, field.Required(fldPath.Child("email"), "must be set when providing service account"))
    -	}
    +	allErrs = append(allErrs, validateServiceAccountEmail(sa.Email, fldPath.Child("email"))...)
     
     	if len(sa.Scopes) == 0 {
     		allErrs = append(allErrs, field.Required(fldPath.Child("scopes"), "must have at least one scope"))
     	} else {
     		existingScopes := sets.NewString()
     
     		for i, scope := range sa.Scopes {
    +			idxPath := fldPath.Child("scopes").Index(i)
    +
    +			allErrs = append(allErrs, validateServiceAccountScopeName(scope, idxPath)...)
    +
     			switch {
    -			case scope == "":
    -				allErrs = append(allErrs, field.Required(fldPath.Child("scopes").Index(i), "must not be empty"))
     			case existingScopes.Has(scope):
    -				allErrs = append(allErrs, field.Duplicate(fldPath.Child("scopes").Index(i), scope))
    +				allErrs = append(allErrs, field.Duplicate(idxPath, scope))
     			default:
     				existingScopes.Insert(scope)
     			}
    @@ -101,104 +99,106 @@ func validateServiceAccount(sa *gcp.ServiceAccount, fldPath *field.Path) field.E
     	return allErrs
     }
     
    -// validateDiskEncryption validates the provider specific disk encryption configuration for a volume
    -func validateDiskEncryption(encryption *gcp.DiskEncryption, fldPath *field.Path) field.ErrorList {
    +func validateVolumeConfig(volume gcp.Volume, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	if encryption == nil {
    -		return allErrs
    +	if volume.LocalSSDInterface != nil {
    +		if err := validVolumeLocalSSDInterfacesTypes.Has(*volume.LocalSSDInterface); !err {
    +			allErrs = append(allErrs, field.NotSupported(fldPath.Child("interface"),
    +				*volume.LocalSSDInterface, validVolumeLocalSSDInterfacesTypes.UnsortedList()))
    +		}
     	}
     
    -	if encryption.KmsKeyName == nil || strings.TrimSpace(*encryption.KmsKeyName) == "" {
    -		// Currently DiskEncryption only contains CMEK fields. Hence if not nil, then kmsKeyName is a must
    -		// Validation logic will need to be modified when CSEK fields are possibly added to gcp.DiskEncryption in the future.
    -		allErrs = append(allErrs, field.Required(fldPath.Child("kmsKeyName"), "must be specified when configuring disk encryption"))
    +	if volume.Encryption != nil {
    +		allErrs = append(allErrs, validateDiskEncryption(*volume.Encryption, fldPath.Child("encryption"))...)
     	}
     
     	return allErrs
     }
     
    -func validateDataVolume(workerConfig *gcp.WorkerConfig, volume core.DataVolume, fldPath *field.Path) field.ErrorList {
    +// validateDiskEncryption validates the provider specific disk encryption configuration for a volume
    +func validateDiskEncryption(encryption gcp.DiskEncryption, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	if volume.Type == nil {
    -		allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty"))
    +	if encryption.KmsKeyName == nil || strings.TrimSpace(*encryption.KmsKeyName) == "" {
    +		// Currently DiskEncryption only contains CMEK fields. Hence if not nil, then kmsKeyName is a must
    +		// Validation logic will need to be modified when CSEK fields are possibly added to gcp.DiskEncryption in the future.
    +		allErrs = append(allErrs, field.Required(fldPath.Child("kmsKeyName"), "must be specified when configuring disk encryption"))
     		return allErrs
     	}
     
    -	allErrs = append(allErrs, validateScratchDisk(*volume.Type, workerConfig)...)
    +	allErrs = append(allErrs, validateVolumeKmsKeyName(*encryption.KmsKeyName, fldPath.Child("kmsKeyName"))...)
    +
    +	if encryption.KmsKeyServiceAccount != nil {
    +		allErrs = append(allErrs, validateVolumeKmsKeyServiceAccount(*encryption.KmsKeyServiceAccount, fldPath.Child("kmsKeyServiceAccount"))...)
    +	}
     
     	return allErrs
     }
     
    -func validateScratchDisk(volumeType string, workerConfig *gcp.WorkerConfig) field.ErrorList {
    +func validateScratchDisk(volumeType string, workerConfig gcp.WorkerConfig, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    -	interfacePath := volumeFldPath.Child("interface")
    -	encryptionPath := volumeFldPath.Child("encryption")
    +	interfacePath := fldPath.Child("interface")
    +	encryptionPath := fldPath.Child("encryption")
     
     	if volumeType == worker.VolumeTypeScratch {
    -		if workerConfig == nil || workerConfig.Volume == nil || workerConfig.Volume.LocalSSDInterface == nil {
    +		if workerConfig.Volume == nil || workerConfig.Volume.LocalSSDInterface == nil {
     			allErrs = append(allErrs, field.Required(interfacePath, fmt.Sprintf("must be set when using %s volumes", worker.VolumeTypeScratch)))
    -		} else {
    -			if !validVolumeLocalSSDInterfacesTypes.Has(*workerConfig.Volume.LocalSSDInterface) {
    -				allErrs = append(allErrs, field.NotSupported(interfacePath, *workerConfig.Volume.LocalSSDInterface, validVolumeLocalSSDInterfacesTypes.UnsortedList()))
    -			}
     		}
     		// DiskEncryption not allowed for type SCRATCH
    -		if workerConfig != nil && workerConfig.Volume != nil && workerConfig.Volume.Encryption != nil {
    +		if workerConfig.Volume != nil && workerConfig.Volume.Encryption != nil {
     			allErrs = append(allErrs, field.Invalid(encryptionPath, *workerConfig.Volume.Encryption, fmt.Sprintf("must not be set in combination with %s volumes", worker.VolumeTypeScratch)))
     		}
     	} else {
     		// LocalSSDInterface only allowed for type SCRATCH
    -		if workerConfig != nil && workerConfig.Volume != nil && workerConfig.Volume.LocalSSDInterface != nil {
    +		if workerConfig.Volume != nil && workerConfig.Volume.LocalSSDInterface != nil {
     			allErrs = append(allErrs, field.Invalid(interfacePath, *workerConfig.Volume.LocalSSDInterface, fmt.Sprintf("is only allowed for type %s", worker.VolumeTypeScratch)))
     		}
     	}
     	return allErrs
     }
     
    -func validateHyperDisk(dataVolume core.DataVolume, config gcp.DataVolume) field.ErrorList {
    -	allErrs := field.ErrorList{}
    -
    -	if config.ProvisionedIops != nil && !slices.Contains(worker.AllowedTypesIops, *dataVolume.Type) {
    -		allErrs = append(allErrs, field.Forbidden(
    -			dataVolumeFldPath.Child("provisionedIops"),
    -			fmt.Sprintf("is only allowed for types: %v", worker.AllowedTypesIops)))
    -	}
    -	if config.ProvisionedThroughput != nil && !slices.Contains(worker.AllowedTypesThroughput, *dataVolume.Type) {
    -		allErrs = append(allErrs, field.Forbidden(
    -			dataVolumeFldPath.Child("provisionedThroughput"),
    -			fmt.Sprintf("is only allowed for types: %v", worker.AllowedTypesThroughput)))
    -	}
    -	return allErrs
    -}
    -
    -func validateDataVolumeConfigs(dataVolumes []core.DataVolume, configs []gcp.DataVolume) field.ErrorList {
    +func validateDataVolumeConfigs(dataVolumes []core.DataVolume, configs []gcp.DataVolume, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
    -
     	volumeNames := sets.New[string]()
    +
     	for i, configDataVolume := range configs {
    +		if sourceImage := configDataVolume.SourceImage; sourceImage != nil {
    +			allErrs = append(allErrs, validateVolumeSourceImage(*sourceImage, fldPath.Index(i).Child("sourceImage"))...)
    +		}
     		idx := slices.IndexFunc(dataVolumes, func(dv core.DataVolume) bool { return dv.Name == configDataVolume.Name })
     		volumeName := configDataVolume.Name
     		if idx == -1 {
    -			allErrs = append(allErrs, field.Invalid(
    -				dataVolumeFldPath,
    -				volumeName,
    +			allErrs = append(allErrs, field.Invalid(fldPath, volumeName,
     				fmt.Sprintf("could not find dataVolume with name %s", volumeName)))
     			continue
     		}
     		if volumeNames.Has(volumeName) {
    -			allErrs = append(allErrs, field.Duplicate(dataVolumeFldPath.Index(i), volumeName))
    +			allErrs = append(allErrs, field.Duplicate(fldPath.Index(i), volumeName))
     			continue
     		}
     		volumeNames.Insert(volumeName)
    -		allErrs = append(allErrs, validateHyperDisk(dataVolumes[idx], configDataVolume)...)
    +		allErrs = append(allErrs, validateHyperDisk(dataVolumes[idx], configDataVolume, fldPath)...)
     	}
     
     	return allErrs
     }
     
    +func validateHyperDisk(dataVolume core.DataVolume, config gcp.DataVolume, fldPath *field.Path) field.ErrorList {
    +	allErrs := field.ErrorList{}
    +
    +	if config.ProvisionedIops != nil && !slices.Contains(worker.AllowedTypesIops, *dataVolume.Type) {
    +		allErrs = append(allErrs, field.Forbidden(fldPath.Child("provisionedIops"),
    +			fmt.Sprintf("is only allowed for types: %v", worker.AllowedTypesIops)))
    +	}
    +	if config.ProvisionedThroughput != nil && !slices.Contains(worker.AllowedTypesThroughput, *dataVolume.Type) {
    +		allErrs = append(allErrs, field.Forbidden(fldPath.Child("provisionedThroughput"),
    +			fmt.Sprintf("is only allowed for types: %v", worker.AllowedTypesThroughput)))
    +	}
    +	return allErrs
    +}
    +
     func validateNodeTemplate(nt *extensionsv1alpha1.NodeTemplate, fldPath *field.Path) field.ErrorList {
     	allErrs := field.ErrorList{}
     
    
  • pkg/apis/gcp/validation/worker_test.go+463 342 modified
    @@ -5,6 +5,8 @@
     package validation_test
     
     import (
    +	"fmt"
    +
     	"github.com/gardener/gardener/pkg/apis/core"
     	extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
     	. "github.com/onsi/ginkgo/v2"
    @@ -22,8 +24,8 @@ import (
     
     var _ = Describe("#ValidateWorkers", func() {
     	var (
    -		workers []core.Worker
     		nilPath *field.Path
    +		workers []core.Worker
     	)
     
     	BeforeEach(func() {
    @@ -70,15 +72,14 @@ var _ = Describe("#ValidateWorkers", func() {
     			},
     		}
     	})
    -	It("should pass because workers are configured correctly", func() {
    -		errorList := ValidateWorkers(workers, field.NewPath(""))
     
    +	It("should pass because workers are configured correctly", func() {
    +		errorList := ValidateWorkers(workers, nilPath)
     		Expect(errorList).To(BeEmpty())
     	})
     
     	It("should forbid because volume is not configured", func() {
     		workers[1].Volume = nil
    -
     		errorList := ValidateWorkers(workers, field.NewPath("workers"))
     
     		Expect(errorList).To(ConsistOf(
    @@ -92,7 +93,6 @@ var _ = Describe("#ValidateWorkers", func() {
     	It("should forbid because volume type and size are not configured", func() {
     		workers[0].Volume.Type = nil
     		workers[0].Volume.VolumeSize = ""
    -
     		errorList := ValidateWorkers(workers, field.NewPath("workers"))
     
     		Expect(errorList).To(ConsistOf(
    @@ -107,480 +107,610 @@ var _ = Describe("#ValidateWorkers", func() {
     		))
     	})
     
    -	It("should forbid because data volume type is empty", func() {
    -		workers[0].DataVolumes[0].Type = nil
    -		errorList := validateWorkerConfig(workers, nil)
    +	It("should forbid because worker does not specify a zone", func() {
    +		workers[0].Zones = nil
    +		errorList := ValidateWorkers(workers, field.NewPath("workers"))
    +
     		Expect(errorList).To(ConsistOf(
     			PointTo(MatchFields(IgnoreExtras, Fields{
     				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("dataVolumes[0].type"),
    +				"Field": Equal("workers[0].zones"),
     			})),
     		))
     	})
     
    -	It("should forbid because worker does not specify a zone", func() {
    -		workers[0].Zones = nil
    -
    +	It("should forbid because volume type SCRATCH must not be main volume", func() {
    +		workers[0].Volume.Type = ptr.To(worker.VolumeTypeScratch)
     		errorList := ValidateWorkers(workers, field.NewPath("workers"))
     
     		Expect(errorList).To(ConsistOf(
     			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("workers[0].zones"),
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("workers[0].volume.type"),
    +				"Detail": Equal(fmt.Sprintf("type %s is not allowed as boot disk", worker.VolumeTypeScratch)),
     			})),
     		))
     	})
     
    +	Describe("#ValidateWorkersUpdate", func() {
    +		It("should pass because workers are unchanged", func() {
    +			newWorkers := copyWorkers(workers)
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(BeEmpty())
    +		})
    +
    +		It("should allow adding workers", func() {
    +			newWorkers := copyWorkers(workers)
    +			workers = workers[:1]
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(BeEmpty())
    +		})
    +
    +		It("should allow adding a zone to a worker", func() {
    +			newWorkers := copyWorkers(workers)
    +			newWorkers[0].Zones = append(newWorkers[0].Zones, "another-zone")
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(BeEmpty())
    +		})
    +
    +		It("should forbid removing a zone from a worker", func() {
    +			newWorkers := copyWorkers(workers)
    +			newWorkers[1].Zones = newWorkers[1].Zones[1:]
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("[1].zones"),
    +					"Detail": Equal("field is immutable"),
    +				})),
    +			))
    +		})
    +
    +		It("should forbid changing the zone order", func() {
    +			newWorkers := copyWorkers(workers)
    +			newWorkers[0].Zones[0] = workers[0].Zones[1]
    +			newWorkers[0].Zones[1] = workers[0].Zones[0]
    +			newWorkers[1].Zones[0] = workers[1].Zones[1]
    +			newWorkers[1].Zones[1] = workers[1].Zones[0]
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("[0].zones"),
    +					"Detail": Equal("field is immutable"),
    +				})),
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("[1].zones"),
    +					"Detail": Equal("field is immutable"),
    +				})),
    +			))
    +		})
    +
    +		It("should forbid adding a zone while changing an existing one", func() {
    +			newWorkers := copyWorkers(workers)
    +			newWorkers = append(newWorkers, core.Worker{Name: "worker3", Zones: []string{"zone1"}})
    +			newWorkers[1].Zones[0] = workers[1].Zones[1]
    +			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +
    +			Expect(errorList).To(ConsistOf(
    +				PointTo(MatchFields(IgnoreExtras, Fields{
    +					"Type":   Equal(field.ErrorTypeInvalid),
    +					"Field":  Equal("[1].zones"),
    +					"Detail": Equal("field is immutable"),
    +				})),
    +			))
    +		})
    +	})
    +})
    +
    +var _ = Describe("#ValidateWorkerConfig", func() {
    +	var (
    +		nilPath      *field.Path
    +		workerConfig gcp.WorkerConfig
    +	)
    +
    +	BeforeEach(func() {
    +		workerConfig = gcp.WorkerConfig{
    +			GPU: &gcp.GPU{
    +				AcceleratorType: "nvidia-tesla-p100",
    +				Count:           1,
    +			},
    +			MinCpuPlatform: ptr.To("Intel Haswell"),
    +			Volume:         &gcp.Volume{},
    +			ServiceAccount: &gcp.ServiceAccount{
    +				Email:  "user@projectid.iam.gserviceaccount.com",
    +				Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
    +			},
    +		}
    +	})
    +
    +	It("should forbid because data volume type is nil", func() {
    +		dataVolumes := []core.DataVolume{{Type: nil}}
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{}, dataVolumes, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeRequired),
    +			"Field": Equal("dataVolumes[0].type"),
    +		}))))
    +	})
    +
    +	It("should allow valid source image", func() {
    +		workerConfig.DataVolumes = []gcp.DataVolume{{
    +			Name:        "foo",
    +			SourceImage: ptr.To("/debian-cloud/global/images/family/debian-9"),
    +		}}
    +		errorList := ValidateWorkerConfig(workerConfig, []core.DataVolume{{
    +			Name:       "foo",
    +			Type:       ptr.To("Volume"),
    +			VolumeSize: "30G",
    +		}}, nilPath)
    +
    +		Expect(errorList).To(BeEmpty())
    +	})
    +
    +	It("should forbid invalid data volume source image", func() {
    +		workerConfig.DataVolumes = []gcp.DataVolume{{
    +			Name:        "foo",
    +			SourceImage: ptr.To("projects/my-project/NO_UPPER_CASE"),
    +		}}
    +		errorList := ValidateWorkerConfig(workerConfig, []core.DataVolume{{
    +			Name:       "foo",
    +			Type:       ptr.To("Volume"),
    +			VolumeSize: "30G",
    +		}}, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("dataVolumes[0].sourceImage"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", VolumeSourceImageRegex)),
    +		}))))
    +	})
    +
     	It("should forbid because service account's email is empty", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			ServiceAccount: &gcp.ServiceAccount{
     				Email:  "",
    -				Scopes: []string{"scope-1"},
    +				Scopes: []string{"https://www.googleapis.com/auth"},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("providerConfig.serviceAccount.email"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("serviceAccount.email"),
    +			"Detail": Equal("must not be fewer than 1 characters, got 0"),
    +		}))))
    +	})
    +
    +	It("should forbid invalid service account email", func() {
    +		workerConfig.ServiceAccount.Email = "user@projectid.iam.WRONG_SUFFIX.com"
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("serviceAccount.email"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", ServiceAccountRegex)),
    +		}))))
    +	})
    +
    +	It("should forbid invalid service account scope", func() {
    +		workerConfig.ServiceAccount.Scopes[0] = "https://www.wrong-host.com/auth/cloud-platform"
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("serviceAccount.scopes[0]"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", ServiceAccountScopeRegex)),
    +		}))))
    +	})
    +
    +	It("should pass for valid volume interface", func() {
    +		workerConfig.Volume.LocalSSDInterface = ptr.To("NVME")
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(BeEmpty())
    +	})
    +
    +	It("should forbid invalid volume interface", func() {
    +		workerConfig.Volume.LocalSSDInterface = ptr.To("not in set")
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeNotSupported),
    +			"Field": Equal("volume.interface"),
    +		}))))
    +	})
    +
    +	It("should forbid invalid volume encryption key name", func() {
    +		workerConfig.Volume.Encryption = &gcp.DiskEncryption{
    +			KmsKeyName: ptr.To("projects/my-project/NO_SPECIAL_CHARS#"),
    +		}
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("volume.encryption.kmsKeyName"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", VolumeKmsKeyNameRegex)),
    +		}))))
    +	})
    +
    +	It("should pass for valid volume encryption", func() {
    +		workerConfig.Volume.Encryption = &gcp.DiskEncryption{
    +			KmsKeyName:           ptr.To("projects/my-project/locations/global/keyRings/my-keyring"),
    +			KmsKeyServiceAccount: ptr.To("user@projectid.iam.gserviceaccount.com"),
    +		}
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(BeEmpty())
    +	})
    +
    +	It("should forbid invalid volume encryption service account", func() {
    +		workerConfig.Volume.Encryption = &gcp.DiskEncryption{
    +			KmsKeyName:           ptr.To("projects/my-project/locations/global/keyRings/my-keyring"),
    +			KmsKeyServiceAccount: ptr.To("userMISSING_ATprojectid.iam.gserviceaccount.com"),
    +		}
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("volume.encryption.kmsKeyServiceAccount"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", ServiceAccountRegex)),
    +		}))))
     	})
     
     	It("should forbid because volume.encryption.kmsKeyName should be specified", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			Volume: &gcp.Volume{
     				Encryption: &gcp.DiskEncryption{
     					KmsKeyName: ptr.To("  "),
     				},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("providerConfig.volume.encryption.kmsKeyName"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeRequired),
    +			"Field": Equal("volume.encryption.kmsKeyName"),
    +		}))))
     	})
     
     	It("should forbid because service account scope is empty", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			ServiceAccount: &gcp.ServiceAccount{
    -				Email:  "foo",
    +				Email:  "user@projectid.iam.gserviceaccount.com",
     				Scopes: []string{},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("providerConfig.serviceAccount.scopes"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeRequired),
    +			"Field": Equal("serviceAccount.scopes"),
    +		}))))
     	})
     
     	It("should forbid because service account scopes has an empty scope", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			ServiceAccount: &gcp.ServiceAccount{
    -				Email:  "foo",
    -				Scopes: []string{"baz", ""},
    +				Email:  "user@projectid.iam.gserviceaccount.com",
    +				Scopes: []string{"https://www.googleapis.com/auth/test", ""},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("providerConfig.serviceAccount.scopes[1]"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("serviceAccount.scopes[1]"),
    +			"Detail": Equal("must not be fewer than 1 characters, got 0"),
    +		}))))
     	})
     
     	It("should forbid because service account scopes are duplicated", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			ServiceAccount: &gcp.ServiceAccount{
    -				Email:  "foo",
    -				Scopes: []string{"baz", "bar", "baz"},
    +				Email:  "user@projectid.iam.gserviceaccount.com",
    +				Scopes: []string{"https://www.googleapis.com/auth/test", "https://www.googleapis.com/auth/test"},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeDuplicate),
    -				"Field": Equal("providerConfig.serviceAccount.scopes[2]"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeDuplicate),
    +			"Field": Equal("serviceAccount.scopes[1]"),
    +		}))))
     	})
     
     	It("should allow valid service account", func() {
    -		errorList := ValidateWorkerConfig(&gcp.WorkerConfig{
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
     			ServiceAccount: &gcp.ServiceAccount{
    -				Email:  "foo",
    -				Scopes: []string{"baz"},
    +				Email:  "user@projectid.iam.gserviceaccount.com",
    +				Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
     			},
    -		}, nil)
    +		}, nil, nilPath)
     
     		Expect(errorList).To(BeEmpty())
     	})
     
     	It("should forbid because gpu accelerator type is empty", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				GPU: &gcp.GPU{
    -					AcceleratorType: "",
    -					Count:           1},
    -				ServiceAccount: &gcp.ServiceAccount{
    -					Email:  "foo",
    -					Scopes: []string{"baz"},
    -				},
    -			},
    -			nil,
    -		)
    +		workerConfig.GPU.AcceleratorType = ""
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("gpu.acceleratorType"),
    +			"Detail": Equal("must not be fewer than 1 characters, got 0"),
    +		}))))
    +	})
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeRequired),
    -				"Field": Equal("providerConfig.gpu.acceleratorType"),
    -			})),
    -		))
    +	It("should forbid invalid gpu accelerator type", func() {
    +		workerConfig.GPU.AcceleratorType = "CAPITAL-NOT-ALLOWED"
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("gpu.acceleratorType"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", GpuAcceleratorTypeRegex)),
    +		}))))
     	})
     
     	It("should forbid because gpu count is zero", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				GPU: &gcp.GPU{
    -					AcceleratorType: "foo",
    -					Count:           0},
    -				ServiceAccount: &gcp.ServiceAccount{
    -					Email:  "foo",
    -					Scopes: []string{"baz"},
    -				},
    -			},
    -			nil,
    -		)
    +		workerConfig.GPU.Count = 0
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeForbidden),
    -				"Field": Equal("providerConfig.gpu.count"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeForbidden),
    +			"Field": Equal("gpu.count"),
    +		}))))
    +	})
    +
    +	It("should forbid invalid min CPU platform", func() {
    +		workerConfig.MinCpuPlatform = ptr.To("No Special Chars#")
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("minCpuPlatform"),
    +			"Detail": Equal(fmt.Sprintf("does not match expected regex %s", MinCPUsPlatformRegex)),
    +		}))))
     	})
     
     	It("should fail because WorkerConfig NodeTemplate is specified with empty capacity", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{},
    -				},
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
    +			NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{},
     			},
    -			nil,
    -		)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":   Equal(field.ErrorTypeRequired),
    -				"Field":  Equal("providerConfig.nodeTemplate.capacity"),
    -				"Detail": Equal("capacity must not be empty"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeRequired),
    +			"Field":  Equal("nodeTemplate.capacity"),
    +			"Detail": Equal("capacity must not be empty"),
    +		}))))
     	})
     
     	It("should not fail for WorkerConfig NodeTemplate not populated with all fields", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{
    -						"cpu": resource.MustParse("80m"),
    -					},
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
    +			NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{
    +					"cpu": resource.MustParse("80m"),
     				},
     			},
    -			nil,
    -		)
    +		}, nil, nilPath)
     
     		Expect(errorList).To(BeEmpty())
     	})
     
     	It("should fail for WorkerConfig NodeTemplate resource value less than zero", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    -					Capacity: corev1.ResourceList{
    -						"cpu": resource.MustParse("-80m"),
    -					},
    +		errorList := ValidateWorkerConfig(gcp.WorkerConfig{
    +			NodeTemplate: &extensionsv1alpha1.NodeTemplate{
    +				Capacity: corev1.ResourceList{
    +					"cpu": resource.MustParse("-80m"),
     				},
     			},
    -			nil,
    -		)
    +		}, nil, nilPath)
     
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":   Equal(field.ErrorTypeInvalid),
    -				"Field":  Equal("providerConfig.nodeTemplate.capacity.cpu"),
    -				"Detail": Equal("cpu value must not be negative"),
    -			})),
    -		))
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("nodeTemplate.capacity.cpu"),
    +			"Detail": Equal("cpu value must not be negative"),
    +		}))))
     	})
     
     	It("should allow valid gpu configurations", func() {
    -		errorList := ValidateWorkerConfig(
    -			&gcp.WorkerConfig{
    -				GPU: &gcp.GPU{
    -					AcceleratorType: "foo",
    -					Count:           1},
    -				ServiceAccount: &gcp.ServiceAccount{
    -					Email:  "foo",
    -					Scopes: []string{"baz"},
    -				},
    -			},
    -			nil,
    -		)
    +		workerConfig.GPU = &gcp.GPU{
    +			AcceleratorType: "nvidia-tesla-p100",
    +			Count:           1,
    +		}
    +		errorList := ValidateWorkerConfig(workerConfig, nil, nilPath)
     
     		Expect(errorList).To(BeEmpty())
     	})
     
     	It("should allow valid dataVolume name", func() {
    -		errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    +		workerConfig := gcp.WorkerConfig{
     			DataVolumes: []gcp.DataVolume{{
     				Name: "foo",
     			}},
    -		})
    +		}
    +		dataVolumes := []core.DataVolume{{
    +			Type: ptr.To("Volume"),
    +			Name: "foo",
    +		}}
    +		errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
     		Expect(errorList).To(BeEmpty())
     	})
     
     	It("should detect duplicate dataVolume names", func() {
    -		errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    +		workerConfig := gcp.WorkerConfig{
     			DataVolumes: []gcp.DataVolume{
     				{
     					Name: "foo",
     				}, {
     					Name: "foo",
     				},
     			},
    -		})
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeDuplicate),
    -				"Field": Equal("providerConfig.dataVolume[1]"),
    -			})),
    -		))
    +		}
    +		dataVolumes := []core.DataVolume{{
    +			Type: ptr.To("Volume"),
    +			Name: "foo",
    +		}}
    +		errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":  Equal(field.ErrorTypeDuplicate),
    +			"Field": Equal("dataVolumes[1]"),
    +		}))))
     	})
     
     	It("should forbid invalid dataVolume name", func() {
    -		errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    +		workerConfig := gcp.WorkerConfig{
     			DataVolumes: []gcp.DataVolume{{
     				Name: "foo-invalid",
     			}},
    -		})
    -		Expect(errorList).To(ConsistOf(
    -			PointTo(MatchFields(IgnoreExtras, Fields{
    -				"Type":  Equal(field.ErrorTypeInvalid),
    -				"Field": Equal("providerConfig.dataVolume"),
    -			})),
    -		))
    +		}
    +		dataVolumes := []core.DataVolume{{
    +			Type: ptr.To("Volume"),
    +			Name: "foo",
    +		}}
    +		errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
    +		Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +			"Type":   Equal(field.ErrorTypeInvalid),
    +			"Field":  Equal("dataVolumes"),
    +			"Detail": Equal("could not find dataVolume with name foo-invalid"),
    +		}))))
     	})
     
     	Describe("#Volume type hyper-disk", func() {
     		It("should pass because setting ProvisionedIops is allowed for hyperdisk-extreme", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To("hyperdisk-extreme")
    -			errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    -				DataVolumes: []gcp.DataVolume{
    -					{
    -						Name:            workers[0].DataVolumes[0].Name,
    -						ProvisionedIops: ptr.To[int64](3000),
    -					},
    -				},
    -			})
    +			workerConfig := gcp.WorkerConfig{
    +				DataVolumes: []gcp.DataVolume{{
    +					Name:            "foo",
    +					ProvisionedIops: ptr.To[int64](3000),
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To("hyperdisk-extreme"),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
     			Expect(errorList).To(BeEmpty())
     		})
     
     		It("should fail because setting ProvisionedIops is not allowed for hyperdisk-throughput", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To("hyperdisk-throughput")
    -			errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    -				DataVolumes: []gcp.DataVolume{
    -					{
    -						Name:            workers[0].DataVolumes[0].Name,
    -						ProvisionedIops: ptr.To[int64](3000),
    -					},
    -				},
    -			})
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeForbidden),
    -					"Field": Equal("providerConfig.dataVolume.provisionedIops"),
    -				})),
    -			))
    +			workerConfig := gcp.WorkerConfig{
    +				DataVolumes: []gcp.DataVolume{{
    +					Name:            "foo",
    +					ProvisionedIops: ptr.To[int64](3000),
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To("hyperdisk-throughput"),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":  Equal(field.ErrorTypeForbidden),
    +				"Field": Equal("dataVolumes.provisionedIops"),
    +			}))))
     		})
     
     		It("should pass because setting ProvisionedThroughput is allowed for hyperdisk-throughput", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To("hyperdisk-throughput")
    -			errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    -				DataVolumes: []gcp.DataVolume{
    -					{
    -						Name:                  workers[0].DataVolumes[0].Name,
    -						ProvisionedThroughput: ptr.To[int64](150),
    -					},
    -				},
    -			})
    +			workerConfig := gcp.WorkerConfig{
    +				DataVolumes: []gcp.DataVolume{{
    +					Name:                  "foo",
    +					ProvisionedThroughput: ptr.To[int64](150),
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To("hyperdisk-throughput"),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
     			Expect(errorList).To(BeEmpty())
     		})
     
     		It("should fail because setting ProvisionedThroughput is not allowed for hyperdisk-extreme", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To("hyperdisk-extreme")
    -			errorList := validateWorkerConfig([]core.Worker{workers[0]}, &gcp.WorkerConfig{
    -				DataVolumes: []gcp.DataVolume{
    -					{
    -						Name:                  workers[0].DataVolumes[0].Name,
    -						ProvisionedThroughput: ptr.To[int64](150),
    -					},
    -				},
    -			})
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeForbidden),
    -					"Field": Equal("providerConfig.dataVolume.provisionedThroughput"),
    -				})),
    -			))
    +			workerConfig := gcp.WorkerConfig{
    +				DataVolumes: []gcp.DataVolume{{
    +					Name:                  "foo",
    +					ProvisionedThroughput: ptr.To[int64](150),
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To("hyperdisk-extreme"),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
    +
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":  Equal(field.ErrorTypeForbidden),
    +				"Field": Equal("dataVolumes.provisionedThroughput"),
    +			}))))
     		})
     	})
     
     	Describe("#Volume type SCRATCH", func() {
     		It("should pass because worker config is configured correctly", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To(worker.VolumeTypeScratch)
    -			errorList := validateWorkerConfig(workers, &gcp.WorkerConfig{
    +			workerConfig := gcp.WorkerConfig{
     				Volume: &gcp.Volume{
     					LocalSSDInterface: ptr.To("NVME"),
     				},
    -			})
    -			Expect(errorList).To(BeEmpty())
    -		})
    -		It("should forbid because volume type SCRATCH must not be main volume", func() {
    -			workers[0].Volume.Type = ptr.To(worker.VolumeTypeScratch)
    -
    -			errorList := ValidateWorkers(workers, field.NewPath("workers"))
    -
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("workers[0].volume.type"),
    -				})),
    -			))
    -		})
    +				DataVolumes: []gcp.DataVolume{{
    +					Name: "foo",
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To(worker.VolumeTypeScratch),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
     
    -		It("should forbid because interface of worker config is misconfiguration", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To(worker.VolumeTypeScratch)
    -			errorList := validateWorkerConfig(workers, &gcp.WorkerConfig{
    -				Volume: &gcp.Volume{
    -					LocalSSDInterface: ptr.To("Interface"),
    -				},
    -			})
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeNotSupported),
    -					"Field": Equal("providerConfig.volume.interface"),
    -				})),
    -			))
    +			Expect(errorList).To(BeEmpty())
     		})
     
     		It("should forbid because encryption is not allowed for volume type SCRATCH", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To(worker.VolumeTypeScratch)
    -			errorList := validateWorkerConfig(workers, &gcp.WorkerConfig{
    +			workerConfig := gcp.WorkerConfig{
     				Volume: &gcp.Volume{
     					LocalSSDInterface: ptr.To("NVME"),
     					Encryption: &gcp.DiskEncryption{
     						KmsKeyName: ptr.To("KmsKey"),
     					},
     				},
    -			})
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("providerConfig.volume.encryption"),
    -				})),
    -			))
    -		})
    -
    -		It("should forbid because interface of worker config is not configured", func() {
    -			workers[0].DataVolumes[0].Type = ptr.To(worker.VolumeTypeScratch)
    -			errorList := validateWorkerConfig(workers, nil)
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeRequired),
    -					"Field": Equal("providerConfig.volume.interface"),
    -				})),
    -			))
    -		})
    -	})
    -
    -	Describe("#ValidateWorkersUpdate", func() {
    -		It("should pass because workers are unchanged", func() {
    -			newWorkers := copyWorkers(workers)
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    -
    -			Expect(errorList).To(BeEmpty())
    -		})
    -
    -		It("should allow adding workers", func() {
    -			newWorkers := append(workers[:0:0], workers...)
    -			workers = workers[:1]
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    -
    -			Expect(errorList).To(BeEmpty())
    -		})
    -
    -		It("should allow adding a zone to a worker", func() {
    -			newWorkers := copyWorkers(workers)
    -			newWorkers[0].Zones = append(newWorkers[0].Zones, "another-zone")
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    -
    -			Expect(errorList).To(BeEmpty())
    -		})
    -
    -		It("should forbid removing a zone from a worker", func() {
    -			newWorkers := copyWorkers(workers)
    -			newWorkers[1].Zones = newWorkers[1].Zones[1:]
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    -
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("[1].zones"),
    -				})),
    -			))
    -		})
    -
    -		It("should forbid changing the zone order", func() {
    -			newWorkers := copyWorkers(workers)
    -			newWorkers[0].Zones[0] = workers[0].Zones[1]
    -			newWorkers[0].Zones[1] = workers[0].Zones[0]
    -			newWorkers[1].Zones[0] = workers[1].Zones[1]
    -			newWorkers[1].Zones[1] = workers[1].Zones[0]
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +				DataVolumes: []gcp.DataVolume{{
    +					Name: "foo",
    +				}},
    +			}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To(worker.VolumeTypeScratch),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
     
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("[0].zones"),
    -				})),
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("[1].zones"),
    -				})),
    -			))
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":   Equal(field.ErrorTypeInvalid),
    +				"Field":  Equal("volume.encryption"),
    +				"Detail": Equal(fmt.Sprintf("must not be set in combination with %s volumes", worker.VolumeTypeScratch)),
    +			}))))
     		})
     
    -		It("should forbid adding a zone while changing an existing one", func() {
    -			newWorkers := copyWorkers(workers)
    -			newWorkers = append(newWorkers, core.Worker{Name: "worker3", Zones: []string{"zone1"}})
    -			newWorkers[1].Zones[0] = workers[1].Zones[1]
    -			errorList := ValidateWorkersUpdate(workers, newWorkers, nilPath)
    +		It("should forbid because interface of worker config is not configured", func() {
    +			workerConfig := gcp.WorkerConfig{}
    +			dataVolumes := []core.DataVolume{{
    +				Type: ptr.To(worker.VolumeTypeScratch),
    +				Name: "foo",
    +			}}
    +			errorList := ValidateWorkerConfig(workerConfig, dataVolumes, nilPath)
     
    -			Expect(errorList).To(ConsistOf(
    -				PointTo(MatchFields(IgnoreExtras, Fields{
    -					"Type":  Equal(field.ErrorTypeInvalid),
    -					"Field": Equal("[1].zones"),
    -				})),
    -			))
    +			Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
    +				"Type":  Equal(field.ErrorTypeRequired),
    +				"Field": Equal("volume.interface"),
    +			}))))
     		})
     	})
     })
    @@ -592,12 +722,3 @@ func copyWorkers(workers []core.Worker) []core.Worker {
     	}
     	return cp
     }
    -
    -func validateWorkerConfig(workers []core.Worker, workerConfig *gcp.WorkerConfig) field.ErrorList {
    -	allErrs := field.ErrorList{}
    -	for _, worker := range workers {
    -		allErrs = append(allErrs, ValidateWorkerConfig(workerConfig, worker.DataVolumes)...)
    -	}
    -
    -	return allErrs
    -}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.