knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/defaulting/defaulting.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 defaulting
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/gobuffalo/flect"
    27  	"go.uber.org/zap"
    28  	"gomodules.xyz/jsonpatch/v2"
    29  
    30  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    31  
    32  	admissionv1 "k8s.io/api/admission/v1"
    33  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    34  	corev1 "k8s.io/api/core/v1"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/apimachinery/pkg/types"
    39  	"k8s.io/client-go/kubernetes"
    40  	admissionlisters "k8s.io/client-go/listers/admissionregistration/v1"
    41  	corelisters "k8s.io/client-go/listers/core/v1"
    42  
    43  	"knative.dev/pkg/apis"
    44  	"knative.dev/pkg/apis/duck"
    45  	"knative.dev/pkg/controller"
    46  	"knative.dev/pkg/kmp"
    47  	"knative.dev/pkg/logging"
    48  	"knative.dev/pkg/ptr"
    49  	pkgreconciler "knative.dev/pkg/reconciler"
    50  	"knative.dev/pkg/system"
    51  	"knative.dev/pkg/webhook"
    52  	certresources "knative.dev/pkg/webhook/certificates/resources"
    53  	"knative.dev/pkg/webhook/json"
    54  	"knative.dev/pkg/webhook/resourcesemantics"
    55  )
    56  
    57  var errMissingNewObject = errors.New("the new object may not be nil")
    58  
    59  // reconciler implements the AdmissionController for resources
    60  type reconciler struct {
    61  	webhook.StatelessAdmissionImpl
    62  	pkgreconciler.LeaderAwareFuncs
    63  
    64  	key       types.NamespacedName
    65  	path      string
    66  	handlers  map[schema.GroupVersionKind]resourcesemantics.GenericCRD
    67  	callbacks map[schema.GroupVersionKind]Callback
    68  
    69  	withContext func(context.Context) context.Context
    70  
    71  	client       kubernetes.Interface
    72  	mwhlister    admissionlisters.MutatingWebhookConfigurationLister
    73  	secretlister corelisters.SecretLister
    74  
    75  	disallowUnknownFields     bool
    76  	secretName                string
    77  	disableNamespaceOwnership bool
    78  }
    79  
    80  // CallbackFunc is the function to be invoked.
    81  type CallbackFunc func(ctx context.Context, unstructured *unstructured.Unstructured) error
    82  
    83  // Callback is a generic function to be called by a consumer of defaulting.
    84  type Callback struct {
    85  	// function is the callback to be invoked.
    86  	function CallbackFunc
    87  
    88  	// supportedVerbs are the verbs supported for the callback.
    89  	// The function will only be called on these actions.
    90  	supportedVerbs map[webhook.Operation]struct{}
    91  }
    92  
    93  // NewCallback creates a new callback function to be invoked on supported verbs.
    94  func NewCallback(function func(context.Context, *unstructured.Unstructured) error, supportedVerbs ...webhook.Operation) Callback {
    95  	if function == nil {
    96  		panic("expected function, got nil")
    97  	}
    98  	m := make(map[webhook.Operation]struct{})
    99  	for _, op := range supportedVerbs {
   100  		if op == webhook.Delete {
   101  			panic("Verb " + webhook.Delete + " not allowed")
   102  		}
   103  		if _, has := m[op]; has {
   104  			panic("duplicate verbs not allowed")
   105  		}
   106  		m[op] = struct{}{}
   107  	}
   108  	return Callback{function: function, supportedVerbs: m}
   109  }
   110  
   111  var (
   112  	_ controller.Reconciler                = (*reconciler)(nil)
   113  	_ pkgreconciler.LeaderAware            = (*reconciler)(nil)
   114  	_ webhook.AdmissionController          = (*reconciler)(nil)
   115  	_ webhook.StatelessAdmissionController = (*reconciler)(nil)
   116  )
   117  
   118  // Reconcile implements controller.Reconciler
   119  func (ac *reconciler) Reconcile(ctx context.Context, key string) error {
   120  	logger := logging.FromContext(ctx)
   121  
   122  	if !ac.IsLeaderFor(ac.key) {
   123  		return controller.NewSkipKey(key)
   124  	}
   125  
   126  	// Look up the webhook secret, and fetch the CA cert bundle.
   127  	secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName)
   128  	if err != nil {
   129  		logger.Errorw("Error fetching secret", zap.Error(err))
   130  		return err
   131  	}
   132  	caCert, ok := secret.Data[certresources.CACert]
   133  	if !ok {
   134  		return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert)
   135  	}
   136  
   137  	// Reconcile the webhook configuration.
   138  	return ac.reconcileMutatingWebhook(ctx, caCert)
   139  }
   140  
   141  // Path implements AdmissionController
   142  func (ac *reconciler) Path() string {
   143  	return ac.path
   144  }
   145  
   146  // Admit implements AdmissionController
   147  func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
   148  	// otelhttp middleware creates the labeler
   149  	labeler, _ := otelhttp.LabelerFromContext(ctx)
   150  	labeler.Add(webhook.WebhookTypeAttr.With(webhook.WebhookTypeDefaulting))
   151  
   152  	if ac.withContext != nil {
   153  		ctx = ac.withContext(ctx)
   154  	}
   155  
   156  	logger := logging.FromContext(ctx)
   157  	switch request.Operation {
   158  	case admissionv1.Create, admissionv1.Update:
   159  	default:
   160  		logger.Info("Unhandled webhook operation, letting it through ", request.Operation)
   161  		return &admissionv1.AdmissionResponse{Allowed: true}
   162  	}
   163  
   164  	patchBytes, err := ac.mutate(ctx, request)
   165  	if err != nil {
   166  		return webhook.MakeErrorStatus("mutation failed: %v", err)
   167  	}
   168  	logger.Infof("Kind: %q PatchBytes: %v", request.Kind, string(patchBytes))
   169  
   170  	return &admissionv1.AdmissionResponse{
   171  		Patch:   patchBytes,
   172  		Allowed: true,
   173  		PatchType: func() *admissionv1.PatchType {
   174  			pt := admissionv1.PatchTypeJSONPatch
   175  			return &pt
   176  		}(),
   177  	}
   178  }
   179  
   180  func (ac *reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error {
   181  	logger := logging.FromContext(ctx)
   182  
   183  	rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(ac.handlers))
   184  	gvks := make(map[schema.GroupVersionKind]struct{}, len(ac.handlers)+len(ac.callbacks))
   185  	for gvk := range ac.handlers {
   186  		gvks[gvk] = struct{}{}
   187  	}
   188  	for gvk := range ac.callbacks {
   189  		if _, ok := gvks[gvk]; !ok {
   190  			gvks[gvk] = struct{}{}
   191  		}
   192  	}
   193  
   194  	for gvk := range gvks {
   195  		plural := strings.ToLower(flect.Pluralize(gvk.Kind))
   196  
   197  		rules = append(rules, admissionregistrationv1.RuleWithOperations{
   198  			Operations: []admissionregistrationv1.OperationType{
   199  				admissionregistrationv1.Create,
   200  				admissionregistrationv1.Update,
   201  			},
   202  			Rule: admissionregistrationv1.Rule{
   203  				APIGroups:   []string{gvk.Group},
   204  				APIVersions: []string{gvk.Version},
   205  				Resources:   []string{plural, plural + "/status"},
   206  			},
   207  		})
   208  	}
   209  
   210  	// Sort the rules by Group, Version, Kind so that things are deterministically ordered.
   211  	sort.Slice(rules, func(i, j int) bool {
   212  		lhs, rhs := rules[i], rules[j]
   213  		if lhs.APIGroups[0] != rhs.APIGroups[0] {
   214  			return lhs.APIGroups[0] < rhs.APIGroups[0]
   215  		}
   216  		if lhs.APIVersions[0] != rhs.APIVersions[0] {
   217  			return lhs.APIVersions[0] < rhs.APIVersions[0]
   218  		}
   219  		return lhs.Resources[0] < rhs.Resources[0]
   220  	})
   221  
   222  	configuredWebhook, err := ac.mwhlister.Get(ac.key.Name)
   223  	if err != nil {
   224  		return fmt.Errorf("error retrieving webhook: %w", err)
   225  	}
   226  
   227  	current := configuredWebhook.DeepCopy()
   228  
   229  	if !ac.disableNamespaceOwnership {
   230  		ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{})
   231  		if err != nil {
   232  			return fmt.Errorf("failed to fetch namespace: %w", err)
   233  		}
   234  		nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace"))
   235  		nsRef.Controller = ptr.Bool(false)
   236  		current.OwnerReferences = []metav1.OwnerReference{nsRef}
   237  	}
   238  
   239  	for i, wh := range current.Webhooks {
   240  		if wh.Name != current.Name {
   241  			continue
   242  		}
   243  
   244  		cur := &current.Webhooks[i]
   245  		cur.Rules = rules
   246  
   247  		cur.NamespaceSelector = webhook.EnsureLabelSelectorExpressions(
   248  			cur.NamespaceSelector,
   249  			&metav1.LabelSelector{
   250  				MatchExpressions: []metav1.LabelSelectorRequirement{{
   251  					Key:      "webhooks.knative.dev/exclude",
   252  					Operator: metav1.LabelSelectorOpDoesNotExist,
   253  				}},
   254  			})
   255  
   256  		cur.ClientConfig.CABundle = caCert
   257  		if cur.ClientConfig.Service == nil {
   258  			return fmt.Errorf("missing service reference for webhook: %s", wh.Name)
   259  		}
   260  		cur.ClientConfig.Service.Path = ptr.String(ac.Path())
   261  
   262  		cur.ReinvocationPolicy = ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy)
   263  	}
   264  
   265  	if ok, err := kmp.SafeEqual(configuredWebhook, current); err != nil {
   266  		return fmt.Errorf("error diffing webhooks: %w", err)
   267  	} else if !ok {
   268  		logger.Info("Updating webhook")
   269  		mwhclient := ac.client.AdmissionregistrationV1().MutatingWebhookConfigurations()
   270  		if _, err := mwhclient.Update(ctx, current, metav1.UpdateOptions{}); err != nil {
   271  			return fmt.Errorf("failed to update webhook: %w", err)
   272  		}
   273  	} else {
   274  		logger.Info("Webhook is valid")
   275  	}
   276  	return nil
   277  }
   278  
   279  func (ac *reconciler) mutate(ctx context.Context, req *admissionv1.AdmissionRequest) ([]byte, error) {
   280  	kind := req.Kind
   281  	newBytes := req.Object.Raw
   282  	oldBytes := req.OldObject.Raw
   283  	// Why, oh why are these different types...
   284  	gvk := schema.GroupVersionKind{
   285  		Group:   kind.Group,
   286  		Version: kind.Version,
   287  		Kind:    kind.Kind,
   288  	}
   289  
   290  	logger := logging.FromContext(ctx)
   291  	handler, ok := ac.handlers[gvk]
   292  	if !ok {
   293  		if _, ok := ac.callbacks[gvk]; !ok {
   294  			logger.Error("Unhandled kind: ", gvk)
   295  			return nil, fmt.Errorf("unhandled kind: %v", gvk)
   296  		}
   297  		patches, err := ac.callback(ctx, gvk, req, true /* shouldSetUserInfo */, duck.JSONPatch{})
   298  		if err != nil {
   299  			logger.Errorw("Failed the callback defaulter", zap.Error(err))
   300  			// Return the error message as-is to give the defaulter callback
   301  			// discretion over (our portion of) the message that the user sees.
   302  			return nil, err
   303  		}
   304  		return json.Marshal(patches)
   305  	}
   306  
   307  	// nil values denote absence of `old` (create) or `new` (delete) objects.
   308  	var oldObj, newObj resourcesemantics.GenericCRD
   309  
   310  	if len(newBytes) != 0 {
   311  		newObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD)
   312  		err := json.Decode(newBytes, newObj, ac.disallowUnknownFields)
   313  		if err != nil {
   314  			return nil, fmt.Errorf("cannot decode incoming new object: %w", err)
   315  		}
   316  	}
   317  	if len(oldBytes) != 0 {
   318  		oldObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD)
   319  		err := json.Decode(oldBytes, oldObj, ac.disallowUnknownFields)
   320  		if err != nil {
   321  			return nil, fmt.Errorf("cannot decode incoming old object: %w", err)
   322  		}
   323  	}
   324  	var patches duck.JSONPatch
   325  
   326  	var err error
   327  	// Skip this step if the type we're dealing with is a duck type, since it is inherently
   328  	// incomplete and this will patch away all of the unspecified fields.
   329  	if _, ok := newObj.(duck.Populatable); !ok {
   330  		// Add these before defaulting fields, otherwise defaulting may cause an illegal patch
   331  		// because it expects the round tripped through Golang fields to be present already.
   332  		rtp, err := roundTripPatch(newBytes, newObj)
   333  		if err != nil {
   334  			return nil, fmt.Errorf("cannot create patch for round tripped newBytes: %w", err)
   335  		}
   336  		patches = append(patches, rtp...)
   337  	}
   338  
   339  	// Set up the context for defaulting and validation
   340  	if oldObj != nil {
   341  		// Copy the old object and set defaults so that we don't reject our own
   342  		// defaulting done earlier in the webhook.
   343  		oldObj = oldObj.DeepCopyObject().(resourcesemantics.GenericCRD)
   344  		oldObj.SetDefaults(ctx)
   345  
   346  		s, ok := oldObj.(apis.HasSpec)
   347  		if ok {
   348  			setUserInfoAnnotations(ctx, s, req.Resource.Group)
   349  		}
   350  
   351  		if req.SubResource == "" {
   352  			ctx = apis.WithinUpdate(ctx, oldObj)
   353  		} else {
   354  			ctx = apis.WithinSubResourceUpdate(ctx, oldObj, req.SubResource)
   355  		}
   356  	} else {
   357  		ctx = apis.WithinCreate(ctx)
   358  	}
   359  	ctx = apis.WithUserInfo(ctx, &req.UserInfo)
   360  
   361  	// Default the new object.
   362  	if patches, err = setDefaults(ctx, patches, newObj); err != nil {
   363  		logger.Errorw("Failed the resource specific defaulter", zap.Error(err))
   364  		// Return the error message as-is to give the defaulter callback
   365  		// discretion over (our portion of) the message that the user sees.
   366  		return nil, err
   367  	}
   368  
   369  	if patches, err = ac.setUserInfoAnnotations(ctx, patches, newObj, req.Resource.Group); err != nil {
   370  		logger.Errorw("Failed the resource user info annotator", zap.Error(err))
   371  		return nil, err
   372  	}
   373  
   374  	if patches, err = ac.callback(ctx, gvk, req, false /* shouldSetUserInfo */, patches); err != nil {
   375  		logger.Errorw("Failed the callback defaulter", zap.Error(err))
   376  		// Return the error message as-is to give the defaulter callback
   377  		// discretion over (our portion of) the message that the user sees.
   378  		return nil, err
   379  	}
   380  
   381  	// None of the validators will accept a nil value for newObj.
   382  	if newObj == nil {
   383  		return nil, errMissingNewObject
   384  	}
   385  	return json.Marshal(patches)
   386  }
   387  
   388  func (ac *reconciler) setUserInfoAnnotations(ctx context.Context, patches duck.JSONPatch, new resourcesemantics.GenericCRD, groupName string) (duck.JSONPatch, error) {
   389  	if new == nil {
   390  		return patches, nil
   391  	}
   392  	nh, ok := new.(apis.HasSpec)
   393  	if !ok {
   394  		return patches, nil
   395  	}
   396  
   397  	b, a := new.DeepCopyObject().(apis.HasSpec), nh
   398  
   399  	setUserInfoAnnotations(ctx, nh, groupName)
   400  
   401  	patch, err := duck.CreatePatch(b, a)
   402  	if err != nil {
   403  		return nil, err
   404  	}
   405  	return append(patches, patch...), nil
   406  }
   407  
   408  func (ac *reconciler) callback(ctx context.Context, gvk schema.GroupVersionKind, req *admissionv1.AdmissionRequest, shouldSetUserInfo bool, patches duck.JSONPatch) (duck.JSONPatch, error) {
   409  	// Get callback.
   410  	callback, ok := ac.callbacks[gvk]
   411  	if !ok {
   412  		return patches, nil
   413  	}
   414  
   415  	// Check if request operation is a supported webhook operation.
   416  	if _, isSupported := callback.supportedVerbs[req.Operation]; !isSupported {
   417  		return patches, nil
   418  	}
   419  
   420  	oldBytes := req.OldObject.Raw
   421  	newBytes := req.Object.Raw
   422  
   423  	before := &unstructured.Unstructured{}
   424  	after := &unstructured.Unstructured{}
   425  
   426  	// Get unstructured object.
   427  	if err := json.Unmarshal(newBytes, before); err != nil {
   428  		return nil, fmt.Errorf("cannot decode object: %w", err)
   429  	}
   430  	// Copy before in after unstructured objects.
   431  	before.DeepCopyInto(after)
   432  
   433  	// Setup context.
   434  	if len(oldBytes) != 0 {
   435  		if req.SubResource == "" {
   436  			ctx = apis.WithinUpdate(ctx, before)
   437  		} else {
   438  			ctx = apis.WithinSubResourceUpdate(ctx, before, req.SubResource)
   439  		}
   440  	} else {
   441  		ctx = apis.WithinCreate(ctx)
   442  	}
   443  	ctx = apis.WithUserInfo(ctx, &req.UserInfo)
   444  
   445  	// Call callback passing after.
   446  	if err := callback.function(ctx, after); err != nil {
   447  		return patches, err
   448  	}
   449  
   450  	if shouldSetUserInfo {
   451  		setUserInfoAnnotations(adaptUnstructuredHasSpecCtx(ctx, req), unstructuredHasSpec{after}, req.Resource.Group)
   452  	}
   453  
   454  	// Create patches.
   455  	patch, err := duck.CreatePatch(before.Object, after.Object)
   456  	return append(patches, patch...), err
   457  }
   458  
   459  // roundTripPatch generates the JSONPatch that corresponds to round tripping the given bytes through
   460  // the Golang type (JSON -> Golang type -> JSON). Because it is not always true that
   461  // bytes == json.Marshal(json.Unmarshal(bytes)).
   462  //
   463  // For example, if bytes did not contain a 'spec' field and the Golang type specifies its 'spec'
   464  // field without omitempty, then by round tripping through the Golang type, we would have added
   465  // `'spec': {}`.
   466  func roundTripPatch(bytes []byte, unmarshalled interface{}) (duck.JSONPatch, error) {
   467  	if unmarshalled == nil {
   468  		return duck.JSONPatch{}, nil
   469  	}
   470  	marshaledBytes, err := json.Marshal(unmarshalled)
   471  	if err != nil {
   472  		return nil, fmt.Errorf("cannot marshal interface: %w", err)
   473  	}
   474  	return jsonpatch.CreatePatch(bytes, marshaledBytes)
   475  }
   476  
   477  // setDefaults simply leverages apis.Defaultable to set defaults.
   478  func setDefaults(ctx context.Context, patches duck.JSONPatch, crd resourcesemantics.GenericCRD) (duck.JSONPatch, error) {
   479  	before, after := crd.DeepCopyObject(), crd
   480  	after.SetDefaults(ctx)
   481  
   482  	patch, err := duck.CreatePatch(before, after)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	return append(patches, patch...), nil
   488  }
   489  
   490  func ptrReinvocationPolicyType(r admissionregistrationv1.ReinvocationPolicyType) *admissionregistrationv1.ReinvocationPolicyType {
   491  	return &r
   492  }