knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/conversion.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  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/http"
    24  	"strings"
    25  	"time"
    26  
    27  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    28  	"go.opentelemetry.io/otel/codes"
    29  	"go.opentelemetry.io/otel/metric"
    30  	"go.opentelemetry.io/otel/trace"
    31  	"go.uber.org/zap"
    32  	apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  
    36  	"knative.dev/pkg/apis"
    37  	"knative.dev/pkg/logging"
    38  )
    39  
    40  // ConversionController provides the interface for different conversion controllers
    41  type ConversionController interface {
    42  	// Path returns the path that this particular conversion controller serves on.
    43  	Path() string
    44  
    45  	// Convert is the callback which is invoked when an HTTPS request comes in on Path().
    46  	Convert(context.Context, *apixv1.ConversionRequest) *apixv1.ConversionResponse
    47  }
    48  
    49  func conversionHandler(wh *Webhook, c ConversionController) http.HandlerFunc {
    50  	return func(w http.ResponseWriter, r *http.Request) {
    51  		logger := wh.Logger
    52  		logger.Infof("Webhook ServeHTTP request=%#v", r)
    53  
    54  		span := trace.SpanFromContext(r.Context())
    55  		// otelhttp middleware creates the labeler
    56  		labeler, _ := otelhttp.LabelerFromContext(r.Context())
    57  
    58  		defer func() {
    59  			// otelhttp doesn't add labeler attributes to spans
    60  			// so we have to do it manually
    61  			span.SetAttributes(labeler.Get()...)
    62  		}()
    63  
    64  		var review apixv1.ConversionReview
    65  		if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
    66  			msg := fmt.Sprint("could not decode body:", err)
    67  			span.SetStatus(codes.Error, msg)
    68  			http.Error(w, msg, http.StatusBadRequest)
    69  			return
    70  		}
    71  
    72  		gv, err := parseAPIVersion(review.Request.DesiredAPIVersion)
    73  		if err != nil {
    74  			msg := fmt.Sprint("could parse desired api version:", err)
    75  			span.SetStatus(codes.Error, msg)
    76  			http.Error(w, msg, http.StatusBadRequest)
    77  			return
    78  		}
    79  
    80  		labeler.Add(
    81  			WebhookTypeAttr.With(WebhookTypeConversion),
    82  			GroupAttr.With(gv.Group),
    83  			VersionAttr.With(gv.Version),
    84  		)
    85  
    86  		logger = logger.With(
    87  			zap.String("uid", string(review.Request.UID)),
    88  			zap.String("desiredAPIVersion", review.Request.DesiredAPIVersion),
    89  		)
    90  
    91  		ctx := logging.WithLogger(r.Context(), logger)
    92  		ctx = apis.WithHTTPRequest(ctx, r)
    93  
    94  		ttStart := time.Now()
    95  		response := apixv1.ConversionReview{
    96  			// Use the same type meta as the request - this is required by the K8s API
    97  			// note: v1beta1 & v1 ConversionReview shapes are identical so even though
    98  			// we're using v1 types we still support v1beta1 conversion requests
    99  			TypeMeta: review.TypeMeta,
   100  			Response: c.Convert(ctx, review.Request),
   101  		}
   102  
   103  		labeler.Add(
   104  			StatusAttr.With(strings.ToLower(response.Response.Result.Status)),
   105  		)
   106  
   107  		if response.Response.Result.Status == metav1.StatusFailure {
   108  			span.SetStatus(codes.Error, response.Response.Result.Message)
   109  		}
   110  
   111  		wh.metrics.recordHandlerDuration(ctx, time.Since(ttStart),
   112  			metric.WithAttributes(labeler.Get()...),
   113  		)
   114  
   115  		if err := json.NewEncoder(w).Encode(response); err != nil {
   116  			http.Error(w, fmt.Sprint("could not encode response:", err), http.StatusInternalServerError)
   117  			return
   118  		}
   119  
   120  		span.SetStatus(codes.Ok, "")
   121  	}
   122  }
   123  
   124  func parseAPIVersion(apiVersion string) (schema.GroupVersion, error) {
   125  	gv, err := schema.ParseGroupVersion(apiVersion)
   126  	if err != nil {
   127  		err = fmt.Errorf("desired API version %q is not valid", apiVersion)
   128  		return schema.GroupVersion{}, err
   129  	}
   130  
   131  	if !isValidGV(gv) {
   132  		err = fmt.Errorf("desired API version %q is not valid", apiVersion)
   133  		return schema.GroupVersion{}, err
   134  	}
   135  
   136  	return gv, nil
   137  }
   138  
   139  func isValidGV(gk schema.GroupVersion) bool {
   140  	return gk.Group != "" && gk.Version != ""
   141  }