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 }