Skip to content

Commit cb5045f

Browse files
kon-angelohebelsan
andauthored
Shoot input validation (#1479)
* Add input validation (#6) Co-authored-by: Alexander Hebel <a.hebelsun@gmail.com> * Fix AWS IAM instance profile input validation * comments#1 --------- Co-authored-by: Alexander Hebel <a.hebelsun@gmail.com>
1 parent f4e9777 commit cb5045f

7 files changed

Lines changed: 285 additions & 67 deletions

File tree

pkg/apis/aws/validation/controlplane.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import (
1212
)
1313

1414
// ValidateControlPlaneConfig validates a ControlPlaneConfig object.
15-
func ValidateControlPlaneConfig(controlPlaneConfig *apisaws.ControlPlaneConfig, version string, fldPath *field.Path) field.ErrorList {
15+
func ValidateControlPlaneConfig(cpConfig *apisaws.ControlPlaneConfig, version string, fldPath *field.Path) field.ErrorList {
1616
allErrs := field.ErrorList{}
1717

18-
if controlPlaneConfig.CloudControllerManager != nil {
19-
allErrs = append(allErrs, featurevalidation.ValidateFeatureGates(controlPlaneConfig.CloudControllerManager.FeatureGates, version, fldPath.Child("cloudControllerManager", "featureGates"))...)
18+
if cpConfig.CloudControllerManager != nil {
19+
allErrs = append(allErrs, featurevalidation.ValidateFeatureGates(cpConfig.CloudControllerManager.FeatureGates, version, fldPath.Child("cloudControllerManager", "featureGates"))...)
20+
}
21+
22+
if cpConfig.LoadBalancerController != nil && cpConfig.LoadBalancerController.IngressClassName != nil {
23+
ingressClassName := *cpConfig.LoadBalancerController.IngressClassName
24+
ingressPath := fldPath.Child("loadBalancerController", "ingressClassName")
25+
allErrs = append(allErrs, validateK8sResourceName(ingressClassName, ingressPath)...)
2026
}
2127

2228
return allErrs

pkg/apis/aws/validation/controlplane_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
. "github.com/onsi/gomega"
1010
. "github.com/onsi/gomega/gstruct"
1111
"k8s.io/apimachinery/pkg/util/validation/field"
12+
"k8s.io/utils/ptr"
1213

1314
apisaws "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws"
1415
. "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws/validation"
@@ -46,5 +47,26 @@ var _ = Describe("ControlPlaneConfig validation", func() {
4647
})),
4748
))
4849
})
50+
51+
Context("LoadBalancerController", func() {
52+
It("should pass for valid ingress class name", func() {
53+
controlPlane.LoadBalancerController = &apisaws.LoadBalancerControllerConfig{
54+
IngressClassName: ptr.To("valid-ingress-class"),
55+
}
56+
Expect(ValidateControlPlaneConfig(controlPlane, "", fldPath)).To(BeEmpty())
57+
})
58+
59+
It("should fail for invalid ingress class name", func() {
60+
controlPlane.LoadBalancerController = &apisaws.LoadBalancerControllerConfig{
61+
IngressClassName: ptr.To("NoUpperCaseAllowed"),
62+
}
63+
errorList := ValidateControlPlaneConfig(controlPlane, "", fldPath)
64+
Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{
65+
"Type": Equal(field.ErrorTypeInvalid),
66+
"Field": Equal("loadBalancerController.ingressClassName"),
67+
"Detail": Equal("does not match expected regex ^[a-z0-9.-]+$"),
68+
}))))
69+
})
70+
})
4971
})
5072
})

