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() {}