diff --git a/Makefile b/Makefile index f98f648b9..41c297a9c 100644 --- a/Makefile +++ b/Makefile @@ -262,5 +262,5 @@ start-gardener-validator-aws: -ldflags $(LD_FLAGS) \ ./controllers/provider-aws/cmd/gardener-validator-aws \ --webhook-config-server-host=0.0.0.0 \ - --webhook-config-server-port=8443 \ - --webhook-config-cert-dir=$(CERT_DIR) + --webhook-config-server-port=9443 \ + --webhook-config-cert-dir=./controllers/provider-aws/example/validator-aws-certs diff --git a/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml b/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml new file mode 100644 index 000000000..229536c93 --- /dev/null +++ b/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml @@ -0,0 +1,20 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: gardener-validator-aws +webhooks: +- name: validation.aws.provider.extensions.gardener.cloud + rules: + - apiGroups: + - "core.gardener.cloud" + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - shoots + failurePolicy: Fail + clientConfig: + url: "https://localhost:9443/webhooks/validate-shoot-aws" + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUU0akNDQXNvQ0NRRHVLOTQ5QnF0eUt6QU5CZ2txaGtpRzl3MEJBUXNGQURBek1Sb3dHQVlEVlFRS0RCRkgKWVhKa1pXNWxjaUJGZUdGdGNHeGxjekVWTUJNR0ExVUVDd3dNVUhKdmRtbGtaWElnUVZkVE1CNFhEVEU1TVRFeQpNREV4TURRd00xb1hEVEk1TVRFeE56RXhNRFF3TTFvd016RWFNQmdHQTFVRUNnd1JSMkZ5WkdWdVpYSWdSWGhoCmJYQnNaWE14RlRBVEJnTlZCQXNNREZCeWIzWnBaR1Z5SUVGWFV6Q0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFOV0UrY0F4MGg3b1pQems4UzdWY3lsbUx3YVZ3SU9BZkpkd0NSYkpuUmZNbm1RUQpqNWFaeExqN3VGR2ZhMmdxeDNFZkxZNU1YYXA1V21GSUVNM0Y4N1AxdmRzRU1lRW9ndUNLb0NsVk95cGZLQnZQCm10cXRDUWEzK3dwOUwvSlg3dGZubmJKQVZ3RTF0RzY4Mk5OajVwaVIxUXlrc1dXTGl6MFpMdG1MaFR1QlgxQWgKOW9vWnNTZy9FS1JkUmZvU1hGUWE3bFlmTkthVWNuRUNDSnNtQ0l2NjNJWFo0T1dHWU85aXNwRENkZ3ZPMFRCeApwZ3Q0NUZuUmQ5U1lhTE1kanY2WGNaL0hmYmFhWHk0QUZIeU5Lb1NaWVZxUDg0TVVia1BGQkhHZ1ZERENCa3VSCm9tOTVxb1BTK2Q0S0dCN1BnV1VnaXJLVWFpWHdIeWNNUFQ1d0Y3ZHpFWjdtUnNTakpTR1ZpK2NXZnRuMjBBNjAKanp4aG95UjZNb09nSFp6QTd2VC84THpPcDBhQXR6TFhIakJzZ1pJMlhYcU9oS01LL1BSRFE3WlRxdGo1WFpsZQo4c3hPMG51VFlaQllMcjFYTGkrYWU2ZmxtTXozeUZPMmZMR2R0dzRmMFI4Ky85aERZUEFhNHluRFBZWVdnbFVXCnJqWjFXZGptMG12bXAvaDVNeFJCbjlzTFVKb0tUVWV6Q0dzcDBXRkx4MG1BZHYxTjZIZVpUaDVxWG1XdWpjdWkKeGlXcUVNaVlWcGJhTmpWTm1OQWhUUDY4VWNkVEhRQUpnYlU5SVY4TmtmYklIcFRjY3FFaGFqUmFFdmo0SmlIbApnUzQ0V0d6VzQ0SjJKdHJzamZlYVRKZWtpbFh0WjNVdFAreXUxUjZ0NlJyQmc2bmNySmVySWRQc1RkZ0pBZ01CCkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnSUJBSzRyYUVmMEI0dGJzakhiNmIxa0hUVnMyKyt5QjdXMmVxWTgKSUtpZE1VcEFSSjhSWmVZVEh5N0lFUHZYQUQ4aW9IOW1Pd1dPT2ZyRUdCN2pBUjJCb1IyR2ZSd3hoM3RWM0RLdgoydEZZeGVKWlNuTEFuZmNLQzFlZ3duT1JuOTRjRk54bkhOQkRjS2RiU3FHL2VSNFdpVWdjV1YwbmNqVHNTUXZjCnMrdkd1WlQwUFlLcmxHYlB4VkhXSkJEdXFCQ3djczhIWEk4NzlSS3JmMTZOWVZEay9vTE5wUXV1V2NDQ0pDQzMKaWxnMTFGbkR6L1ZBMkppMWdhbGtwZEVxMHVKemlmcnY4S0ZrdmF3TUlFRWdCMTYwQnFrNmdsdkt5dWVUSHFLYQpuaCtJV3IzYlY1VHQ4QXNPdFZCdWJxempHOU9VU2g1LzZYT0FyTXhTaXBwT2llYmNYcFhrZWMyTldMRGNnQjlrCjB1T1FrTzZvUjBxZlFobXAwTEpsaEhCVGNHZkJsK3Yzd1B2RE5KS1hTM2xMVWg3L0J3d0piR3M0Mzd4TU10M1cKaGdzNTJWU0k1RU5EU2lmelplcnJzTXlQYzhHcXJ0NFhkYVRBU3VYNTlja0VDaUxSemFBbS9QQVl2N0Ntd0dvYQpLc0tOMzdZQWgrVXVONVc4Q3BLMzhFbG9Uc0ZVRzl0WEhYWmU5UG4wRjlXQU9Sb3YzVmlHTVZhZERmMzlmRDdzCjAwd1JrTnFNQy9CVFBtcHBqWXRieDQ1M2M3TWZrOGNNa0pnMGdTZnh3d2h0QUtibEszeXNOK3JsbnBMUDRPR0UKZHY4WHpKMUIrZVdzT3ZUcDI0TkRWZnJvQjFWbklmaDVKdnpiV1JZdjlLL09zdHBmWHgzZWRFR1p3djgxTnNHTQoxd0JSVkpJUgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== diff --git a/controllers/provider-aws/example/validator-aws-certs/tls.crt b/controllers/provider-aws/example/validator-aws-certs/tls.crt new file mode 100644 index 000000000..2b65d63e8 --- /dev/null +++ b/controllers/provider-aws/example/validator-aws-certs/tls.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCAasCCQC8zKHK1l+1WzANBgkqhkiG9w0BAQsFADAzMRowGAYDVQQKDBFH +YXJkZW5lciBFeGFtcGxlczEVMBMGA1UECwwMUHJvdmlkZXIgQVdTMB4XDTE5MTEy +MDExMDY1M1oXDTI5MTExNzExMDY1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1n76AhTFkX6VDkJZQSaf9Woy ++vquQtkRbCfjLSuRJc3BCqHs6aPwwUkWGu0fs0DlLtlwj73PDEXXH1vcjJtgktnc +ledZx7zMqsZ18sL7SJuvt+UictFIkGdVD18CDnd8pYDSmWrTX4NrKYwDciIHjqcd +5bKugdH/4cZHGVv2zHfICKTW5CyiX/qwtVrgC5GeEyc8Ayl+pITUSxAlj6gcFmRB +ANDNh2HF3sxwkt1+xM2xOZVQtFiM4mRMcn0yu0DNJzW3sXNAUH3m1emVpG6I8Mns +kRwC6GupFsgpWQy2zl8u7XaFIwRHvrMAtUpGaRQCA7a0nWySVKZDBsXQdtVLRwID +AQABMA0GCSqGSIb3DQEBCwUAA4ICAQBM6cRiTAv+lqDhfEvIU2OMBP6Rb4WebAi7 +qCMHAiTl0hCSDO3ZlFVl73OduOENdDvupMGmez0Zjy5Tz9RiU8x4bt93DbAuOAQF +hOhLvDuZM9p10z1RBoeshyG42JQHAws3qM8hTpdQSrClO+Q21meEGYPYr4stYdeK +5SoZG/Wu9j+jtvylkJNJgoTWHyuDm2C6NakBh6M5aJS28eXgUOQ9IwH9+7pYS+Ow +QHNvAjffo67oXBxVWnbpMmymtTutpuf5QMbb0GSX6sad9oTm5vSXWHJYvg5nCZ7d +dn2lsYjP5ishHy+Kr6vTkYdSRiFgQEyLnk72fIdn2RvsW9073Y6CGpgxSQMhz9tT +L/J43Ym1yTJH1DDDraMJsn7uUERFY7B+h3ZWQ3mmafR2w6VbLlHMJmAhlmFLL9dv +lDDBXr0mwgVEFLiKWQhBc6++AgDcog/J21sGdHiASw7wywf+FaSr+UnPZ27mk74Z +IDNzsP9WULgqGYsS/zCHdeGw3YNgaq/XNpaG+qi5iuqiAPgP66mir9tICJndC+lW +ffgjRcuG8AK366Fx8GtfQ4SJEfVBK+ZNpfDsctHrMCRMlhB0V7JNgPIqnMUVdHUI +QGwKo8OSoyNFL/8QjaHBgNkWlskvvIPPIKjkVTUZUUvoOWWODzv3tw0pht/aEHXe +14bwn68m4Q== +-----END CERTIFICATE----- diff --git a/controllers/provider-aws/example/validator-aws-certs/tls.key b/controllers/provider-aws/example/validator-aws-certs/tls.key new file mode 100644 index 000000000..a26fb165f --- /dev/null +++ b/controllers/provider-aws/example/validator-aws-certs/tls.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA1n76AhTFkX6VDkJZQSaf9Woy+vquQtkRbCfjLSuRJc3BCqHs +6aPwwUkWGu0fs0DlLtlwj73PDEXXH1vcjJtgktncledZx7zMqsZ18sL7SJuvt+Ui +ctFIkGdVD18CDnd8pYDSmWrTX4NrKYwDciIHjqcd5bKugdH/4cZHGVv2zHfICKTW +5CyiX/qwtVrgC5GeEyc8Ayl+pITUSxAlj6gcFmRBANDNh2HF3sxwkt1+xM2xOZVQ +tFiM4mRMcn0yu0DNJzW3sXNAUH3m1emVpG6I8MnskRwC6GupFsgpWQy2zl8u7XaF +IwRHvrMAtUpGaRQCA7a0nWySVKZDBsXQdtVLRwIDAQABAoIBABCrO3iP7q6Y3LKH ++3Gxs7qZry6L7qDpR45VJzVqblQ2wiq2XLfncp1CtcIP7We7wlO6uCGjiYSVpNse +A2y14nJnFdpcaUC5blpTI/Viq65/0s8CsoOjufTm4thX9Mv1Ay3FbhhYEecZSmmn +JNloxZeTayJfmWojTLRZ+UqCOBK5k9h0Kno7M5M3pew46MOhULqGa7WzIK2mCbYY +mY6QrScP2+i7ocUi0wK25cIEMAHJu/6p+Zo2UaSCMnfrFpklMThu6Klc0oQFQSJI +Yal/cqGchB4wzn0hbZgNtkuZefbWvrCnu+hpQofAVN4x+4LN6b+YMkQshIDf3j9v +48iKWkkCgYEA7Uz1rkY9BfpnP2RFzAhX4uaDjjk9mcyK09fhPhr8PtjAbHBVT32v +Maxwj746VxhFrOUlJZ/5tsvjoN2UyKWiCWyYb79ZcAevqCkna1in52Wd0mHb9YYY +aMAVjOyocRZvv+bCMgF5jBJ0EKEhlu4mIM7vtRs097nO8heelpPmtWUCgYEA52X7 +UE5uypG/auOp6enjTrXbAkc9jj+JcK9imBQzcXxR5lQWO7MceuLk+kThyLl2YoGt +oQrS9LU/bWXhzb+TsBgZ1UcGRED50R7Is9PJo/QRbNXIPIkvLMLnK7alCSIcF770 +b0WskH6cs4h61VcrYFntDkSaZBkJudJx6K/OOTsCgYAyQt+yluvr7TqbIajq60V6 +KKrqn9MdVUZ+UjZCCkMtKImxLiXTnWJTGhwJRhhjRB/V2/7/NiAVCKBg/S27ReHJ +LzgmSxgtc2NQMc9InFGL4GkKG3IUUd+vqCeoXqPauA7ZTY4KO2e8NFhjAU31AuIO +huYcrPOOGMvtWPVdHVx7RQKBgFLVKtVgfkB9U+xLevOFCh2O88so/VwCWoy/+6c8 +8/1X52lwCFVulG9Y8Wa1aa2U1lAE48aWPVXj28Sph99DCPcsaXLzbcbZC5RUVLwq +wC+0mtg+3uLsqLp5Oo9nXkSatTu6231Jj7BZ4nZSEMZ14c0n47gLzsiuPdELCEOn +S0cpAoGAL9PGWkxJvZjaoOlCVzWjngMaG4B2U8kFXD3HCJVV3JFxSG3mvCUaQRRP +Q9Szf7CqTxcw6JAZH5GQnQIJBHC3wZSov3e/IshzYlCexYsk95Y/X0buI9rOhQEj +CCNgNMhfvPMI2qjW3zI+btRhngZwDQ7QBgM5ZvArYZuNqGsH0AM= +-----END RSA PRIVATE KEY----- diff --git a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go index caa8f91b8..94cb49863 100644 --- a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go +++ b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go @@ -89,19 +89,27 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, nodesCIDR workerCIDRs = make([]cidrvalidation.CIDR, 0, len(infra.Networks.Zones)) ) + validatedZones := sets.NewString() for i, zone := range infra.Networks.Zones { - internalPath := networksPath.Child("zones").Index(i).Child("internal") + zonePath := networksPath.Child("zones").Index(i) + if validatedZones.Has(zone.Name) { + allErrs = append(allErrs, field.Invalid(zonePath.Child("name"), zone.Name, "each zone may only be specified once")) + } + + internalPath := zonePath.Child("internal") cidrs = append(cidrs, cidrvalidation.NewCIDR(zone.Internal, internalPath)) allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(internalPath, zone.Internal)...) - publicPath := networksPath.Child("zones").Index(i).Child("public") + publicPath := zonePath.Child("public") cidrs = append(cidrs, cidrvalidation.NewCIDR(zone.Public, publicPath)) allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(publicPath, zone.Public)...) - workerPath := networksPath.Child("zones").Index(i).Child("workers") + workerPath := zonePath.Child("workers") cidrs = append(cidrs, cidrvalidation.NewCIDR(zone.Workers, workerPath)) allErrs = append(allErrs, cidrvalidation.ValidateCIDRIsCanonical(workerPath, zone.Workers)...) workerCIDRs = append(workerCIDRs, cidrvalidation.NewCIDR(zone.Workers, workerPath)) + + validatedZones.Insert(zone.Name) } allErrs = append(allErrs, cidrvalidation.ValidateCIDRParse(cidrs...)...) @@ -133,7 +141,27 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, nodesCIDR func ValidateInfrastructureConfigUpdate(oldConfig, newConfig *apisaws.InfrastructureConfig, nodesCIDR, podsCIDR, servicesCIDR *string) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateImmutableField(newConfig.Networks, oldConfig.Networks, field.NewPath("networks"))...) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(newConfig.Networks.VPC, oldConfig.Networks.VPC, field.NewPath("networks.vpc"))...) + + var ( + oldZones = oldConfig.Networks.Zones + newZones = newConfig.Networks.Zones + missingZones = sets.NewString() + ) + + for i, oldZone := range oldZones { + missingZones.Insert(oldZone.Name) + for j, newZone := range newZones { + if newZone.Name == oldZone.Name { + missingZones.Delete(newZone.Name) + allErrs = append(allErrs, apivalidation.ValidateImmutableField(newConfig.Networks.Zones[j], oldConfig.Networks.Zones[j], field.NewPath("networks.zones").Index(i))...) + } + } + } + + for zone := range missingZones { + allErrs = append(allErrs, field.Invalid(field.NewPath("networks.zones"), zone, "zone is missing - removing a zone is not supported")) + } return allErrs } diff --git a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go index 0cbbcadeb..d2df4690d 100644 --- a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go +++ b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go @@ -36,6 +36,14 @@ var _ = Describe("InfrastructureConfig validation", func() { vpc = "10.0.0.0/8" invalidCIDR = "invalid-cidr" zone = "zone1" + zone2 = "zone2" + + awsZone2 = apisaws.Zone{ + Name: zone2, + Internal: "10.250.4.0/24", + Public: "10.250.5.0/24", + Workers: "10.250.6.0/24", + } ) BeforeEach(func() { @@ -62,7 +70,6 @@ var _ = Describe("InfrastructureConfig validation", func() { shoot *gardencorev1alpha1.Shoot region = "eu-west" region2 = "us-west" - zone2 = "zone2" ) Context("zones validation", func() { BeforeEach(func() { @@ -120,6 +127,34 @@ var _ = Describe("InfrastructureConfig validation", func() { }) Describe("#ValidateInfrastructureConfig", func() { + Context("Zones", func() { + It("should forbid empty zones", func() { + infrastructureConfig.Networks.Zones = nil + + errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services) + + Expect(errorList).To(ConsistOfFields(Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("networks.zones"), + "Detail": Equal("must specify at least the networks for one zone"), + })) + + }) + + It("should forbid adding a zone", func() { + infrastructureConfig.Networks.Zones = append(infrastructureConfig.Networks.Zones, awsZone2) + infrastructureConfig.Networks.Zones[1].Name = zone + + errorList := ValidateInfrastructureConfig(infrastructureConfig, &nodes, &pods, &services) + + Expect(errorList).To(ConsistOfFields(Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones[1].name"), + "Detail": Equal("each zone may only be specified once"), + })) + }) + }) + Context("CIDR", func() { It("should forbid invalid VPC CIDRs", func() { infrastructureConfig.Networks.VPC.CIDR = &invalidCIDR @@ -302,7 +337,17 @@ var _ = Describe("InfrastructureConfig validation", func() { Expect(ValidateInfrastructureConfigUpdate(infrastructureConfig, infrastructureConfig, &nodes, &pods, &services)).To(BeEmpty()) }) - It("should forbid changing the network section", func() { + It("should allow adding a zone", func() { + + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones = append(newInfrastructureConfig.Networks.Zones, awsZone2) + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, &nodes, &pods, &services) + + Expect(errorList).To(BeEmpty()) + }) + + It("should forbid changing the VPC", func() { newInfrastructureConfig := infrastructureConfig.DeepCopy() newCIDR := "1.2.3.4/5" newInfrastructureConfig.Networks.VPC.CIDR = &newCIDR @@ -311,7 +356,31 @@ var _ = Describe("InfrastructureConfig validation", func() { Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal("networks"), + "Field": Equal("networks.vpc"), + })))) + }) + + It("should forbid changing the zone information", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones[0].Internal = "10.250.2.0/24" + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, &nodes, &pods, &services) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones[0]"), + })))) + }) + + It("should forbid removing a zone", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones[0] = awsZone2 + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, &nodes, &pods, &services) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones"), })))) }) }) diff --git a/controllers/provider-aws/pkg/validator/shoot_handler.go b/controllers/provider-aws/pkg/validator/shoot_handler.go index 0153c6ae6..68478cdfa 100644 --- a/controllers/provider-aws/pkg/validator/shoot_handler.go +++ b/controllers/provider-aws/pkg/validator/shoot_handler.go @@ -50,7 +50,13 @@ func (v *Shoot) Handle(ctx context.Context, req admission.Request) admission.Res return admission.Allowed("webhook not responsible for this provider") } - if req.Operation == admissionv1beta1.Update { + switch req.Operation { + case admissionv1beta1.Create: + if err := v.validateShoot(ctx, shoot); err != nil { + v.Logger.Error(err, "denied request") + return admission.Errored(http.StatusBadRequest, err) + } + case admissionv1beta1.Update: oldShoot := &gardencorev1alpha1.Shoot{} _, _, err := v.decoder.Decode(req.OldObject.Raw, nil, oldShoot) if err != nil { @@ -62,11 +68,8 @@ func (v *Shoot) Handle(ctx context.Context, req admission.Request) admission.Res v.Logger.Error(err, "denied request") return admission.Errored(http.StatusBadRequest, err) } - } - - if err := v.validateShoot(ctx, shoot); err != nil { - v.Logger.Error(err, "denied request") - return admission.Errored(http.StatusBadRequest, err) + default: + v.Logger.Info("Webhook not responsible", "Operation", req.Operation) } return admission.Allowed("validations succeeded")