pkg/apis/aws/validation/filter.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package validation
6+
7+
import (
8+
"fmt"
9+
"regexp"
10+
"unicode/utf8"
11+
12+
"k8s.io/apimachinery/pkg/util/validation/field"
13+
)
14+
15+
var (
16+
// k8s resource names have max 253 characters, consist of lower case alphanumeric characters, '-' or '.'
17+
k8sResourceNameRegex = `^[a-z0-9.-]+$`
18+
// VpcIDRegex matches e.g. vpc-064b5b7771f6331aa
19+
VpcIDRegex = `^vpc-[a-z0-9]+$`
20+
// EipAllocationIDRegex matches e.g. eipalloc-0676786f3e288044c
21+
EipAllocationIDRegex = `^eipalloc-[a-z0-9]+$`
22+
// SnapshotIDRegex matches e.g. snap-0676786f3e288044c
23+
SnapshotIDRegex = `^snap-[a-z0-9]+$`
24+
// IamInstanceProfileNameRegex matches https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-iam-instanceprofile.html#:~:text=Properties-,InstanceProfileName,-The%20name%20of
25+
IamInstanceProfileNameRegex = `^[\w+=,.@-]+$`
26+
// IamInstanceProfileArnRegex matches arn:aws:iam::<account-id>:instance-profile/<path>/<profile-name>
27+
// Note: for china landscapes it's arn:aws-cn:iam::<account-id>:instance-profile/<path>/<profile-name>
28+
IamInstanceProfileArnRegex = `^arn:[\w +=,.@\-/:]+$`
29+
// ZoneNameRegex matches e.g. us-east-1a
30+
ZoneNameRegex = `^[a-z0-9-]+$`
31+
// TagKeyRegex matches Letters (a–z, A–Z), numbers (0–9), spaces, and the following symbols: + - = . _ : / @
32+
TagKeyRegex = `^[\w +\-=\.:/@]+$`
33+
// GatewayEndpointRegex matches one or more word characters, optionally followed by dot-separated word segments
34+
GatewayEndpointRegex = `^\w+(\.\w+)*$`
35+
36+
validateK8sResourceName = combineValidationFuncs(regex(k8sResourceNameRegex), notEmpty, maxLength(253))
37+
validateVpcID = combineValidationFuncs(regex(VpcIDRegex), notEmpty, maxLength(255))
38+
validateEipAllocationID = combineValidationFuncs(regex(EipAllocationIDRegex), maxLength(255))
39+
validateSnapshotID = combineValidationFuncs(regex(SnapshotIDRegex), maxLength(255))
40+
validateIamInstanceProfileName = combineValidationFuncs(regex(IamInstanceProfileNameRegex), notEmpty, maxLength(128))
41+
validateIamInstanceProfileArn = combineValidationFuncs(regex(IamInstanceProfileArnRegex), maxLength(255))
42+
validateZoneName = combineValidationFuncs(regex(ZoneNameRegex), maxLength(255))
43+
validateTagKey = combineValidationFuncs(regex(TagKeyRegex), notEmpty, maxLength(128))
44+
validateGatewayEndpointName = combineValidationFuncs(regex(GatewayEndpointRegex), maxLength(255))
45+
)
46+
47+
type validateFunc[T any] func(T, *field.Path) field.ErrorList
48+
49+
// combineValidationFuncs validates a value against a list of filters.
50+
func combineValidationFuncs[T any](filters ...validateFunc[T]) validateFunc[T] {
51+
return func(t T, fld *field.Path) field.ErrorList {
52+
var allErrs field.ErrorList
53+
for _, f := range filters {
54+
allErrs = append(allErrs, f(t, fld)...)
55+
}
56+
return allErrs
57+
}
58+
}
59+
60+
// regex returns a filterFunc that validates a string against a regular expression.
61+
func regex(regex string) validateFunc[string] {
62+
compiled := regexp.MustCompile(regex)
63+
return func(name string, fld *field.Path) field.ErrorList {
64+
var allErrs field.ErrorList
65+
if name == "" {
66+
return allErrs // Allow empty strings to pass through
67+
}
68+
if !compiled.MatchString(name) {
69+
allErrs = append(allErrs, field.Invalid(fld, name, fmt.Sprintf("does not match expected regex %s", compiled.String())))
70+
}
71+
return allErrs
72+
}
73+
}
74+
75+
func notEmpty(name string, fld *field.Path) field.ErrorList {
76+
if utf8.RuneCountInString(name) == 0 {
77+
return field.ErrorList{field.Required(fld, "cannot be empty")}
78+
}
79+
return nil
80+
}
81+
82+
func maxLength(max int) validateFunc[string] {
83+
return func(name string, fld *field.Path) field.ErrorList {
84+
var allErrs field.ErrorList
85+
if l := utf8.RuneCountInString(name); l > max {
86+
return field.ErrorList{field.Invalid(fld, name, fmt.Sprintf("must not be more than %d characters, got %d", max, l))}
87+
}
88+
return allErrs
89+
}
90+
}

pkg/apis/aws/validation/infrastructure.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package validation
66

