From 1975c6c36c35c259a4ba2989534ee9f08bbb4672 Mon Sep 17 00:00:00 2001 From: Tim Usner Date: Wed, 6 Nov 2019 10:52:04 +0100 Subject: [PATCH] Add AWS validator for core resources --- Makefile | 10 + .../cmd/gardener-extension-hyper/app/app.go | 2 + .../charts/validator-aws/.helmignore | 21 ++ .../charts/validator-aws/Chart.yaml | 5 + .../charts/application/Chart.yaml | 4 + .../charts/application/templates/_helpers.tpl | 1 + .../charts/application/templates/rbac.yaml | 30 +++ .../application/templates/serviceaccount.yaml | 7 + .../validatingwebhook-validator.yaml | 27 +++ .../charts/application/values.yaml | 8 + .../validator-aws/charts/runtime/Chart.yaml | 4 + .../charts/runtime/templates/_helpers.tpl | 1 + .../charts/runtime/templates/deployment.yaml | 72 +++++++ .../charts/runtime/templates/secret-cert.yaml | 15 ++ .../runtime/templates/secret-kubeconfig.yaml | 14 ++ .../charts/runtime/templates/service.yaml | 15 ++ .../validator-aws/charts/runtime/values.yaml | 20 ++ .../validator-aws/templates/_helpers.tpl | 23 +++ .../charts/validator-aws/values.yaml | 26 +++ .../app/app.go | 95 +++++++++ .../app/options.go | 59 ++++++ .../gardener-extension-validator-aws/main.go | 33 ++++ .../40-validatingwebhookconfiguration.yaml | 20 ++ .../example/validator-aws-certs/tls.crt | 23 +++ .../example/validator-aws-certs/tls.key | 27 +++ .../pkg/apis/aws/validation/infrastructure.go | 77 +++++++- .../aws/validation/infrastructure_test.go | 185 +++++++++++++++++- .../pkg/apis/aws/validation/shoot.go | 73 +++++++ .../pkg/apis/aws/validation/shoot_test.go | 145 ++++++++++++++ .../pkg/apis/aws/validation/worker.go | 3 +- .../pkg/validator/serialization.go | 50 +++++ .../pkg/validator/shoot_handler.go | 88 +++++++++ .../pkg/validator/shoot_validator.go | 123 ++++++++++++ pkg/controller/cmd/options.go | 2 + pkg/util/serialization.go | 43 ++++ 35 files changed, 1336 insertions(+), 15 deletions(-) create mode 100644 controllers/provider-aws/charts/validator-aws/.helmignore create mode 100644 controllers/provider-aws/charts/validator-aws/Chart.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/application/Chart.yaml create mode 120000 controllers/provider-aws/charts/validator-aws/charts/application/templates/_helpers.tpl create mode 100644 controllers/provider-aws/charts/validator-aws/charts/application/templates/rbac.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/application/templates/serviceaccount.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/application/templates/validatingwebhook-validator.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/application/values.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/Chart.yaml create mode 120000 controllers/provider-aws/charts/validator-aws/charts/runtime/templates/_helpers.tpl create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/templates/deployment.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-cert.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-kubeconfig.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/templates/service.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/charts/runtime/values.yaml create mode 100644 controllers/provider-aws/charts/validator-aws/templates/_helpers.tpl create mode 100644 controllers/provider-aws/charts/validator-aws/values.yaml create mode 100644 controllers/provider-aws/cmd/gardener-extension-validator-aws/app/app.go create mode 100644 controllers/provider-aws/cmd/gardener-extension-validator-aws/app/options.go create mode 100644 controllers/provider-aws/cmd/gardener-extension-validator-aws/main.go create mode 100644 controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml create mode 100644 controllers/provider-aws/example/validator-aws-certs/tls.crt create mode 100644 controllers/provider-aws/example/validator-aws-certs/tls.key create mode 100644 controllers/provider-aws/pkg/apis/aws/validation/shoot.go create mode 100644 controllers/provider-aws/pkg/apis/aws/validation/shoot_test.go create mode 100644 controllers/provider-aws/pkg/validator/serialization.go create mode 100644 controllers/provider-aws/pkg/validator/shoot_handler.go create mode 100644 controllers/provider-aws/pkg/validator/shoot_validator.go create mode 100644 pkg/util/serialization.go diff --git a/Makefile b/Makefile index 309440d11..d448dacd9 100644 --- a/Makefile +++ b/Makefile @@ -254,3 +254,13 @@ start-shoot-cert-service: --ignore-operation-annotation=$(IGNORE_OPERATION_ANNOTATION) \ --leader-election=$(LEADER_ELECTION) \ --config=./controllers/extension-shoot-cert-service/example/00-config.yaml + +.PHONY: validator-aws +validator-aws: + @LEADER_ELECTION_NAMESPACE=garden GO111MODULE=on go run \ + -mod=vendor \ + -ldflags $(LD_FLAGS) \ + ./controllers/provider-aws/cmd/gardener-extension-validator-aws \ + --webhook-config-server-host=0.0.0.0 \ + --webhook-config-server-port=9443 \ + --webhook-config-cert-dir=./controllers/provider-aws/example/validator-aws-certs diff --git a/controllers/hyper/cmd/gardener-extension-hyper/app/app.go b/controllers/hyper/cmd/gardener-extension-hyper/app/app.go index f231f7080..862330578 100644 --- a/controllers/hyper/cmd/gardener-extension-hyper/app/app.go +++ b/controllers/hyper/cmd/gardener-extension-hyper/app/app.go @@ -14,6 +14,7 @@ import ( ubuntu "github.com/gardener/gardener-extensions/controllers/os-ubuntu/cmd/gardener-extension-os-ubuntu/app" provideralicloud "github.com/gardener/gardener-extensions/controllers/provider-alicloud/cmd/gardener-extension-provider-alicloud/app" provideraws "github.com/gardener/gardener-extensions/controllers/provider-aws/cmd/gardener-extension-provider-aws/app" + validatoraws "github.com/gardener/gardener-extensions/controllers/provider-aws/cmd/gardener-extension-validator-aws/app" providerazure "github.com/gardener/gardener-extensions/controllers/provider-azure/cmd/gardener-extension-provider-azure/app" providergcp "github.com/gardener/gardener-extensions/controllers/provider-gcp/cmd/gardener-extension-provider-gcp/app" provideropenstack "github.com/gardener/gardener-extensions/controllers/provider-openstack/cmd/gardener-extension-provider-openstack/app" @@ -43,6 +44,7 @@ func NewHyperCommand(ctx context.Context) *cobra.Command { networkcalico.NewControllerManagerCommand(ctx), dnsservice.NewServiceControllerCommand(ctx), shootcertservice.NewServiceControllerCommand(ctx), + validatoraws.NewValidatorCommand(ctx), ) return cmd diff --git a/controllers/provider-aws/charts/validator-aws/.helmignore b/controllers/provider-aws/charts/validator-aws/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/controllers/provider-aws/charts/validator-aws/Chart.yaml b/controllers/provider-aws/charts/validator-aws/Chart.yaml new file mode 100644 index 000000000..9b42d15a1 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for the Gardener AWS validator +name: gardener-extensions-validator-aws +version: 0.1.0 diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/Chart.yaml b/controllers/provider-aws/charts/validator-aws/charts/application/Chart.yaml new file mode 100644 index 000000000..15684690f --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A Helm chart to deploy the gardener-extensions-validator-aws application related resources +name: application +version: 0.1.0 diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/templates/_helpers.tpl b/controllers/provider-aws/charts/validator-aws/charts/application/templates/_helpers.tpl new file mode 120000 index 000000000..adedeb924 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/templates/_helpers.tpl @@ -0,0 +1 @@ +../../../templates/_helpers.tpl \ No newline at end of file diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/templates/rbac.yaml b/controllers/provider-aws/charts/validator-aws/charts/application/templates/rbac.yaml new file mode 100644 index 000000000..fa98d22d0 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/templates/rbac.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "name" . }} + labels: +{{ include "labels" . | indent 4 }} +rules: +- apiGroups: + - core.gardener.cloud + resources: + - cloudprofiles + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "name" . }} + labels: +{{ include "labels" . | indent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "name" . }} +subjects: +- kind: ServiceAccount + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/templates/serviceaccount.yaml b/controllers/provider-aws/charts/validator-aws/charts/application/templates/serviceaccount.yaml new file mode 100644 index 000000000..52458f74c --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "labels" . | indent 4 }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/templates/validatingwebhook-validator.yaml b/controllers/provider-aws/charts/validator-aws/charts/application/templates/validatingwebhook-validator.yaml new file mode 100644 index 000000000..8fa877a2c --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/templates/validatingwebhook-validator.yaml @@ -0,0 +1,27 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ include "name" . }} +webhooks: +- name: validation.aws.provider.extensions.gardener.cloud + rules: + - apiGroups: + - "core.gardener.cloud" + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - shoots + failurePolicy: Fail + clientConfig: + {{- if .Values.global.virtualGarden.enabled }} + url: {{ printf "https://gardener-extensions-validator-aws.%s/webhooks/validate-shoot-aws" .Release.Namespace }} + {{- else }} + service: + namespace: {{ .Release.Namespace }} + name: {{ include "name" . }} + path: /webhooks/validate-shoot-aws + {{- end }} + caBundle: {{ required ".Values.webhookConfig.caBundle is required" (b64enc .Values.global.webhookConfig.caBundle) }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/application/values.yaml b/controllers/provider-aws/charts/validator-aws/charts/application/values.yaml new file mode 100644 index 000000000..41786564b --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/application/values.yaml @@ -0,0 +1,8 @@ +global: + virtualGarden: + enabled: false + webhookConfig: + caBundle: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE------- diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/Chart.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/Chart.yaml new file mode 100644 index 000000000..e3beaa28f --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A Helm chart to deploy the gardener-extensions-validator-aws runtime related resources +name: runtime +version: 0.1.0 diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/_helpers.tpl b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/_helpers.tpl new file mode 120000 index 000000000..adedeb924 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/_helpers.tpl @@ -0,0 +1 @@ +../../../templates/_helpers.tpl \ No newline at end of file diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/deployment.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/deployment.yaml new file mode 100644 index 000000000..bb872ae56 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "labels" . | indent 4 }} +spec: + revisionHistoryLimit: 0 + replicas: {{ .Values.global.replicaCount }} + selector: + matchLabels: +{{ include "labels" . | indent 6 }} + template: + metadata: + annotations: + checksum/secret-gardener-extensions-validator-aws-cert: {{ include (print $.Template.BasePath "/secret-cert.yaml") . | sha256sum }} + {{- if .Values.global.kubeconfig }} + checksum/gardener-extensions-validator-aws-kubeconfig: {{ include (print $.Template.BasePath "/secret-kubeconfig.yaml") . | sha256sum }} + {{- end }} + labels: +{{ include "labels" . | indent 8 }} + spec: + {{- if .Values.global.kubeconfig }} + automountServiceAccountToken: false + {{- end }} + containers: + - name: {{ include "name" . }} + image: {{ include "image" .Values.global.image }} + imagePullPolicy: {{ .Values.global.image.pullPolicy }} + command: + - /gardener-extension-hyper + - validator-aws + - --webhook-config-server-port={{ .Values.global.webhookConfig.serverPort }} + - --webhook-config-cert-dir=/etc/gardener-extensions-validator-aws/srv + {{- if .Values.global.kubeconfig }} + - --kubeconfig=/etc/gardener-extensions-validator-aws/kubeconfig/kubeconfig + {{- end }} + ports: + - name: webhook-server + containerPort: {{ .Values.global.webhookConfig.serverPort }} + protocol: TCP + livenessProbe: + tcpSocket: + port: {{ .Values.global.webhookConfig.serverPort }} + initialDelaySeconds: 5 + periodSeconds: 10 +{{- if .Values.global.resources }} + resources: +{{ toYaml .Values.global.resources | nindent 10 }} +{{- end }} + volumeMounts: + - name: gardener-extensions-validator-aws-cert + mountPath: /etc/gardener-extensions-validator-aws/srv + readOnly: true + {{- if .Values.global.kubeconfig }} + - name: gardener-extensions-validator-aws-kubeconfig + mountPath: /etc/gardener-extensions-validator-aws/kubeconfig + readOnly: true + {{- end }} + serviceAccountName: {{ include "name" . }} + volumes: + - name: gardener-extensions-validator-aws-cert + secret: + secretName: gardener-extensions-validator-aws-cert + defaultMode: 420 + {{- if .Values.global.kubeconfig }} + - name: gardener-extensions-validator-aws-kubeconfig + secret: + secretName: gardener-extensions-validator-aws-kubeconfig + defaultMode: 420 + {{- end }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-cert.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-cert.yaml new file mode 100644 index 000000000..59d64a90c --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-cert.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Secret +metadata: + name: gardener-extensions-validator-aws-cert + namespace: {{ .Release.Namespace }} + labels: + app: gardener + role: controller-manager + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + tls.crt: {{ required ".Values.global.webhookConfig.tls.crt is required" (b64enc .Values.global.webhookConfig.tls.crt) }} + tls.key: {{ required ".Values.global.webhookConfig.tls.key is required" (b64enc .Values.global.webhookConfig.tls.key) }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-kubeconfig.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-kubeconfig.yaml new file mode 100644 index 000000000..3f1a0dd89 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/secret-kubeconfig.yaml @@ -0,0 +1,14 @@ +{{- if .Values.global.kubeconfig }} +apiVersion: v1 +kind: Secret +metadata: + name: gardener-extensions-validator-aws-kubeconfig + namespace: "{{ .Release.Namespace }}" + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +type: Opaque +data: + kubeconfig: {{ .Values.global.kubeconfig | b64enc }} +{{- end }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/service.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/service.yaml new file mode 100644 index 000000000..fd76ba721 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "name" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "labels" . | indent 4 }} +spec: + type: ClusterIP + selector: +{{ include "labels" . | indent 4 }} + ports: + - port: 443 + protocol: TCP + targetPort: {{ .Values.global.webhookConfig.serverPort }} diff --git a/controllers/provider-aws/charts/validator-aws/charts/runtime/values.yaml b/controllers/provider-aws/charts/validator-aws/charts/runtime/values.yaml new file mode 100644 index 000000000..92ee75556 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/charts/runtime/values.yaml @@ -0,0 +1,20 @@ +global: + image: + repository: eu.gcr.io/gardener-project/gardener/gardener-extension-hyper + tag: latest + pullPolicy: IfNotPresent + replicaCount: 1 + resources: {} + webhookConfig: + serverPort: 443 + tls: + crt: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + # Kubeconfig to the target cluster. In-cluster configuration will be used if not specified. + kubeconfig: diff --git a/controllers/provider-aws/charts/validator-aws/templates/_helpers.tpl b/controllers/provider-aws/charts/validator-aws/templates/_helpers.tpl new file mode 100644 index 000000000..d67e1c5b0 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/templates/_helpers.tpl @@ -0,0 +1,23 @@ +{{- define "name" -}} +gardener-extensions-validator-aws +{{- end -}} + +{{- define "labels.app.key" -}} +app.kubernetes.io/name +{{- end -}} +{{- define "labels.app.value" -}} +{{ include "name" . }} +{{- end -}} + +{{- define "labels" -}} +{{ include "labels.app.key" . }}: {{ include "labels.app.value" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "image" -}} + {{- if hasPrefix "sha256:" .tag }} + {{- printf "%s@%s" .repository .tag }} + {{- else }} + {{- printf "%s:%s" .repository .tag }} + {{- end }} +{{- end }} diff --git a/controllers/provider-aws/charts/validator-aws/values.yaml b/controllers/provider-aws/charts/validator-aws/values.yaml new file mode 100644 index 000000000..8bd91f2c9 --- /dev/null +++ b/controllers/provider-aws/charts/validator-aws/values.yaml @@ -0,0 +1,26 @@ +global: + virtualGarden: + enabled: false + image: + repository: eu.gcr.io/gardener-project/gardener/gardener-extension-hyper + tag: latest + pullPolicy: IfNotPresent + replicaCount: 1 + resources: {} + webhookConfig: + caBundle: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + serverPort: 443 + tls: + crt: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + # Kubeconfig to the target cluster. In-cluster configuration will be used if not specified. + kubeconfig: diff --git a/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/app.go b/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/app.go new file mode 100644 index 000000000..6e6cca419 --- /dev/null +++ b/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/app.go @@ -0,0 +1,95 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "context" + "fmt" + + awsinstall "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws/install" + provideraws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/aws" + "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/validator" + controllercmd "github.com/gardener/gardener-extensions/pkg/controller/cmd" + "github.com/gardener/gardener-extensions/pkg/util" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + componentbaseconfig "k8s.io/component-base/config" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +var log = logf.Log.WithName("gardener-extensions-validator-aws") + +// NewValidatorCommand creates a new command for running a AWS shoot validator. +func NewValidatorCommand(ctx context.Context) *cobra.Command { + var ( + restOpts = &controllercmd.RESTOptions{} + mgrOpts = &controllercmd.ManagerOptions{ + WebhookServerPort: 443, + } + serverOpts = &ServerOptions{} + + aggOption = controllercmd.NewOptionAggregator( + restOpts, + mgrOpts, + serverOpts, + ) + ) + + cmd := &cobra.Command{ + Use: fmt.Sprintf("validator-%s", provideraws.Type), + + Run: func(cmd *cobra.Command, args []string) { + if err := aggOption.Complete(); err != nil { + controllercmd.LogErrAndExit(err, "Error completing options") + } + + util.ApplyClientConnectionConfigurationToRESTConfig(&componentbaseconfig.ClientConnectionConfiguration{ + QPS: 100.0, + Burst: 130, + }, restOpts.Completed().Config) + + mgr, err := manager.New(restOpts.Completed().Config, mgrOpts.Completed().Options()) + if err != nil { + controllercmd.LogErrAndExit(err, "Could not instantiate manager") + } + + if err := gardencorev1alpha1.AddToScheme(mgr.GetScheme()); err != nil { + controllercmd.LogErrAndExit(err, "Could not update manager scheme") + } + + if err := awsinstall.AddToScheme(mgr.GetScheme()); err != nil { + controllercmd.LogErrAndExit(err, "Could not update manager scheme") + } + + log.Info("Setting up webhook server") + hookServer := mgr.GetWebhookServer() + hookServer.CertDir = serverOpts.Completed().CertDir + + log.Info("Registering webhooks") + hookServer.Register("/webhooks/validate-shoot-aws", &webhook.Admission{Handler: &validator.Shoot{Logger: log.WithName("shoot-validator")}}) + + if err := mgr.Start(ctx.Done()); err != nil { + controllercmd.LogErrAndExit(err, "Error running manager") + } + }, + } + + aggOption.AddFlags(cmd.Flags()) + + return cmd +} diff --git a/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/options.go b/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/options.go new file mode 100644 index 000000000..88f3d0cae --- /dev/null +++ b/controllers/provider-aws/cmd/gardener-extension-validator-aws/app/options.go @@ -0,0 +1,59 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package app + +import ( + "github.com/spf13/pflag" +) + +const ( + // CertDirFlag is the name of the command line flag to specify the directory that contains the webhook server key and certificate. + CertDirFlag = "webhook-config-cert-dir" +) + +// ServerOptions are command line options that can be set for ServerConfig. +type ServerOptions struct { + // CertDir is the directory that contains the webhook server key and certificate. + CertDir string + + config *ServerConfig +} + +// ServerConfig is a completed webhook server configuration. +type ServerConfig struct { + // CertDir is the directory that contains the webhook server key and certificate. + CertDir string +} + +// Complete implements Completer.Complete. +func (w *ServerOptions) Complete() error { + w.config = &ServerConfig{ + CertDir: w.CertDir, + } + + return nil +} + +// Completed returns the completed ServerConfig. Only call this if `Complete` was successful. +func (w *ServerOptions) Completed() *ServerConfig { + return w.config +} + +// AddFlags implements Flagger.AddFlags. +// TODO: (timuthy) This flag can be removed and added to ServerOptions as soons as we use Controller-Runtime v0.2.2 +// https://github.com/kubernetes-sigs/controller-runtime/pull/569 +func (w *ServerOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&w.CertDir, CertDirFlag, w.CertDir, "The directory that contains the webhook server key and certificate.") +} diff --git a/controllers/provider-aws/cmd/gardener-extension-validator-aws/main.go b/controllers/provider-aws/cmd/gardener-extension-validator-aws/main.go new file mode 100644 index 000000000..e81efe140 --- /dev/null +++ b/controllers/provider-aws/cmd/gardener-extension-validator-aws/main.go @@ -0,0 +1,33 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/gardener/gardener-extensions/controllers/provider-aws/cmd/gardener-extension-validator-aws/app" + "github.com/gardener/gardener-extensions/pkg/controller" + controllercmd "github.com/gardener/gardener-extensions/pkg/controller/cmd" + "github.com/gardener/gardener-extensions/pkg/log" + + runtimelog "sigs.k8s.io/controller-runtime/pkg/log" +) + +func main() { + runtimelog.SetLogger(log.ZapLogger(false)) + cmd := app.NewValidatorCommand(controller.SetupSignalHandlerContext()) + + if err := cmd.Execute(); err != nil { + controllercmd.LogErrAndExit(err, "error executing the main command") + } +} diff --git a/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml b/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml new file mode 100644 index 000000000..81e552e3e --- /dev/null +++ b/controllers/provider-aws/example/40-validatingwebhookconfiguration.yaml @@ -0,0 +1,20 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: gardener-extensions-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 c80a82a23..d61f22262 100644 --- a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go +++ b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,13 +15,50 @@ package validation import ( + "fmt" + apisaws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" cidrvalidation "github.com/gardener/gardener/pkg/utils/validation/cidr" apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" ) +// ValidateInfrastructureConfigAgainstCloudProfile validates the given `InfrastructureConfig` against the given `CloudProfile`. +func ValidateInfrastructureConfigAgainstCloudProfile(infra *apisaws.InfrastructureConfig, shoot *gardencorev1alpha1.Shoot, cloudProfile *gardencorev1alpha1.CloudProfile, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + shootRegion := shoot.Spec.Region + for _, region := range cloudProfile.Spec.Regions { + if region.Name == shootRegion { + allErrs = append(allErrs, validateInfrastructureConfigZones(infra, region.Zones, fldPath.Child("network"))...) + break + } + } + + return allErrs +} + +// validateInfrastructureConfigZones validates the given `InfrastructureConfig` against the given `Zones`. +func validateInfrastructureConfigZones(infra *apisaws.InfrastructureConfig, zones []gardencorev1alpha1.AvailabilityZone, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + awsZones := sets.NewString() + for _, awsZone := range zones { + awsZones.Insert(awsZone.Name) + } + + for i, zone := range infra.Networks.Zones { + if !awsZones.Has(zone.Name) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("zones").Index(i).Child("name"), zone.Name, fmt.Sprintf("supported values: %v", awsZones.UnsortedList()))) + } + } + + return allErrs +} + // ValidateInfrastructureConfig validates a InfrastructureConfig object. func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, nodesCIDR, podsCIDR, servicesCIDR *string) field.ErrorList { allErrs := field.ErrorList{} @@ -52,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...)...) @@ -93,10 +138,30 @@ func ValidateInfrastructureConfig(infra *apisaws.InfrastructureConfig, nodesCIDR } // ValidateInfrastructureConfigUpdate validates a InfrastructureConfig object. -func ValidateInfrastructureConfigUpdate(oldConfig, newConfig *apisaws.InfrastructureConfig, nodesCIDR, podsCIDR, servicesCIDR *string) field.ErrorList { +func ValidateInfrastructureConfigUpdate(oldConfig, newConfig *apisaws.InfrastructureConfig) 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 12c5de970..14f3acfe9 100644 --- a/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go +++ b/controllers/provider-aws/pkg/apis/aws/validation/infrastructure_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,12 +17,13 @@ package validation_test import ( apisaws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" . "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws/validation" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + "k8s.io/apimachinery/pkg/util/validation/field" . "github.com/gardener/gardener/pkg/utils/validation/gomega" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" - "k8s.io/apimachinery/pkg/util/validation/field" ) var _ = Describe("InfrastructureConfig validation", func() { @@ -34,6 +35,15 @@ var _ = Describe("InfrastructureConfig validation", func() { nodes = "10.250.0.0/16" 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() { @@ -44,7 +54,7 @@ var _ = Describe("InfrastructureConfig validation", func() { }, Zones: []apisaws.Zone{ { - Name: "zone1", + Name: zone, Internal: "10.250.1.0/24", Public: "10.250.2.0/24", Workers: "10.250.3.0/24", @@ -54,7 +64,97 @@ var _ = Describe("InfrastructureConfig validation", func() { } }) + Describe("#ValidateInfrastructureConfigAgainstCloudProfile", func() { + var ( + cloudProfile *gardencorev1alpha1.CloudProfile + shoot *gardencorev1alpha1.Shoot + region = "eu-west" + region2 = "us-west" + ) + Context("zones validation", func() { + BeforeEach(func() { + cloudProfile = &gardencorev1alpha1.CloudProfile{ + Spec: gardencorev1alpha1.CloudProfileSpec{ + Regions: []gardencorev1alpha1.Region{ + gardencorev1alpha1.Region{ + Name: region2, + Zones: []gardencorev1alpha1.AvailabilityZone{ + gardencorev1alpha1.AvailabilityZone{ + Name: zone2, + }, + gardencorev1alpha1.AvailabilityZone{ + Name: zone, + }, + }, + }, + gardencorev1alpha1.Region{ + Name: region, + Zones: []gardencorev1alpha1.AvailabilityZone{ + gardencorev1alpha1.AvailabilityZone{ + Name: zone2, + }, + gardencorev1alpha1.AvailabilityZone{ + Name: zone, + }, + }, + }, + }, + }, + } + shoot = &gardencorev1alpha1.Shoot{ + Spec: gardencorev1alpha1.ShootSpec{ + Region: region, + }, + } + }) + + It("should pass because zone is configured in CloudProfile", func() { + errorList := ValidateInfrastructureConfigAgainstCloudProfile(infrastructureConfig, shoot, cloudProfile, &field.Path{}) + + Expect(errorList).To(BeEmpty()) + }) + + It("should forbid because zone is not specified in CloudProfile", func() { + infrastructureConfig.Networks.Zones[0].Name = "not-available" + errorList := ValidateInfrastructureConfigAgainstCloudProfile(infrastructureConfig, shoot, cloudProfile, field.NewPath("spec")) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("spec.network.zones[0].name"), + })))) + }) + }) + }) + 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 @@ -234,19 +334,90 @@ var _ = Describe("InfrastructureConfig validation", func() { Describe("#ValidateInfrastructureConfigUpdate", func() { It("should return no errors for an unchanged config", func() { - Expect(ValidateInfrastructureConfigUpdate(infrastructureConfig, infrastructureConfig, &nodes, &pods, &services)).To(BeEmpty()) + Expect(ValidateInfrastructureConfigUpdate(infrastructureConfig, infrastructureConfig)).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) + + 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 - errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig, &nodes, &pods, &services) + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.vpc"), + })))) + }) + + It("should forbid changing the internal network of a zone", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones[0].Internal = awsZone2.Internal + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones[0]"), + })))) + }) + + It("should forbid changing the public network of a zone", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones[0].Public = awsZone2.Public + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones[0]"), + })))) + }) + + It("should forbid changing the workers network of a zone", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones[0].Workers = awsZone2.Workers + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig) + + 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) + + Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("networks.zones"), + })))) + }) + + It("should allow adding a zone but forbid removing a zone", func() { + newInfrastructureConfig := infrastructureConfig.DeepCopy() + newInfrastructureConfig.Networks.Zones = append(newInfrastructureConfig.Networks.Zones, awsZone2) + newInfrastructureConfig.Networks.Zones[0].Name = "zone3" + + errorList := ValidateInfrastructureConfigUpdate(infrastructureConfig, newInfrastructureConfig) Expect(errorList).To(ConsistOf(PointTo(MatchFields(IgnoreExtras, Fields{ "Type": Equal(field.ErrorTypeInvalid), - "Field": Equal("networks"), + "Field": Equal("networks.zones"), })))) }) }) diff --git a/controllers/provider-aws/pkg/apis/aws/validation/shoot.go b/controllers/provider-aws/pkg/apis/aws/validation/shoot.go new file mode 100644 index 000000000..af3dd84a3 --- /dev/null +++ b/controllers/provider-aws/pkg/apis/aws/validation/shoot.go @@ -0,0 +1,73 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation + +import ( + "fmt" + + apisaws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + "k8s.io/apimachinery/pkg/util/validation/field" + + "k8s.io/apimachinery/pkg/util/sets" +) + +// ValidateWorkers validates the workers of a Shoot. +func ValidateWorkers(workers []gardencorev1alpha1.Worker, zones []apisaws.Zone, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + awsZones := sets.NewString() + for _, awsZone := range zones { + awsZones.Insert(awsZone.Name) + } + + for i, worker := range workers { + if worker.Volume == nil { + allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("volume"), "must not be nil")) + } else { + allErrs = append(allErrs, validateVolume(worker.Volume, fldPath.Index(i).Child("volume"))...) + } + + if len(worker.Zones) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("zones"), "at least one zone must be configured")) + continue + } + + allErrs = append(allErrs, validateZones(worker.Zones, awsZones, fldPath.Index(i).Child("zones"))...) + } + + return allErrs +} + +func validateZones(zones []string, allowedZones sets.String, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + for i, workerZone := range zones { + if !allowedZones.Has(workerZone) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i), workerZone, fmt.Sprintf("supported values %v", allowedZones.UnsortedList()))) + } + } + return allErrs +} + +func validateVolume(vol *gardencorev1alpha1.Volume, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if vol.Type == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty")) + } + if vol.Size == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("size"), "must not be empty")) + } + return allErrs +} diff --git a/controllers/provider-aws/pkg/apis/aws/validation/shoot_test.go b/controllers/provider-aws/pkg/apis/aws/validation/shoot_test.go new file mode 100644 index 000000000..5d8797172 --- /dev/null +++ b/controllers/provider-aws/pkg/apis/aws/validation/shoot_test.go @@ -0,0 +1,145 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validation_test + +import ( + apisaws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" + . "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws/validation" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +var _ = Describe("ValidateWorkerConfig", func() { + var ( + workers []gardencorev1alpha1.Worker + awsZones []apisaws.Zone + ) + + BeforeEach(func() { + workers = []gardencorev1alpha1.Worker{ + gardencorev1alpha1.Worker{ + Volume: &gardencorev1alpha1.Volume{ + Type: makeStringPointer("Volume"), + Size: "30G", + }, + Zones: []string{ + "zone1", + "zone2", + }, + }, + gardencorev1alpha1.Worker{ + Volume: &gardencorev1alpha1.Volume{ + Type: makeStringPointer("Volume"), + Size: "20G", + }, + Zones: []string{ + "zone2", + "zone3", + }, + }, + } + + awsZones = []apisaws.Zone{ + apisaws.Zone{ + Name: "zone1", + }, + apisaws.Zone{ + Name: "zone2", + }, + apisaws.Zone{ + Name: "zone3", + }, + } + }) + + Describe("#ValidateWorkers", func() { + It("should pass because workers are configured correctly", func() { + errorList := ValidateWorkers(workers, awsZones, field.NewPath("")) + + Expect(errorList).To(BeEmpty()) + }) + + It("should forbid because volume is not configured", func() { + workers[1].Volume = nil + + errorList := ValidateWorkers(workers, awsZones, field.NewPath("workers")) + + Expect(errorList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("workers[1].volume"), + })), + )) + }) + + It("should forbid because volume type and size are not configured", func() { + workers[0].Volume.Type = nil + workers[0].Volume.Size = "" + + errorList := ValidateWorkers(workers, awsZones, field.NewPath("workers")) + + Expect(errorList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("workers[0].volume.type"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("workers[0].volume.size"), + })), + )) + }) + + It("should forbid because worker does not specify a zone", func() { + workers[0].Zones = nil + + errorList := ValidateWorkers(workers, awsZones, field.NewPath("workers")) + + Expect(errorList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("workers[0].zones"), + })), + )) + }) + + It("should forbid because worker use zones which are not available", func() { + workers[0].Zones[0] = "" + workers[1].Zones[1] = "not-available" + + errorList := ValidateWorkers(workers, awsZones, field.NewPath("workers")) + + Expect(errorList).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("workers[0].zones[0]"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("workers[1].zones[1]"), + })), + )) + }) + }) +}) + +func makeStringPointer(s string) *string { + ptr := s + return &ptr +} diff --git a/controllers/provider-aws/pkg/apis/aws/validation/worker.go b/controllers/provider-aws/pkg/apis/aws/validation/worker.go index 945fb0a4f..6003c4f33 100644 --- a/controllers/provider-aws/pkg/apis/aws/validation/worker.go +++ b/controllers/provider-aws/pkg/apis/aws/validation/worker.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package validation import ( apisaws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" - "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/controllers/provider-aws/pkg/validator/serialization.go b/controllers/provider-aws/pkg/validator/serialization.go new file mode 100644 index 000000000..f586f5ec4 --- /dev/null +++ b/controllers/provider-aws/pkg/validator/serialization.go @@ -0,0 +1,50 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validator + +import ( + "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws" + "github.com/gardener/gardener-extensions/pkg/util" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func decodeWorkerConfig(decoder runtime.Decoder, worker *gardencorev1alpha1.ProviderConfig, fldPath *field.Path) (*aws.WorkerConfig, error) { + workerConfig := &aws.WorkerConfig{} + if err := util.Decode(decoder, worker.Raw, workerConfig); err != nil { + return nil, field.Invalid(fldPath, string(worker.Raw), "isn't a supported version") + } + + return workerConfig, nil +} + +func decodeControlPlaneConfig(decoder runtime.Decoder, cp *gardencorev1alpha1.ProviderConfig, fldPath *field.Path) (*aws.ControlPlaneConfig, error) { + controlPlaneConfig := &aws.ControlPlaneConfig{} + if err := util.Decode(decoder, cp.Raw, controlPlaneConfig); err != nil { + return nil, field.Invalid(fldPath, string(cp.Raw), "isn't a supported version") + } + + return controlPlaneConfig, nil +} + +func decodeInfrastructureConfig(decoder runtime.Decoder, infra *gardencorev1alpha1.ProviderConfig, fldPath *field.Path) (*aws.InfrastructureConfig, error) { + infraConfig := &aws.InfrastructureConfig{} + if err := util.Decode(decoder, infra.Raw, infraConfig); err != nil { + return nil, field.Invalid(fldPath, string(infra.Raw), "isn't a supported version") + } + + return infraConfig, nil +} diff --git a/controllers/provider-aws/pkg/validator/shoot_handler.go b/controllers/provider-aws/pkg/validator/shoot_handler.go new file mode 100644 index 000000000..68478cdfa --- /dev/null +++ b/controllers/provider-aws/pkg/validator/shoot_handler.go @@ -0,0 +1,88 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validator + +import ( + "context" + "net/http" + + provideraws "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/aws" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// Shoot validates shoots +type Shoot struct { + client client.Client + decoder runtime.Decoder + Logger logr.Logger +} + +// Handle implements Handler.Handle +func (v *Shoot) Handle(ctx context.Context, req admission.Request) admission.Response { + shoot := &gardencorev1alpha1.Shoot{} + + _, _, err := v.decoder.Decode(req.Object.Raw, nil, shoot) + if err != nil { + v.Logger.Error(err, "failed to decode shoot", string(req.Object.Raw)) + return admission.Errored(http.StatusBadRequest, err) + } + + if shoot.Spec.Provider.Type != provideraws.Type { + return admission.Allowed("webhook not responsible for this provider") + } + + 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 { + v.Logger.Error(err, "failed to decode old shoot", string(req.OldObject.Raw)) + return admission.Errored(http.StatusBadRequest, err) + } + + if err := v.validateShootUpdate(ctx, oldShoot, 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") +} + +// InjectClient injects the client. +func (v *Shoot) InjectClient(c client.Client) error { + v.client = c + return nil +} + +// InjectScheme injects the scheme. +func (v *Shoot) InjectScheme(s *runtime.Scheme) error { + v.decoder = serializer.NewCodecFactory(s).UniversalDecoder() + return nil +} diff --git a/controllers/provider-aws/pkg/validator/shoot_validator.go b/controllers/provider-aws/pkg/validator/shoot_validator.go new file mode 100644 index 000000000..06a348e73 --- /dev/null +++ b/controllers/provider-aws/pkg/validator/shoot_validator.go @@ -0,0 +1,123 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validator + +import ( + "context" + "errors" + "reflect" + + awsvalidation "github.com/gardener/gardener-extensions/controllers/provider-aws/pkg/apis/aws/validation" + gardencorev1alpha1 "github.com/gardener/gardener/pkg/apis/core/v1alpha1" + kutil "github.com/gardener/gardener/pkg/utils/kubernetes" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func (v *Shoot) validateShoot(ctx context.Context, shoot *gardencorev1alpha1.Shoot) error { + fldPath := field.NewPath("spec", "provider") + + // InfrastructureConfig + infraConfigFldPath := fldPath.Child("infrastructureConfig") + if shoot.Spec.Provider.InfrastructureConfig == nil { + return field.Required(infraConfigFldPath, "InfrastructureConfig must be set for AWS shoots") + } + + infraConfig, err := decodeInfrastructureConfig(v.decoder, shoot.Spec.Provider.InfrastructureConfig, infraConfigFldPath) + if err != nil { + return err + } + + errList := awsvalidation.ValidateInfrastructureConfig(infraConfig, &shoot.Spec.Networking.Nodes, shoot.Spec.Networking.Pods, shoot.Spec.Networking.Services) + if len(errList) != 0 { + return errList.ToAggregate() + } + + cloudProfile := &gardencorev1alpha1.CloudProfile{} + if err := v.client.Get(ctx, kutil.Key(shoot.Spec.CloudProfileName), cloudProfile); err != nil { + return err + } + + errList = awsvalidation.ValidateInfrastructureConfigAgainstCloudProfile(infraConfig, shoot, cloudProfile, infraConfigFldPath) + if len(errList) != 0 { + return errList.ToAggregate() + } + + // ControlPlaneConfig + if shoot.Spec.Provider.ControlPlaneConfig != nil { + _, err = decodeControlPlaneConfig(v.decoder, shoot.Spec.Provider.ControlPlaneConfig, fldPath.Child("controlPlaneConfig")) + if err != nil { + return err + } + } + + // WorkerConfig + fldPath = fldPath.Child("workers") + for i, worker := range shoot.Spec.Provider.Workers { + if worker.ProviderConfig != nil { + workerConfig, err := decodeWorkerConfig(v.decoder, worker.ProviderConfig, fldPath.Index(i).Child("providerConfig")) + if err != nil { + return err + } + + var volumeType *string + if worker.Volume != nil { + volumeType = worker.Volume.Type + } + errList := awsvalidation.ValidateWorkerConfig(workerConfig, volumeType) + if len(errList) != 0 { + return errList.ToAggregate() + } + } + } + + // Shoot workers + errList = awsvalidation.ValidateWorkers(shoot.Spec.Provider.Workers, infraConfig.Networks.Zones, fldPath) + if len(errList) != 0 { + return errList.ToAggregate() + } + return nil +} + +func (v *Shoot) validateShootUpdate(ctx context.Context, oldShoot, shoot *gardencorev1alpha1.Shoot) error { + fldPath := field.NewPath("spec", "provider") + + // InfrastructureConfig update + if shoot.Spec.Provider.InfrastructureConfig == nil { + return field.Required(fldPath.Child("infrastructureConfig"), "InfrastructureConfig must be set for AWS shoots") + } + + infraConfig, err := decodeInfrastructureConfig(v.decoder, shoot.Spec.Provider.InfrastructureConfig, fldPath) + if err != nil { + return err + } + + if oldShoot.Spec.Provider.InfrastructureConfig == nil { + return field.InternalError(fldPath.Child("infrastructureConfig"), errors.New("InfrastructureConfig is not available on old shoot")) + } + + oldInfraConfig, err := decodeInfrastructureConfig(v.decoder, oldShoot.Spec.Provider.InfrastructureConfig, fldPath) + if err != nil { + return err + } + + if !reflect.DeepEqual(oldInfraConfig, infraConfig) { + errList := awsvalidation.ValidateInfrastructureConfigUpdate(oldInfraConfig, infraConfig) + if len(errList) != 0 { + return errList.ToAggregate() + } + } + + return v.validateShoot(ctx, shoot) +} diff --git a/pkg/controller/cmd/options.go b/pkg/controller/cmd/options.go index 984208ff3..864d01ef2 100644 --- a/pkg/controller/cmd/options.go +++ b/pkg/controller/cmd/options.go @@ -156,6 +156,8 @@ type ManagerOptions struct { } // AddFlags implements Flagger.AddFlags. +// TODO: (timuthy) Add certDir flag as soon as Controller-Runtime v0.2.2 is used. +// https://github.com/kubernetes-sigs/controller-runtime/pull/569 func (m *ManagerOptions) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&m.LeaderElection, LeaderElectionFlag, m.LeaderElection, "Whether to use leader election or not when running this controller manager.") fs.StringVar(&m.LeaderElectionID, LeaderElectionIDFlag, m.LeaderElectionID, "The leader election id to use.") diff --git a/pkg/util/serialization.go b/pkg/util/serialization.go new file mode 100644 index 000000000..12600a7be --- /dev/null +++ b/pkg/util/serialization.go @@ -0,0 +1,43 @@ +// Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" +) + +// Decode takes a `decoder` and decodes the provided `data` into the provided object. +// The underlying `into` address is used to assign the decoded object. +func Decode(decoder runtime.Decoder, data []byte, into runtime.Object) error { + // By not providing an `into` it is necessary that the serialized `data` is configured with + // a proper `apiVersion` and `kind` field. This also makes sure that the conversion logic to + // the internal version is called. + output, _, err := decoder.Decode(data, nil, nil) + if err != nil { + return err + } + + intoType := reflect.TypeOf(into) + + if reflect.TypeOf(output) == intoType { + reflect.ValueOf(into).Elem().Set(reflect.ValueOf(output).Elem()) + return nil + } + + return fmt.Errorf("is not of type %s", intoType) +}