knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/admission.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 webhook 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "strings" 27 "time" 28 29 admissionv1 "k8s.io/api/admission/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "knative.dev/pkg/apis" 33 "knative.dev/pkg/logging" 34 "knative.dev/pkg/logging/logkey" 35 36 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 37 "go.opentelemetry.io/otel/codes" 38 "go.opentelemetry.io/otel/metric" 39 "go.opentelemetry.io/otel/trace" 40 ) 41 42 const ( 43 // AdmissionReviewUID is the key used to represent the admission review 44 // request/response UID in logs 45 AdmissionReviewUID = "admissionreview/uid" 46 47 // AdmissionReviewAllowed is the key used to represent whether or not 48 // the admission request was permitted in logs 49 AdmissionReviewAllowed = "admissionreview/allowed" 50 51 // AdmissionReviewResult is the key used to represent extra details into 52 // why an admission request was denied in logs 53 AdmissionReviewResult = "admissionreview/result" 54 55 // AdmissionReviewPatchType is the key used to represent the type of Patch in logs 56 AdmissionReviewPatchType = "admissionreview/patchtype" 57 ) 58 59 // AdmissionController provides the interface for different admission controllers 60 type AdmissionController interface { 61 // Path returns the path that this particular admission controller serves on. 62 Path() string 63 64 // Admit is the callback which is invoked when an HTTPS request comes in on Path(). 65 Admit(context.Context, *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse 66 } 67 68 // StatelessAdmissionController is implemented by AdmissionControllers where Admit may be safely 69 // called before informers have finished syncing. This is implemented by inlining 70 // StatelessAdmissionImpl in your Go type. 71 type StatelessAdmissionController interface { 72 // A silly name that should avoid collisions. 73 ThisTypeDoesNotDependOnInformerState() 74 } 75 76 // MakeErrorStatus creates an 'BadRequest' error AdmissionResponse 77 func MakeErrorStatus(reason string, args ...any) *admissionv1.AdmissionResponse { 78 result := apierrors.NewBadRequest(fmt.Sprintf(reason, args...)).Status() 79 return &admissionv1.AdmissionResponse{ 80 Result: &result, 81 Allowed: false, 82 } 83 } 84 85 func admissionHandler(wh *Webhook, c AdmissionController, synced <-chan struct{}) http.HandlerFunc { 86 return func(w http.ResponseWriter, r *http.Request) { 87 if _, ok := c.(StatelessAdmissionController); ok { 88 // Stateless admission controllers do not require Informers to have 89 // finished syncing before Admit is called. 90 } else { 91 // Don't allow admission control requests through until we have been 92 // notified that informers have been synchronized. 93 <-synced 94 } 95 96 logger := wh.Logger 97 logger.Infof("Webhook ServeHTTP request=%#v", r) 98 99 span := trace.SpanFromContext(r.Context()) 100 // otelhttp middleware creates the labeler 101 labeler, _ := otelhttp.LabelerFromContext(r.Context()) 102 103 defer func() { 104 // otelhttp doesn't add labeler attributes to spans 105 // so we have to do it manually 106 span.SetAttributes(labeler.Get()...) 107 }() 108 109 var review admissionv1.AdmissionReview 110 bodyBuffer := bytes.Buffer{} 111 if err := json.NewDecoder(io.TeeReader(r.Body, &bodyBuffer)).Decode(&review); err != nil { 112 msg := fmt.Sprint("could not decode body:", err) 113 span.SetStatus(codes.Error, msg) 114 http.Error(w, msg, http.StatusBadRequest) 115 return 116 } 117 r.Body = io.NopCloser(&bodyBuffer) 118 119 labeler.Add( 120 KindAttr.With(review.Request.Kind.Kind), 121 GroupAttr.With(review.Request.Kind.Group), 122 VersionAttr.With(review.Request.Kind.Version), 123 OperationAttr.With(string(review.Request.Operation)), 124 SubresourceAttr.With(review.Request.SubResource), 125 WebhookTypeAttr.With(WebhookTypeAdmission), 126 ) 127 128 logger = logger.With( 129 logkey.Kind, review.Request.Kind.String(), 130 logkey.Namespace, review.Request.Namespace, 131 logkey.Name, review.Request.Name, 132 logkey.Operation, string(review.Request.Operation), 133 logkey.Resource, review.Request.Resource.String(), 134 logkey.SubResource, review.Request.SubResource, 135 logkey.UserInfo, review.Request.UserInfo.Username, 136 ) 137 138 ctx := logging.WithLogger(r.Context(), logger) 139 ctx = apis.WithHTTPRequest(ctx, r) 140 141 response := admissionv1.AdmissionReview{ 142 // Use the same type meta as the request - this is required by the K8s API 143 // note: v1beta1 & v1 AdmissionReview shapes are identical so even though 144 // we're using v1 types we still support v1beta1 admission requests 145 TypeMeta: review.TypeMeta, 146 } 147 148 ttStart := time.Now() 149 reviewResponse := c.Admit(ctx, review.Request) 150 151 var patchType string 152 if reviewResponse.PatchType != nil { 153 patchType = string(*reviewResponse.PatchType) 154 } 155 156 status := metav1.StatusFailure 157 if reviewResponse.Allowed { 158 status = metav1.StatusSuccess 159 } else { 160 span.SetStatus(codes.Error, reviewResponse.Result.Message) 161 } 162 163 labeler.Add(StatusAttr.With(strings.ToLower(status))) 164 165 wh.metrics.recordHandlerDuration(ctx, 166 time.Since(ttStart), 167 metric.WithAttributes(labeler.Get()...), 168 ) 169 170 if !reviewResponse.Allowed || reviewResponse.PatchType != nil || response.Response == nil { 171 response.Response = reviewResponse 172 } 173 174 // If warnings contain newlines, which they will do by default if 175 // using Knative apis.FieldError, split them based on newlines 176 // and create a new warning. This is because any control characters 177 // in the warnings will cause the warning to be dropped silently. 178 if reviewResponse.Warnings != nil { 179 cleanedWarnings := make([]string, 0, len(reviewResponse.Warnings)) 180 for _, w := range reviewResponse.Warnings { 181 cleanedWarnings = append(cleanedWarnings, strings.Split(w, "\n")...) 182 } 183 reviewResponse.Warnings = cleanedWarnings 184 } 185 response.Response.UID = review.Request.UID 186 187 logger = logger.With( 188 AdmissionReviewUID, string(reviewResponse.UID), 189 AdmissionReviewAllowed, reviewResponse.Allowed, 190 AdmissionReviewResult, reviewResponse.Result.String()) 191 192 logger.Infof("remote admission controller audit annotations=%#v", reviewResponse.AuditAnnotations) 193 logger.Debugf("AdmissionReview patch={ type: %s, body: %s }", patchType, string(reviewResponse.Patch)) 194 195 if err := json.NewEncoder(w).Encode(response); err != nil { 196 http.Error(w, fmt.Sprint("could not encode response:", err), http.StatusInternalServerError) 197 return 198 } 199 span.SetStatus(codes.Ok, "") 200 } 201 } 202 203 // StatelessAdmissionImpl marks a reconciler as stateless. 204 // Inline this type to implement StatelessAdmissionController. 205 type StatelessAdmissionImpl struct{} 206 207 func (sai StatelessAdmissionImpl) ThisTypeDoesNotDependOnInformerState() {}