77
import (
88
"fmt"
9-
"regexp"
109
"slices"
1110
"strings"
1211

@@ -20,9 +19,6 @@ import (
2019
apisaws "github.com/gardener/gardener-extension-provider-aws/pkg/apis/aws"
2120
)
2221

23-
// valid values for networks.vpc.gatewayEndpoints
24-
var gatewayEndpointPattern = regexp.MustCompile(`^[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$`)
25-
2622
// ValidateInfrastructureConfigAgainstCloudProfile validates the given `InfrastructureConfig` against the given `CloudProfile`.
2723
func ValidateInfrastructureConfigAgainstCloudProfile(oldInfra, infra *apisaws.InfrastructureConfig, shoot *core.Shoot, cloudProfileSpec *gardencorev1beta1.CloudProfileSpec, fldPath *field.Path) field.ErrorList {
2824
allErrs := field.ErrorList{}
@@ -95,9 +91,7 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
9591
if len(infra.Networks.VPC.GatewayEndpoints) > 0 {
9692
epsPath := networksPath.Child("vpc", "gatewayEndpoints")
9793
for i, svc := range infra.Networks.VPC.GatewayEndpoints {
98-
if !gatewayEndpointPattern.MatchString(svc) {
99-
allErrs = append(allErrs, field.Invalid(epsPath.Index(i), svc, "must be a valid domain name"))
100-
}
94+
allErrs = append(allErrs, validateGatewayEndpointName(svc, epsPath.Index(i))...)
10195
}
10296
}
10397

@@ -110,6 +104,8 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
110104
for i, zone := range infra.Networks.Zones {
111105
zonePath := networksPath.Child("zones").Index(i)
112106

107+
allErrs = append(allErrs, validateZoneName(zone.Name, zonePath.Child("name"))...)
108+
113109
publicPath := zonePath.Child("public")
114110
cidrs = append(cidrs, cidrvalidation.NewCIDR(zone.Public, publicPath))
115111
allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(publicPath, zone.Public)...)
@@ -134,9 +130,7 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
134130
}
135131
referencedElasticIPAllocationIDs = append(referencedElasticIPAllocationIDs, *zone.ElasticIPAllocationID)
136132

137-
if !strings.HasPrefix(*zone.ElasticIPAllocationID, "eipalloc-") {
138-
allErrs = append(allErrs, field.Invalid(zonePath.Child("elasticIPAllocationID"), *zone.ElasticIPAllocationID, "must start with eipalloc-"))
139-
}
133+
allErrs = append(allErrs, validateEipAllocationID(*zone.ElasticIPAllocationID, zonePath.Child("elasticIPAllocationID"))...)
140134
}
141135
}
142136

@@ -146,16 +140,23 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, ipFamilie
146140
allErrs = append(allErrs, nodes.ValidateSubset(workerCIDRs...)...)
147141
}
148142

149-
if (infra.Networks.VPC.ID == nil && infra.Networks.VPC.CIDR == nil) || (infra.Networks.VPC.ID != nil && infra.Networks.VPC.CIDR != nil) {
143+
idProvided := infra.Networks.VPC.ID != nil
144+
cidrProvided := infra.Networks.VPC.CIDR != nil
145+
switch {
146+
case !idProvided && !cidrProvided:
150147
allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc"), infra.Networks.VPC, "must specify either a vpc id or a cidr"))
151-
} else if infra.Networks.VPC.CIDR != nil && infra.Networks.VPC.ID == nil && !slices.Contains(ipFamilies, core.IPFamilyIPv6) {
148+
case idProvided && cidrProvided:
149+
allErrs = append(allErrs, field.Invalid(networksPath.Child("vpc"), infra.Networks.VPC, "cannot specify both vpc id and cidr"))
150+
case cidrProvided && !idProvided && !slices.Contains(ipFamilies, core.IPFamilyIPv6):
152151
cidrPath := networksPath.Child("vpc", "cidr")
153152
vpcCIDR := cidrvalidation.NewCIDR(*infra.Networks.VPC.CIDR, cidrPath)
154153
allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(cidrPath, *infra.Networks.VPC.CIDR)...)
155154
allErrs = append(allErrs, vpcCIDR.ValidateParse()...)
156155
allErrs = append(allErrs, vpcCIDR.ValidateSubset(nodes)...)
157156
allErrs = append(allErrs, vpcCIDR.ValidateSubset(cidrs...)...)
158157
allErrs = append(allErrs, vpcCIDR.ValidateNotOverlap(pods, services)...)
158+
case idProvided && !cidrProvided:
159+
allErrs = append(allErrs, validateVpcID(*infra.Networks.VPC.ID, networksPath.Child("vpc", "id"))...)
159160
}
160161

161162
// make sure that VPC cidrs don't overlap with each other
@@ -259,8 +260,8 @@ func ValidateIgnoreTags(fldPath *field.Path, ignoreTags *apisaws.IgnoreTags) fie
259260
keysPath := fldPath.Child("keys")
260261
for i, key := range ignoreTags.Keys {
261262
idxPath := keysPath.Index(i)
262-
if key == "" {
263-
allErrs = append(allErrs, field.Invalid(idxPath, key, "ignored key must not be empty"))
263+
if errs := validateTagKey(key, idxPath); errs != nil {
264+
allErrs = append(allErrs, errs...)
264265
continue
265266
}
266267
allErrs = append(allErrs, validateKeyIsReserved(idxPath, key)...)
@@ -270,8 +271,8 @@ func ValidateIgnoreTags(fldPath *field.Path, ignoreTags *apisaws.IgnoreTags) fie
270271
prefixesPath := fldPath.Child("keyPrefixes")
271272
for i, prefix := range ignoreTags.KeyPrefixes {
272273
idxPath := prefixesPath.Index(i)
273-
if prefix == "" {
274-
allErrs = append(allErrs, field.Invalid(idxPath, prefix, "ignored key prefix must not be empty"))
274+
if errs := validateTagKey(prefix, idxPath); errs != nil {
275+
allErrs = append(allErrs, errs...)
275276
continue
276277
}
277278
allErrs = append(allErrs, validatePrefixIncludesReservedKey(idxPath, prefix)...)

0 commit comments

Comments
 (0)