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 }