knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/configmaps/configmaps.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package configmaps
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"reflect"
    25  
    26  	"go.uber.org/zap"
    27  	admissionv1 "k8s.io/api/admission/v1"
    28  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/client-go/kubernetes"
    34  	admissionlisters "k8s.io/client-go/listers/admissionregistration/v1"
    35  	corelisters "k8s.io/client-go/listers/core/v1"
    36  
    37  	"knative.dev/pkg/configmap"
    38  	"knative.dev/pkg/controller"
    39  	"knative.dev/pkg/kmp"
    40  	"knative.dev/pkg/logging"
    41  	"knative.dev/pkg/ptr"
    42  	pkgreconciler "knative.dev/pkg/reconciler"
    43  	"knative.dev/pkg/system"
    44  	"knative.dev/pkg/webhook"
    45  	certresources "knative.dev/pkg/webhook/certificates/resources"
    46  )
    47  
    48  // reconciler implements the AdmissionController for ConfigMaps
    49  type reconciler struct {
    50  	webhook.StatelessAdmissionImpl
    51  	pkgreconciler.LeaderAwareFuncs
    52  
    53  	key          types.NamespacedName
    54  	path         string
    55  	constructors map[string]reflect.Value
    56  
    57  	client       kubernetes.Interface
    58  	vwhlister    admissionlisters.ValidatingWebhookConfigurationLister
    59  	secretlister corelisters.SecretLister
    60  
    61  	secretName                string
    62  	disableNamespaceOwnership bool
    63  }
    64  
    65  var (
    66  	_ controller.Reconciler                = (*reconciler)(nil)
    67  	_ pkgreconciler.LeaderAware            = (*reconciler)(nil)
    68  	_ webhook.AdmissionController          = (*reconciler)(nil)
    69  	_ webhook.StatelessAdmissionController = (*reconciler)(nil)
    70  )
    71  
    72  // Reconcile implements controller.Reconciler
    73  func (ac *reconciler) Reconcile(ctx context.Context, key string) error {
    74  	logger := logging.FromContext(ctx)
    75  
    76  	if !ac.IsLeaderFor(ac.key) {
    77  		return controller.NewSkipKey(key)
    78  	}
    79  
    80  	secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName)
    81  	if err != nil {
    82  		logger.Errorw("Error fetching secret ", zap.Error(err))
    83  		return err
    84  	}
    85  
    86  	caCert, ok := secret.Data[certresources.CACert]
    87  	if !ok {
    88  		return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert)
    89  	}
    90  
    91  	return ac.reconcileValidatingWebhook(ctx, caCert)
    92  }
    93  
    94  // Path implements AdmissionController
    95  func (ac *reconciler) Path() string {
    96  	return ac.path
    97  }
    98  
    99  // Admit implements AdmissionController
   100  func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
   101  	logger := logging.FromContext(ctx)
   102  	switch request.Operation {
   103  	case admissionv1.Create, admissionv1.Update:
   104  	default:
   105  		logger.Info("Unhandled webhook operation, letting it through ", request.Operation)
   106  		return &admissionv1.AdmissionResponse{Allowed: true}
   107  	}
   108  
   109  	if err := ac.validate(ctx, request); err != nil {
   110  		return webhook.MakeErrorStatus("validation failed: %v", err)
   111  	}
   112  
   113  	return &admissionv1.AdmissionResponse{
   114  		Allowed: true,
   115  	}
   116  }
   117  
   118  func (ac *reconciler) reconcileValidatingWebhook(ctx context.Context, caCert []byte) error {
   119  	logger := logging.FromContext(ctx)
   120  
   121  	ruleScope := admissionregistrationv1.NamespacedScope
   122  	rules := []admissionregistrationv1.RuleWithOperations{{
   123  		Operations: []admissionregistrationv1.OperationType{
   124  			admissionregistrationv1.Create,
   125  			admissionregistrationv1.Update,
   126  		},
   127  		Rule: admissionregistrationv1.Rule{
   128  			APIGroups:   []string{""},
   129  			APIVersions: []string{"v1"},
   130  			Resources:   []string{"configmaps/*"},
   131  			Scope:       &ruleScope,
   132  		},
   133  	}}
   134  
   135  	configuredWebhook, err := ac.vwhlister.Get(ac.key.Name)
   136  	if err != nil {
   137  		return fmt.Errorf("error retrieving webhook: %w", err)
   138  	}
   139  
   140  	webhook := configuredWebhook.DeepCopy()
   141  
   142  	if !ac.disableNamespaceOwnership {
   143  		// Set the owner to namespace.
   144  		ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{})
   145  		if err != nil {
   146  			return fmt.Errorf("failed to fetch namespace: %w", err)
   147  		}
   148  		nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace"))
   149  		nsRef.Controller = ptr.Bool(false)
   150  		webhook.OwnerReferences = []metav1.OwnerReference{nsRef}
   151  	}
   152  
   153  	for i, wh := range webhook.Webhooks {
   154  		if wh.Name != webhook.Name {
   155  			continue
   156  		}
   157  		webhook.Webhooks[i].Rules = rules
   158  		webhook.Webhooks[i].ClientConfig.CABundle = caCert
   159  		if webhook.Webhooks[i].ClientConfig.Service == nil {
   160  			return errors.New("missing service reference for webhook: " + wh.Name)
   161  		}
   162  		webhook.Webhooks[i].ClientConfig.Service.Path = ptr.String(ac.Path())
   163  	}
   164  
   165  	if ok, err := kmp.SafeEqual(configuredWebhook, webhook); err != nil {
   166  		return fmt.Errorf("error diffing webhooks: %w", err)
   167  	} else if !ok {
   168  		logger.Info("Updating webhook")
   169  		vwhclient := ac.client.AdmissionregistrationV1().ValidatingWebhookConfigurations()
   170  		if _, err := vwhclient.Update(ctx, webhook, metav1.UpdateOptions{}); err != nil {
   171  			return fmt.Errorf("failed to update webhook: %w", err)
   172  		}
   173  	} else {
   174  		logger.Info("Webhook is valid")
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  func (ac *reconciler) validate(ctx context.Context, req *admissionv1.AdmissionRequest) error {
   181  	logger := logging.FromContext(ctx)
   182  	kind := req.Kind
   183  	newBytes := req.Object.Raw
   184  
   185  	// Why, oh why are these different types...
   186  	gvk := schema.GroupVersionKind{
   187  		Group:   kind.Group,
   188  		Version: kind.Version,
   189  		Kind:    kind.Kind,
   190  	}
   191  
   192  	resourceGVK := corev1.SchemeGroupVersion.WithKind("ConfigMap")
   193  	if gvk != resourceGVK {
   194  		logger.Error("Unhandled kind: ", gvk)
   195  		return fmt.Errorf("unhandled kind: %v", gvk)
   196  	}
   197  
   198  	var newObj corev1.ConfigMap
   199  	if len(newBytes) != 0 {
   200  		if err := json.Unmarshal(newBytes, &newObj); err != nil {
   201  			return fmt.Errorf("cannot decode incoming new object: %w", err)
   202  		}
   203  	}
   204  
   205  	if constructor, ok := ac.constructors[newObj.Name]; ok {
   206  		// Only validate example data if this is a configMap we know about.
   207  		exampleData, hasExampleData := newObj.Data[configmap.ExampleKey]
   208  		exampleChecksum, hasExampleChecksumAnnotation := newObj.Annotations[configmap.ExampleChecksumAnnotation]
   209  		if hasExampleData && hasExampleChecksumAnnotation &&
   210  			exampleChecksum != configmap.Checksum(exampleData) {
   211  			return fmt.Errorf(
   212  				"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",
   213  				configmap.ExampleKey, "data")
   214  		}
   215  
   216  		inputs := []reflect.Value{
   217  			reflect.ValueOf(&newObj),
   218  		}
   219  
   220  		outputs := constructor.Call(inputs)
   221  		errVal := outputs[1]
   222  
   223  		if !errVal.IsNil() {
   224  			return errVal.Interface().(error)
   225  		}
   226  	}
   227  
   228  	return nil
   229  }
   230  
   231  func (ac *reconciler) registerConfig(name string, constructor interface{}) {
   232  	if err := configmap.ValidateConstructor(constructor); err != nil {
   233  		panic(err)
   234  	}
   235  
   236  	ac.constructors[name] = reflect.ValueOf(constructor)
   237  }