From 3fcf453d10605d017e32b40bf5f9142677455d52 Mon Sep 17 00:00:00 2001 From: Izabela Gomes Date: Mon, 22 Apr 2024 18:09:42 -0400 Subject: [PATCH] add validating webhook for config-gateway Config Map (#705) * add validation webhook for configmap * add EOF new line in yaml files * run ./hack/update-deps.sh * rename config-gateway yaml file * renamed yaml files * add object selector labels to config-gateway in contour folder --- cmd/webhook/main.go | 61 +++++ .../{controller.yaml => 300-controller.yaml} | 0 config/400-webhook-deployment.yaml | 103 ++++++++ config/400-webhook-secret.yaml | 24 ++ config/400-webhook-service.yaml | 39 +++ config/500-validating-webhook.yaml | 38 +++ third_party/contour/config-gateway.yaml | 2 + .../validatingwebhookconfiguration.go | 52 ++++ .../informers/core/v1/secret/secret.go | 50 ++++ .../pkg/webhook/certificates/certificates.go | 108 ++++++++ .../pkg/webhook/certificates/controller.go | 81 ++++++ .../pkg/webhook/configmaps/configmaps.go | 231 ++++++++++++++++++ .../pkg/webhook/configmaps/controller.go | 96 ++++++++ vendor/modules.txt | 4 + 14 files changed, 889 insertions(+) create mode 100644 cmd/webhook/main.go rename config/{controller.yaml => 300-controller.yaml} (100%) create mode 100644 config/400-webhook-deployment.yaml create mode 100644 config/400-webhook-secret.yaml create mode 100644 config/400-webhook-service.yaml create mode 100644 config/500-validating-webhook.yaml create mode 100644 vendor/knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration/validatingwebhookconfiguration.go create mode 100644 vendor/knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/secret.go create mode 100644 vendor/knative.dev/pkg/webhook/certificates/certificates.go create mode 100644 vendor/knative.dev/pkg/webhook/certificates/controller.go create mode 100644 vendor/knative.dev/pkg/webhook/configmaps/configmaps.go create mode 100644 vendor/knative.dev/pkg/webhook/configmaps/controller.go diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go new file mode 100644 index 000000000..5108f01cb --- /dev/null +++ b/cmd/webhook/main.go @@ -0,0 +1,61 @@ +/* +Copyright 2024 The Knative Authors + +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 ( + "context" + + gatewayapiconfig "knative.dev/net-gateway-api/pkg/reconciler/ingress/config" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + "knative.dev/pkg/injection/sharedmain" + "knative.dev/pkg/signals" + "knative.dev/pkg/webhook" + "knative.dev/pkg/webhook/certificates" + "knative.dev/pkg/webhook/configmaps" +) + +func NewConfigValidationController(ctx context.Context, _ configmap.Watcher) *controller.Impl { + return configmaps.NewAdmissionController(ctx, + + // Name of the resource webhook. + "config.webhook.gateway-api.networking.internal.knative.dev", + + // The path on which to serve the webhook. + "/config-validation", + + // The configmaps to validate. + configmap.Constructors{ + gatewayapiconfig.GatewayConfigName: gatewayapiconfig.FromConfigMap, + }, + ) +} + +func main() { + ctx := webhook.WithOptions(signals.NewContext(), webhook.Options{ + ServiceName: "net-gateway-api-webhook", + SecretName: "net-gateway-api-webhook-certs", + Port: webhook.PortFromEnv(8443), + }) + + ctx = sharedmain.WithHealthProbesDisabled(ctx) + sharedmain.WebhookMainWithContext( + ctx, "net-gateway-api-webhook", + certificates.NewController, + NewConfigValidationController, + ) +} diff --git a/config/controller.yaml b/config/300-controller.yaml similarity index 100% rename from config/controller.yaml rename to config/300-controller.yaml diff --git a/config/400-webhook-deployment.yaml b/config/400-webhook-deployment.yaml new file mode 100644 index 000000000..d861949f8 --- /dev/null +++ b/config/400-webhook-deployment.yaml @@ -0,0 +1,103 @@ +# Copyright 2024 The Knative Authors +# +# 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 +# +# https://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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: net-gateway-api-webhook + namespace: knative-serving + labels: + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: devel + networking.knative.dev/ingress-provider: gateway-api +spec: + selector: + matchLabels: + app: net-gateway-api-webhook + role: net-gateway-api-webhook + template: + metadata: + labels: + app: net-gateway-api-webhook + role: net-gateway-api-webhook + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: devel + spec: + serviceAccountName: controller + containers: + - name: webhook + # This is the Go import path for the binary that is containerized + # and substituted here. + image: ko://knative.dev/net-gateway-api/cmd/webhook + + resources: + requests: + cpu: 20m + memory: 20Mi + limits: + cpu: 200m + memory: 200Mi + + env: + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: CONFIG_OBSERVABILITY_NAME + value: config-observability + + # TODO(https://github.com/knative/pkg/pull/953): Remove stackdriver specific config + - name: METRICS_DOMAIN + value: knative.dev/net-gateway-api + - name: WEBHOOK_NAME + value: net-gateway-api-webhook + # If you change WEBHOOK_PORT, you will also need to change the + # containerPort "https-webhook" to the same value. + - name: WEBHOOK_PORT + value: "8443" + + securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + readinessProbe: + periodSeconds: 1 + httpGet: + scheme: HTTPS + port: 8443 + failureThreshold: 3 + livenessProbe: + periodSeconds: 1 + httpGet: + scheme: HTTPS + port: 8443 + failureThreshold: 6 + initialDelaySeconds: 20 + + ports: + - name: metrics + containerPort: 9090 + - name: profiling + containerPort: 8008 + - name: https-webhook + containerPort: 8443 diff --git a/config/400-webhook-secret.yaml b/config/400-webhook-secret.yaml new file mode 100644 index 000000000..1d5e6883a --- /dev/null +++ b/config/400-webhook-secret.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 The Knative Authors +# +# 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 +# +# https://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. + +apiVersion: v1 +kind: Secret +metadata: + name: net-gateway-api-webhook-certs + namespace: knative-serving + labels: + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: devel + networking.knative.dev/ingress-provider: gateway-api diff --git a/config/400-webhook-service.yaml b/config/400-webhook-service.yaml new file mode 100644 index 000000000..1e37ff230 --- /dev/null +++ b/config/400-webhook-service.yaml @@ -0,0 +1,39 @@ +# Copyright 2024 The Knative Authors +# +# 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 +# +# https://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. + +apiVersion: v1 +kind: Service +metadata: + name: net-gateway-api-webhook + namespace: knative-serving + labels: + role: net-gateway-api-webhook + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: devel + networking.knative.dev/ingress-provider: gateway-api +spec: + ports: + # Define metrics and profiling for them to be accessible within service meshes. + - name: http-metrics + port: 9090 + targetPort: metrics + - name: http-profiling + port: 8008 + targetPort: profiling + - name: https-webhook + port: 443 + targetPort: https-webhook + selector: + app: net-gateway-api-webhook diff --git a/config/500-validating-webhook.yaml b/config/500-validating-webhook.yaml new file mode 100644 index 000000000..388ee8d86 --- /dev/null +++ b/config/500-validating-webhook.yaml @@ -0,0 +1,38 @@ +# Copyright 2024 The Knative Authors +# +# 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 +# +# https://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. + +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: config.webhook.gateway-api.networking.internal.knative.dev + labels: + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: devel + networking.knative.dev/ingress-provider: gateway-api +webhooks: + - admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: net-gateway-api-webhook + namespace: knative-serving + failurePolicy: Fail + sideEffects: None + name: config.webhook.gateway-api.networking.internal.knative.dev + objectSelector: + matchLabels: + app.kubernetes.io/name: knative-serving + app.kubernetes.io/component: net-gateway-api diff --git a/third_party/contour/config-gateway.yaml b/third_party/contour/config-gateway.yaml index 7aa9db95e..4505d9c34 100644 --- a/third_party/contour/config-gateway.yaml +++ b/third_party/contour/config-gateway.yaml @@ -18,6 +18,8 @@ metadata: name: config-gateway namespace: knative-serving labels: + app.kubernetes.io/component: net-gateway-api + app.kubernetes.io/name: knative-serving serving.knative.dev/release: devel data: external-gateways: | diff --git a/vendor/knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration/validatingwebhookconfiguration.go b/vendor/knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration/validatingwebhookconfiguration.go new file mode 100644 index 000000000..e8a23f717 --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration/validatingwebhookconfiguration.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 The Knative Authors + +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. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package validatingwebhookconfiguration + +import ( + context "context" + + v1 "k8s.io/client-go/informers/admissionregistration/v1" + factory "knative.dev/pkg/client/injection/kube/informers/factory" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterInformer(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct{} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := factory.Get(ctx) + inf := f.Admissionregistration().V1().ValidatingWebhookConfigurations() + return context.WithValue(ctx, Key{}, inf), inf.Informer() +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) v1.ValidatingWebhookConfigurationInformer { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/informers/admissionregistration/v1.ValidatingWebhookConfigurationInformer from context.") + } + return untyped.(v1.ValidatingWebhookConfigurationInformer) +} diff --git a/vendor/knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/secret.go b/vendor/knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/secret.go new file mode 100644 index 000000000..90d70627f --- /dev/null +++ b/vendor/knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/secret.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 The Knative Authors + +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 secret + +import ( + context "context" + + v1 "k8s.io/client-go/informers/core/v1" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + factory "knative.dev/pkg/injection/clients/namespacedkube/informers/factory" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterInformer(withInformer) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct{} + +func withInformer(ctx context.Context) (context.Context, controller.Informer) { + f := factory.Get(ctx) + inf := f.Core().V1().Secrets() + return context.WithValue(ctx, Key{}, inf), inf.Informer() +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) v1.SecretInformer { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/informers/core/v1.SecretInformer from context.") + } + return untyped.(v1.SecretInformer) +} diff --git a/vendor/knative.dev/pkg/webhook/certificates/certificates.go b/vendor/knative.dev/pkg/webhook/certificates/certificates.go new file mode 100644 index 000000000..5239279e5 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/certificates/certificates.go @@ -0,0 +1,108 @@ +/* +Copyright 2019 The Knative Authors + +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 certificates + +import ( + "context" + "crypto/tls" + "crypto/x509" + "time" + + "go.uber.org/zap" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + certresources "knative.dev/pkg/webhook/certificates/resources" +) + +const ( + // Time used for updating a certificate before it expires. + oneDay = 24 * time.Hour +) + +type reconciler struct { + pkgreconciler.LeaderAwareFuncs + + client kubernetes.Interface + secretlister corelisters.SecretLister + key types.NamespacedName + serviceName string +} + +var _ controller.Reconciler = (*reconciler)(nil) +var _ pkgreconciler.LeaderAware = (*reconciler)(nil) + +// Reconcile implements controller.Reconciler +func (r *reconciler) Reconcile(ctx context.Context, key string) error { + if r.IsLeaderFor(r.key) { + // only reconciler the certificate when we are leader. + return r.reconcileCertificate(ctx) + } + return controller.NewSkipKey(key) +} + +func (r *reconciler) reconcileCertificate(ctx context.Context) error { + logger := logging.FromContext(ctx) + + secret, err := r.secretlister.Secrets(r.key.Namespace).Get(r.key.Name) + if apierrors.IsNotFound(err) { + // The secret should be created explicitly by a higher-level system + // that's responsible for install/updates. We simply populate the + // secret information. + return nil + } else if err != nil { + logger.Errorf("Error accessing certificate secret %q: %v", r.key.Name, err) + return err + } + + if _, haskey := secret.Data[certresources.ServerKey]; !haskey { + logger.Infof("Certificate secret %q is missing key %q", r.key.Name, certresources.ServerKey) + } else if _, haskey := secret.Data[certresources.ServerCert]; !haskey { + logger.Infof("Certificate secret %q is missing key %q", r.key.Name, certresources.ServerCert) + } else if _, haskey := secret.Data[certresources.CACert]; !haskey { + logger.Infof("Certificate secret %q is missing key %q", r.key.Name, certresources.CACert) + } else { + // Check the expiration date of the certificate to see if it needs to be updated + cert, err := tls.X509KeyPair(secret.Data[certresources.ServerCert], secret.Data[certresources.ServerKey]) + if err != nil { + logger.Warnw("Error creating pem from certificate and key", zap.Error(err)) + } else { + certData, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + logger.Errorw("Error parsing certificate", zap.Error(err)) + } else if time.Now().Add(oneDay).Before(certData.NotAfter) { + return nil + } + } + } + // Don't modify the informer copy. + secret = secret.DeepCopy() + + // One of the secret's keys is missing, so synthesize a new one and update the secret. + newSecret, err := certresources.MakeSecret(ctx, r.key.Name, r.key.Namespace, r.serviceName) + if err != nil { + return err + } + secret.Data = newSecret.Data + _, err = r.client.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{}) + return err +} diff --git a/vendor/knative.dev/pkg/webhook/certificates/controller.go b/vendor/knative.dev/pkg/webhook/certificates/controller.go new file mode 100644 index 000000000..1932e28c2 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/certificates/controller.go @@ -0,0 +1,81 @@ +/* +Copyright 2019 The Knative Authors + +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 certificates + +import ( + "context" + + // Injection stuff + kubeclient "knative.dev/pkg/client/injection/kube/client" + secretinformer "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + "knative.dev/pkg/system" + "knative.dev/pkg/webhook" +) + +// NewController constructs a controller for materializing webhook certificates. +// In order for it to bootstrap, an empty secret should be created with the +// expected name (and lifecycle managed accordingly), and thereafter this controller +// will ensure it has the appropriate shape for the webhook. +func NewController( + ctx context.Context, + cmw configmap.Watcher, +) *controller.Impl { + + client := kubeclient.Get(ctx) + secretInformer := secretinformer.Get(ctx) + options := webhook.GetOptions(ctx) + + key := types.NamespacedName{ + Namespace: system.Namespace(), + Name: options.SecretName, + } + + wh := &reconciler{ + LeaderAwareFuncs: pkgreconciler.LeaderAwareFuncs{ + // Enqueue the key whenever we become leader. + PromoteFunc: func(bkt pkgreconciler.Bucket, enq func(pkgreconciler.Bucket, types.NamespacedName)) error { + enq(bkt, key) + return nil + }, + }, + key: key, + serviceName: options.ServiceName, + + client: client, + secretlister: secretInformer.Lister(), + } + + const queueName = "WebhookCertificates" + c := controller.NewContext(ctx, wh, controller.ControllerOptions{WorkQueueName: queueName, Logger: logging.FromContext(ctx).Named(queueName)}) + + // Reconcile when the cert bundle changes. + secretInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterWithNameAndNamespace(key.Namespace, key.Name), + // It doesn't matter what we enqueue because we will always Reconcile + // the named MWH resource. + Handler: controller.HandleAll(c.Enqueue), + }) + + return c +} diff --git a/vendor/knative.dev/pkg/webhook/configmaps/configmaps.go b/vendor/knative.dev/pkg/webhook/configmaps/configmaps.go new file mode 100644 index 000000000..5a4c4d888 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/configmaps/configmaps.go @@ -0,0 +1,231 @@ +/* +Copyright 2019 The Knative Authors + +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 configmaps + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + + "go.uber.org/zap" + admissionv1 "k8s.io/api/admission/v1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + admissionlisters "k8s.io/client-go/listers/admissionregistration/v1" + corelisters "k8s.io/client-go/listers/core/v1" + + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + "knative.dev/pkg/kmp" + "knative.dev/pkg/logging" + "knative.dev/pkg/ptr" + pkgreconciler "knative.dev/pkg/reconciler" + "knative.dev/pkg/system" + "knative.dev/pkg/webhook" + certresources "knative.dev/pkg/webhook/certificates/resources" +) + +// reconciler implements the AdmissionController for ConfigMaps +type reconciler struct { + webhook.StatelessAdmissionImpl + pkgreconciler.LeaderAwareFuncs + + key types.NamespacedName + path string + constructors map[string]reflect.Value + + client kubernetes.Interface + vwhlister admissionlisters.ValidatingWebhookConfigurationLister + secretlister corelisters.SecretLister + + secretName string +} + +var _ controller.Reconciler = (*reconciler)(nil) +var _ pkgreconciler.LeaderAware = (*reconciler)(nil) +var _ webhook.AdmissionController = (*reconciler)(nil) +var _ webhook.StatelessAdmissionController = (*reconciler)(nil) + +// Reconcile implements controller.Reconciler +func (ac *reconciler) Reconcile(ctx context.Context, key string) error { + logger := logging.FromContext(ctx) + + if !ac.IsLeaderFor(ac.key) { + return controller.NewSkipKey(key) + } + + secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName) + if err != nil { + logger.Errorw("Error fetching secret ", zap.Error(err)) + return err + } + + caCert, ok := secret.Data[certresources.CACert] + if !ok { + return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert) + } + + return ac.reconcileValidatingWebhook(ctx, caCert) +} + +// Path implements AdmissionController +func (ac *reconciler) Path() string { + return ac.path +} + +// Admit implements AdmissionController +func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { + logger := logging.FromContext(ctx) + switch request.Operation { + case admissionv1.Create, admissionv1.Update: + default: + logger.Info("Unhandled webhook operation, letting it through ", request.Operation) + return &admissionv1.AdmissionResponse{Allowed: true} + } + + if err := ac.validate(ctx, request); err != nil { + return webhook.MakeErrorStatus("validation failed: %v", err) + } + + return &admissionv1.AdmissionResponse{ + Allowed: true, + } +} + +func (ac *reconciler) reconcileValidatingWebhook(ctx context.Context, caCert []byte) error { + logger := logging.FromContext(ctx) + + ruleScope := admissionregistrationv1.NamespacedScope + rules := []admissionregistrationv1.RuleWithOperations{{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + admissionregistrationv1.Update, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"configmaps/*"}, + Scope: &ruleScope, + }, + }} + + configuredWebhook, err := ac.vwhlister.Get(ac.key.Name) + if err != nil { + return fmt.Errorf("error retrieving webhook: %w", err) + } + + webhook := configuredWebhook.DeepCopy() + + // Set the owner to namespace. + ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to fetch namespace: %w", err) + } + nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace")) + webhook.OwnerReferences = []metav1.OwnerReference{nsRef} + + for i, wh := range webhook.Webhooks { + if wh.Name != webhook.Name { + continue + } + webhook.Webhooks[i].Rules = rules + webhook.Webhooks[i].ClientConfig.CABundle = caCert + if webhook.Webhooks[i].ClientConfig.Service == nil { + return errors.New("missing service reference for webhook: " + wh.Name) + } + webhook.Webhooks[i].ClientConfig.Service.Path = ptr.String(ac.Path()) + } + + if ok, err := kmp.SafeEqual(configuredWebhook, webhook); err != nil { + return fmt.Errorf("error diffing webhooks: %w", err) + } else if !ok { + logger.Info("Updating webhook") + vwhclient := ac.client.AdmissionregistrationV1().ValidatingWebhookConfigurations() + if _, err := vwhclient.Update(ctx, webhook, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update webhook: %w", err) + } + } else { + logger.Info("Webhook is valid") + } + + return nil +} + +func (ac *reconciler) validate(ctx context.Context, req *admissionv1.AdmissionRequest) error { + logger := logging.FromContext(ctx) + kind := req.Kind + newBytes := req.Object.Raw + + // Why, oh why are these different types... + gvk := schema.GroupVersionKind{ + Group: kind.Group, + Version: kind.Version, + Kind: kind.Kind, + } + + resourceGVK := corev1.SchemeGroupVersion.WithKind("ConfigMap") + if gvk != resourceGVK { + logger.Error("Unhandled kind: ", gvk) + return fmt.Errorf("unhandled kind: %v", gvk) + } + + var newObj corev1.ConfigMap + if len(newBytes) != 0 { + if err := json.Unmarshal(newBytes, &newObj); err != nil { + return fmt.Errorf("cannot decode incoming new object: %w", err) + } + } + + if constructor, ok := ac.constructors[newObj.Name]; ok { + // Only validate example data if this is a configMap we know about. + exampleData, hasExampleData := newObj.Data[configmap.ExampleKey] + exampleChecksum, hasExampleChecksumAnnotation := newObj.Annotations[configmap.ExampleChecksumAnnotation] + if hasExampleData && hasExampleChecksumAnnotation && + exampleChecksum != configmap.Checksum(exampleData) { + return fmt.Errorf( + "the update modifies a key in %q which is probably not what you want. Instead, copy the respective setting to the top-level of the ConfigMap, directly below %q", + configmap.ExampleKey, "data") + } + + inputs := []reflect.Value{ + reflect.ValueOf(&newObj), + } + + outputs := constructor.Call(inputs) + errVal := outputs[1] + + if !errVal.IsNil() { + return errVal.Interface().(error) + } + } + + return nil +} + +func (ac *reconciler) registerConfig(name string, constructor interface{}) { + if err := configmap.ValidateConstructor(constructor); err != nil { + panic(err) + } + + ac.constructors[name] = reflect.ValueOf(constructor) +} diff --git a/vendor/knative.dev/pkg/webhook/configmaps/controller.go b/vendor/knative.dev/pkg/webhook/configmaps/controller.go new file mode 100644 index 000000000..c2d71eb03 --- /dev/null +++ b/vendor/knative.dev/pkg/webhook/configmaps/controller.go @@ -0,0 +1,96 @@ +/* +Copyright 2019 The Knative Authors + +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 configmaps + +import ( + "context" + "reflect" + + // Injection stuff + kubeclient "knative.dev/pkg/client/injection/kube/client" + vwhinformer "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration" + secretinformer "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + "knative.dev/pkg/system" + "knative.dev/pkg/webhook" +) + +// NewAdmissionController constructs a reconciler +func NewAdmissionController( + ctx context.Context, + name, path string, + constructors configmap.Constructors, +) *controller.Impl { + + client := kubeclient.Get(ctx) + vwhInformer := vwhinformer.Get(ctx) + secretInformer := secretinformer.Get(ctx) + options := webhook.GetOptions(ctx) + + key := types.NamespacedName{Name: name} + + wh := &reconciler{ + LeaderAwareFuncs: pkgreconciler.LeaderAwareFuncs{ + // Have this reconciler enqueue our singleton whenever it becomes leader. + PromoteFunc: func(bkt pkgreconciler.Bucket, enq func(pkgreconciler.Bucket, types.NamespacedName)) error { + enq(bkt, key) + return nil + }, + }, + + key: key, + path: path, + + constructors: make(map[string]reflect.Value), + secretName: options.SecretName, + + client: client, + vwhlister: vwhInformer.Lister(), + secretlister: secretInformer.Lister(), + } + + for configName, constructor := range constructors { + wh.registerConfig(configName, constructor) + } + + const queueName = "ConfigMapWebhook" + c := controller.NewContext(ctx, wh, controller.ControllerOptions{WorkQueueName: queueName, Logger: logging.FromContext(ctx).Named(queueName)}) + + // Reconcile when the named ValidatingWebhookConfiguration changes. + vwhInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterWithName(name), + // It doesn't matter what we enqueue because we will always Reconcile + // the named VWH resource. + Handler: controller.HandleAll(c.Enqueue), + }) + + // Reconcile when the cert bundle changes. + secretInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterWithNameAndNamespace(system.Namespace(), wh.secretName), + // It doesn't matter what we enqueue because we will always Reconcile + // the named VWH resource. + Handler: controller.HandleAll(c.Enqueue), + }) + + return c +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 198ba3171..60a456cd9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -943,6 +943,7 @@ knative.dev/pkg/apis/duck/v1 knative.dev/pkg/changeset knative.dev/pkg/client/injection/kube/client knative.dev/pkg/client/injection/kube/client/fake +knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration knative.dev/pkg/client/injection/kube/informers/core/v1/endpoints knative.dev/pkg/client/injection/kube/informers/core/v1/endpoints/fake knative.dev/pkg/client/injection/kube/informers/core/v1/pod @@ -960,6 +961,7 @@ knative.dev/pkg/environment knative.dev/pkg/hack knative.dev/pkg/hash knative.dev/pkg/injection +knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret knative.dev/pkg/injection/clients/namespacedkube/informers/factory knative.dev/pkg/injection/sharedmain knative.dev/pkg/kflag @@ -999,7 +1001,9 @@ knative.dev/pkg/tracing/propagation/tracecontextb3 knative.dev/pkg/tracker knative.dev/pkg/version knative.dev/pkg/webhook +knative.dev/pkg/webhook/certificates knative.dev/pkg/webhook/certificates/resources +knative.dev/pkg/webhook/configmaps # sigs.k8s.io/gateway-api v1.0.0 ## explicit; go 1.21 sigs.k8s.io/gateway-api/apis/v1