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

     1  /*
     2  Copyright 2020 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 validation
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  
    24  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    25  	"go.uber.org/zap"
    26  
    27  	admissionv1 "k8s.io/api/admission/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  
    31  	"knative.dev/pkg/apis"
    32  	kubeclient "knative.dev/pkg/client/injection/kube/client"
    33  	"knative.dev/pkg/logging"
    34  	"knative.dev/pkg/webhook"
    35  	"knative.dev/pkg/webhook/json"
    36  	"knative.dev/pkg/webhook/resourcesemantics"
    37  )
    38  
    39  var errMissingNewObject = errors.New("the new object may not be nil")
    40  
    41  // Callback is a generic function to be called by a consumer of validation
    42  type Callback struct {
    43  	// function is the callback to be invoked
    44  	function func(ctx context.Context, unstructured *unstructured.Unstructured) error
    45  
    46  	// supportedVerbs are the verbs supported for the callback.
    47  	supportedVerbs map[webhook.Operation]struct{}
    48  }
    49  
    50  // NewCallback creates a new callback function to be invoked on supported verbs.
    51  func NewCallback(function func(context.Context, *unstructured.Unstructured) error, supportedVerbs ...webhook.Operation) Callback {
    52  	m := make(map[webhook.Operation]struct{})
    53  	for _, op := range supportedVerbs {
    54  		if _, has := m[op]; has {
    55  			panic("duplicate verbs not allowed")
    56  		}
    57  		m[op] = struct{}{}
    58  	}
    59  	return Callback{function: function, supportedVerbs: m}
    60  }
    61  
    62  var _ webhook.AdmissionController = (*reconciler)(nil)
    63  
    64  // Admit implements AdmissionController
    65  func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) (resp *admissionv1.AdmissionResponse) {
    66  	// otelhttp middleware creates the labeler
    67  	labeler, _ := otelhttp.LabelerFromContext(ctx)
    68  	labeler.Add(webhook.WebhookTypeAttr.With(webhook.WebhookTypeValidation))
    69  
    70  	if ac.withContext != nil {
    71  		ctx = ac.withContext(ctx)
    72  	}
    73  
    74  	kind := request.Kind
    75  	gvk := schema.GroupVersionKind{
    76  		Group:   kind.Group,
    77  		Version: kind.Version,
    78  		Kind:    kind.Kind,
    79  	}
    80  
    81  	ctx, resource, err := ac.decodeRequestAndPrepareContext(ctx, request, gvk)
    82  	if err != nil {
    83  		return webhook.MakeErrorStatus("decoding request failed: %v", err)
    84  	}
    85  
    86  	errors, warnings := validate(ctx, resource, request)
    87  	if warnings != nil {
    88  		// If there were warnings, then keep processing things, but augment
    89  		// whatever AdmissionResponse we send with the warnings.  We cannot
    90  		// simply set `resp.Warnings` directly here because the return paths
    91  		// below all overwrite `resp`, but the `defer` affords us one final
    92  		// crack at things.
    93  		defer func() {
    94  			resp.Warnings = make([]string, 0, len(warnings))
    95  			for _, w := range warnings {
    96  				resp.Warnings = append(resp.Warnings, w.Error())
    97  			}
    98  		}()
    99  	}
   100  	if errors != nil {
   101  		return webhook.MakeErrorStatus("validation failed: %v", errors)
   102  	}
   103  
   104  	if err := ac.callback(ctx, request, gvk); err != nil {
   105  		return webhook.MakeErrorStatus("validation callback failed: %v", err)
   106  	}
   107  
   108  	return &admissionv1.AdmissionResponse{Allowed: true}
   109  }
   110  
   111  // decodeRequestAndPrepareContext deserializes the old and new GenericCrds from the incoming request and sets up the context.
   112  // nil oldObj or newObj denote absence of `old` (create) or `new` (delete) objects.
   113  func (ac *reconciler) decodeRequestAndPrepareContext(
   114  	ctx context.Context,
   115  	req *admissionv1.AdmissionRequest,
   116  	gvk schema.GroupVersionKind,
   117  ) (context.Context, resourcesemantics.GenericCRD, error) {
   118  	logger := logging.FromContext(ctx)
   119  	handler, ok := ac.handlers[gvk]
   120  	if !ok {
   121  		logger.Error("Unhandled kind: ", gvk)
   122  		return ctx, nil, fmt.Errorf("unhandled kind: %v", gvk)
   123  	}
   124  
   125  	newBytes := req.Object.Raw
   126  	oldBytes := req.OldObject.Raw
   127  
   128  	// Decode json to a GenericCRD
   129  	var newObj resourcesemantics.GenericCRD
   130  	if len(newBytes) != 0 {
   131  		newObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD)
   132  		err := json.Decode(newBytes, newObj, ac.disallowUnknownFields)
   133  		if err != nil {
   134  			return ctx, nil, fmt.Errorf("cannot decode incoming new object: %w", err)
   135  		}
   136  	}
   137  
   138  	var oldObj resourcesemantics.GenericCRD
   139  	if len(oldBytes) != 0 {
   140  		oldObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD)
   141  		err := json.Decode(oldBytes, oldObj, ac.disallowUnknownFields)
   142  		if err != nil {
   143  			return ctx, nil, fmt.Errorf("cannot decode incoming old object: %w", err)
   144  		}
   145  	}
   146  
   147  	ctx = apis.WithUserInfo(ctx, &req.UserInfo)
   148  	ctx = context.WithValue(ctx, kubeclient.Key{}, ac.client)
   149  	if req.DryRun != nil && *req.DryRun {
   150  		ctx = apis.WithDryRun(ctx)
   151  	}
   152  
   153  	switch req.Operation {
   154  	case admissionv1.Update:
   155  		if req.SubResource != "" {
   156  			ctx = apis.WithinSubResourceUpdate(ctx, oldObj, req.SubResource)
   157  		} else {
   158  			ctx = apis.WithinUpdate(ctx, oldObj)
   159  		}
   160  	case admissionv1.Create:
   161  		ctx = apis.WithinCreate(ctx)
   162  	case admissionv1.Delete:
   163  		ctx = apis.WithinDelete(ctx)
   164  		return ctx, oldObj, nil
   165  	}
   166  
   167  	return ctx, newObj, nil
   168  }
   169  
   170  //nolint:staticcheck
   171  func validate(ctx context.Context, resource resourcesemantics.GenericCRD, req *admissionv1.AdmissionRequest) (err error, warn []error) {
   172  	logger := logging.FromContext(ctx)
   173  
   174  	// Only run validation for supported create and update validation.
   175  	switch req.Operation {
   176  	case admissionv1.Create, admissionv1.Update:
   177  		// Supported verbs
   178  	case admissionv1.Delete:
   179  		return nil, nil // Validation handled by optional Callback, but not validatable.
   180  	default:
   181  		logger.Info("Unhandled webhook validation operation, letting it through ", req.Operation)
   182  		return nil, nil
   183  	}
   184  
   185  	// None of the validators will accept a nil value for newObj.
   186  	if resource == nil {
   187  		return errMissingNewObject, nil
   188  	}
   189  
   190  	if result := resource.Validate(ctx); result != nil {
   191  		logger.Infow("Failed the resource specific validation",
   192  			zap.String("name", req.Name),
   193  			zap.String("namespace", req.Namespace),
   194  			zap.String("kind", req.Kind.Kind),
   195  			zap.Error(result))
   196  		// While we have the strong typing of apis.FieldError, partition the
   197  		// returned error into the error-level diagnostics and warning-level
   198  		// diagnostics, so that the admission response can embed things into
   199  		// the appropriate portions of the response.
   200  		// This is expanded like to to avoid problems with typed nils.
   201  		if errorResult := result.Filter(apis.ErrorLevel); errorResult != nil {
   202  			err = errorResult
   203  		}
   204  		if warningResult := result.Filter(apis.WarningLevel); warningResult != nil {
   205  			ws := warningResult.WrappedErrors()
   206  			warn = make([]error, 0, len(ws))
   207  			for _, w := range ws {
   208  				warn = append(warn, w)
   209  			}
   210  		}
   211  	}
   212  	return err, warn
   213  }
   214  
   215  // callback runs optional callbacks on admission
   216  func (ac *reconciler) callback(ctx context.Context, req *admissionv1.AdmissionRequest, gvk schema.GroupVersionKind) error {
   217  	var toDecode []byte
   218  	if req.Operation == admissionv1.Delete {
   219  		toDecode = req.OldObject.Raw
   220  	} else {
   221  		toDecode = req.Object.Raw
   222  	}
   223  	if toDecode == nil {
   224  		logger := logging.FromContext(ctx)
   225  		logger.Errorf("No incoming object found: %v for verb %v", gvk, req.Operation)
   226  		return nil
   227  	}
   228  
   229  	// Generically callback if any are provided for the resource.
   230  	if c, ok := ac.callbacks[gvk]; ok {
   231  		if _, supported := c.supportedVerbs[req.Operation]; supported {
   232  			unstruct := &unstructured.Unstructured{}
   233  			if err := json.Unmarshal(toDecode, unstruct); err != nil {
   234  				return fmt.Errorf("cannot decode incoming new object: %w", err)
   235  			}
   236  
   237  			return c.function(ctx, unstruct)
   238  		}
   239  	}
   240  
   241  	return nil
   242  }