knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/psbinding/psbinding.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 psbinding
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/gobuffalo/flect"
    27  	"go.uber.org/zap"
    28  	admissionv1 "k8s.io/api/admission/v1"
    29  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    30  	"k8s.io/apimachinery/pkg/api/equality"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/labels"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/util/sets"
    35  	"k8s.io/client-go/kubernetes"
    36  	admissionlisters "k8s.io/client-go/listers/admissionregistration/v1"
    37  	corelisters "k8s.io/client-go/listers/core/v1"
    38  	"knative.dev/pkg/apis/duck"
    39  	duckv1 "knative.dev/pkg/apis/duck/v1"
    40  	"knative.dev/pkg/controller"
    41  	"knative.dev/pkg/logging"
    42  	"knative.dev/pkg/ptr"
    43  	pkgreconciler "knative.dev/pkg/reconciler"
    44  	"knative.dev/pkg/system"
    45  	"knative.dev/pkg/webhook"
    46  	certresources "knative.dev/pkg/webhook/certificates/resources"
    47  )
    48  
    49  // ReconcilerOption is a function to modify the Reconciler.
    50  type ReconcilerOption func(*Reconciler)
    51  
    52  // WithSelector specifies the selector for the webhook.
    53  func WithSelector(s metav1.LabelSelector) ReconcilerOption {
    54  	return func(r *Reconciler) {
    55  		r.selector = s
    56  	}
    57  }
    58  
    59  func NewReconciler(
    60  	name, path, secretName string,
    61  	client kubernetes.Interface,
    62  	mwhLister admissionlisters.MutatingWebhookConfigurationLister,
    63  	secretLister corelisters.SecretLister,
    64  	withContext BindableContext,
    65  	options ...ReconcilerOption,
    66  ) *Reconciler {
    67  	r := &Reconciler{
    68  		Name:        name,
    69  		HandlerPath: path,
    70  		SecretName:  secretName,
    71  
    72  		// This is the user-provided context-decorator, which allows
    73  		// them to infuse the context passed to Do/Undo.
    74  		WithContext: withContext,
    75  
    76  		Client:       client,
    77  		MWHLister:    mwhLister,
    78  		SecretLister: secretLister,
    79  		selector:     ExclusionSelector, // Use ExclusionSelector by default.
    80  	}
    81  
    82  	// Apply options.
    83  	for _, opt := range options {
    84  		opt(r)
    85  	}
    86  
    87  	return r
    88  }
    89  
    90  // Reconciler implements an AdmissionController for altering PodSpecable
    91  // resources that are the subject of a particular type of Binding.
    92  // The two key methods are:
    93  //  1. reconcileMutatingWebhook: which enumerates all of the Bindings and
    94  //     compiles a list of resource types that should be intercepted by our
    95  //     webhook.  It also builds an index that can be used to efficiently
    96  //     handle Admit requests.
    97  //  2. Admit: which leverages the index built by the Reconciler to apply
    98  //     mutations to resources.
    99  type Reconciler struct {
   100  	pkgreconciler.LeaderAwareFuncs
   101  
   102  	Name        string
   103  	HandlerPath string
   104  	SecretName  string
   105  
   106  	Client       kubernetes.Interface
   107  	MWHLister    admissionlisters.MutatingWebhookConfigurationLister
   108  	SecretLister corelisters.SecretLister
   109  	ListAll      ListAll
   110  
   111  	// WithContext is a callback that infuses the context supplied to
   112  	// Do/Undo with additional context to enable them to complete their
   113  	// respective tasks.
   114  	WithContext BindableContext
   115  
   116  	selector metav1.LabelSelector
   117  
   118  	index index
   119  }
   120  
   121  var (
   122  	_ controller.Reconciler       = (*Reconciler)(nil)
   123  	_ pkgreconciler.LeaderAware   = (*Reconciler)(nil)
   124  	_ webhook.AdmissionController = (*Reconciler)(nil)
   125  )
   126  
   127  // We need to specifically exclude our deployment(s) from consideration, but this provides a way
   128  // of excluding other things as well.
   129  var (
   130  	ExclusionSelector = metav1.LabelSelector{
   131  		MatchExpressions: []metav1.LabelSelectorRequirement{{
   132  			Key:      duck.BindingExcludeLabel,
   133  			Operator: metav1.LabelSelectorOpNotIn,
   134  			Values:   []string{"true"},
   135  		}},
   136  		// TODO(mattmoor): Consider also having a GVR-based one, e.g.
   137  		//    foobindings.blah.knative.dev/exclude: "true"
   138  	}
   139  	InclusionSelector = metav1.LabelSelector{
   140  		MatchExpressions: []metav1.LabelSelectorRequirement{{
   141  			Key:      duck.BindingIncludeLabel,
   142  			Operator: metav1.LabelSelectorOpIn,
   143  			Values:   []string{"true"},
   144  		}},
   145  		// TODO(mattmoor): Consider also having a GVR-based one, e.g.
   146  		//    foobindings.blah.knative.dev/include: "true"
   147  	}
   148  )
   149  
   150  // Reconcile implements controller.Reconciler
   151  func (ac *Reconciler) Reconcile(ctx context.Context, key string) error {
   152  	// Look up the webhook secret, and fetch the CA cert bundle.
   153  	secret, err := ac.SecretLister.Secrets(system.Namespace()).Get(ac.SecretName)
   154  	if err != nil {
   155  		logging.FromContext(ctx).Errorw("Error fetching secret", zap.Error(err))
   156  		return err
   157  	}
   158  	caCert, ok := secret.Data[certresources.CACert]
   159  	if !ok {
   160  		return fmt.Errorf("secret %q is missing %q key", ac.SecretName, certresources.CACert)
   161  	}
   162  
   163  	// Reconcile the webhook configuration.
   164  	return ac.reconcileMutatingWebhook(ctx, caCert)
   165  }
   166  
   167  // Path implements AdmissionController
   168  func (ac *Reconciler) Path() string {
   169  	return ac.HandlerPath
   170  }
   171  
   172  // Admit implements AdmissionController
   173  func (ac *Reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
   174  	switch request.Operation {
   175  	case admissionv1.Create, admissionv1.Update:
   176  	default:
   177  		logging.FromContext(ctx).Info("Unhandled webhook operation, letting it through ", request.Operation)
   178  		return &admissionv1.AdmissionResponse{Allowed: true}
   179  	}
   180  
   181  	orig := &duckv1.WithPod{}
   182  	if err := json.Unmarshal(request.Object.Raw, orig); err != nil {
   183  		return webhook.MakeErrorStatus("unable to decode object: %v", err)
   184  	}
   185  
   186  	// Look up the Bindables for this resource.
   187  	fbs := ac.index.lookUp(exactKey{
   188  		Group:     request.Kind.Group,
   189  		Kind:      request.Kind.Kind,
   190  		Namespace: request.Namespace,
   191  		Name:      orig.Name,
   192  	},
   193  		labels.Set(orig.Labels))
   194  	if len(fbs) == 0 {
   195  		// This doesn't apply!
   196  		return &admissionv1.AdmissionResponse{Allowed: true}
   197  	}
   198  
   199  	// Copy the subject state.
   200  	mutated := orig.DeepCopy()
   201  
   202  	// Apply the Bindables to the copy of the subject state. If conflicts occur, for example because multiple Bindables
   203  	// make incompatible changes, the reconciler will attempt to correct the state later.
   204  	for _, fb := range fbs {
   205  		var bindingContext context.Context
   206  		// Callback into the user's code to setup the context with additional
   207  		// information needed to perform the mutation.
   208  		if ac.WithContext != nil {
   209  			var err error
   210  			bindingContext, err = ac.WithContext(ctx, fb)
   211  			if err != nil {
   212  				return webhook.MakeErrorStatus("unable to setup binding context: %v", err)
   213  			}
   214  		} else {
   215  			bindingContext = ctx
   216  		}
   217  
   218  		// Mutate the copy of the subject state according to the deletion state of the Bindable.
   219  		if fb.GetDeletionTimestamp() != nil {
   220  			fb.Undo(bindingContext, mutated)
   221  		} else {
   222  			fb.Do(bindingContext, mutated)
   223  		}
   224  	}
   225  
   226  	// Synthesize a patch from the changes and return it in our AdmissionResponse
   227  	patchBytes, err := duck.CreateBytePatch(orig, mutated)
   228  	if err != nil {
   229  		return webhook.MakeErrorStatus("unable to create patch with binding: %v", err)
   230  	}
   231  	return &admissionv1.AdmissionResponse{
   232  		Patch:   patchBytes,
   233  		Allowed: true,
   234  		PatchType: func() *admissionv1.PatchType {
   235  			pt := admissionv1.PatchTypeJSONPatch
   236  			return &pt
   237  		}(),
   238  	}
   239  }
   240  
   241  func (ac *Reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error {
   242  	// Build a deduplicated list of all of the GVKs we see.
   243  	gks := map[schema.GroupKind]sets.Set[string]{}
   244  
   245  	// When reconciling the webhook, enumerate all of the bindings, so that
   246  	// we can index them to efficiently respond to webhook requests.
   247  	fbs, err := ac.ListAll()
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	ib := newIndexBuilder()
   253  	for _, fb := range fbs {
   254  		ref := fb.GetSubject()
   255  		gv, err := schema.ParseGroupVersion(ref.APIVersion)
   256  		if err != nil {
   257  			return err
   258  		}
   259  		gk := schema.GroupKind{
   260  			Group: gv.Group,
   261  			Kind:  ref.Kind,
   262  		}
   263  		set := gks[gk]
   264  		if set == nil {
   265  			set = make(sets.Set[string], 1)
   266  		}
   267  		set.Insert(gv.Version)
   268  		gks[gk] = set
   269  
   270  		if ref.Name != "" {
   271  			ib.associate(exactKey{
   272  				Group:     gk.Group,
   273  				Kind:      gk.Kind,
   274  				Namespace: ref.Namespace,
   275  				Name:      ref.Name,
   276  			},
   277  				fb)
   278  		} else {
   279  			selector, err := metav1.LabelSelectorAsSelector(ref.Selector)
   280  			if err != nil {
   281  				return err
   282  			}
   283  			ib.associateSelection(inexactKey{
   284  				Group:     gk.Group,
   285  				Kind:      gk.Kind,
   286  				Namespace: ref.Namespace,
   287  			},
   288  				selector, fb)
   289  		}
   290  	}
   291  
   292  	// Update the index.
   293  	ib.build(&ac.index)
   294  
   295  	// After we've updated our indices, bail out unless we are the leader.
   296  	// Only the leader should be mutating the webhook.
   297  	if !ac.IsLeaderFor(sentinel) {
   298  		// We don't use controller.NewSkipKey here because we did do
   299  		// some amount of processing and the timing information may be
   300  		// useful.
   301  		return nil
   302  	}
   303  
   304  	rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(gks))
   305  	for gk, versions := range gks {
   306  		plural := strings.ToLower(flect.Pluralize(gk.Kind))
   307  
   308  		rules = append(rules, admissionregistrationv1.RuleWithOperations{
   309  			Operations: []admissionregistrationv1.OperationType{
   310  				admissionregistrationv1.Create,
   311  				admissionregistrationv1.Update,
   312  			},
   313  			Rule: admissionregistrationv1.Rule{
   314  				APIGroups:   []string{gk.Group},
   315  				APIVersions: sets.List(versions),
   316  				Resources:   []string{plural + "/*"},
   317  			},
   318  		})
   319  	}
   320  
   321  	// Sort the rules by Group, Version, Kind so that things are deterministically ordered.
   322  	sort.Slice(rules, func(i, j int) bool {
   323  		lhs, rhs := rules[i], rules[j]
   324  		if lhs.APIGroups[0] != rhs.APIGroups[0] {
   325  			return lhs.APIGroups[0] < rhs.APIGroups[0]
   326  		}
   327  		if lhs.APIVersions[0] != rhs.APIVersions[0] {
   328  			return lhs.APIVersions[0] < rhs.APIVersions[0]
   329  		}
   330  		return lhs.Resources[0] < rhs.Resources[0]
   331  	})
   332  
   333  	configuredWebhook, err := ac.MWHLister.Get(ac.Name)
   334  	if err != nil {
   335  		return fmt.Errorf("error retrieving webhook: %w", err)
   336  	}
   337  	current := configuredWebhook.DeepCopy()
   338  
   339  	// Use the "Equivalent" match policy so that we don't need to enumerate versions for same-types.
   340  	// This is only supported by 1.15+ clusters.
   341  	matchPolicy := admissionregistrationv1.Equivalent
   342  
   343  	for i, wh := range current.Webhooks {
   344  		if wh.Name != current.Name {
   345  			continue
   346  		}
   347  		cur := &current.Webhooks[i]
   348  		selector := webhook.EnsureLabelSelectorExpressions(cur.NamespaceSelector, &ac.selector)
   349  
   350  		cur.MatchPolicy = &matchPolicy
   351  		cur.Rules = rules
   352  		cur.NamespaceSelector = selector
   353  		cur.ObjectSelector = selector // 1.15+ only
   354  		cur.ClientConfig.CABundle = caCert
   355  		if cur.ClientConfig.Service == nil {
   356  			return fmt.Errorf("missing service reference for webhook: %s", wh.Name)
   357  		}
   358  		cur.ClientConfig.Service.Path = ptr.String(ac.Path())
   359  		cur.ReinvocationPolicy = ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy)
   360  	}
   361  
   362  	if ok := equality.Semantic.DeepEqual(configuredWebhook, current); !ok {
   363  		logging.FromContext(ctx).Info("Updating webhook")
   364  		mwhclient := ac.Client.AdmissionregistrationV1().MutatingWebhookConfigurations()
   365  		if _, err := mwhclient.Update(ctx, current, metav1.UpdateOptions{}); err != nil {
   366  			return fmt.Errorf("failed to update webhook: %w", err)
   367  		}
   368  	} else {
   369  		logging.FromContext(ctx).Info("Webhook is valid")
   370  	}
   371  	return nil
   372  }
   373  
   374  func ptrReinvocationPolicyType(r admissionregistrationv1.ReinvocationPolicyType) *admissionregistrationv1.ReinvocationPolicyType {
   375  	return &r
   376  }