Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions pkg/apis/aws/validation/controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions pkg/apis/aws/validation/controlplane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.-]+$"),
}))))
})
})
})
})
90 changes: 90 additions & 0 deletions pkg/apis/aws/validation/filter.go
Original file line number Diff line number Diff line change
@@ -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
}
}
33 changes: 17 additions & 16 deletions pkg/apis/aws/validation/infrastructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package validation

import (
"fmt"
"regexp"
"slices"
"strings"

Expand All @@ -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{}
Expand Down Expand Up @@ -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))...)
}
}

Expand All @@ -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)...)
Expand All @@ -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"))...)
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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)...)
Expand All @@ -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)...)
Expand Down
Loading
Loading