k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/images/agnhost/crd-conversion-webhook/converter/framework.go (about) 1 /* 2 Copyright 2018 The Kubernetes 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 converter 18 19 import ( 20 "fmt" 21 "io" 22 "net/http" 23 "strings" 24 25 "github.com/munnerz/goautoneg" 26 27 "k8s.io/klog/v2" 28 29 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 30 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/serializer/json" 35 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 36 ) 37 38 // convertFunc is the user defined function for any conversion. The code in this file is a 39 // template that can be use for any CR conversion given this function. 40 type convertFunc func(Object *unstructured.Unstructured, version string) (*unstructured.Unstructured, metav1.Status) 41 42 func statusErrorWithMessage(msg string, params ...interface{}) metav1.Status { 43 return metav1.Status{ 44 Message: fmt.Sprintf(msg, params...), 45 Status: metav1.StatusFailure, 46 } 47 } 48 49 func statusSucceed() metav1.Status { 50 return metav1.Status{ 51 Status: metav1.StatusSuccess, 52 } 53 } 54 55 // doConversionV1beta1 converts the requested objects in the v1beta1 ConversionRequest using the given conversion function and 56 // returns a conversion response. Failures are reported with the Reason in the conversion response. 57 func doConversionV1beta1(convertRequest *v1beta1.ConversionRequest, convert convertFunc) *v1beta1.ConversionResponse { 58 var convertedObjects []runtime.RawExtension 59 for _, obj := range convertRequest.Objects { 60 cr := unstructured.Unstructured{} 61 if err := cr.UnmarshalJSON(obj.Raw); err != nil { 62 klog.Error(err) 63 return &v1beta1.ConversionResponse{ 64 Result: metav1.Status{ 65 Message: fmt.Sprintf("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err), 66 Status: metav1.StatusFailure, 67 }, 68 } 69 } 70 convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion) 71 if status.Status != metav1.StatusSuccess { 72 klog.Error(status.String()) 73 return &v1beta1.ConversionResponse{ 74 Result: status, 75 } 76 } 77 convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion) 78 convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR}) 79 } 80 return &v1beta1.ConversionResponse{ 81 ConvertedObjects: convertedObjects, 82 Result: statusSucceed(), 83 } 84 } 85 86 // doConversionV1 converts the requested objects in the v1 ConversionRequest using the given conversion function and 87 // returns a conversion response. Failures are reported with the Reason in the conversion response. 88 func doConversionV1(convertRequest *v1.ConversionRequest, convert convertFunc) *v1.ConversionResponse { 89 var convertedObjects []runtime.RawExtension 90 for _, obj := range convertRequest.Objects { 91 cr := unstructured.Unstructured{} 92 if err := cr.UnmarshalJSON(obj.Raw); err != nil { 93 klog.Error(err) 94 return &v1.ConversionResponse{ 95 Result: metav1.Status{ 96 Message: fmt.Sprintf("failed to unmarshall object (%v) with error: %v", string(obj.Raw), err), 97 Status: metav1.StatusFailure, 98 }, 99 } 100 } 101 convertedCR, status := convert(&cr, convertRequest.DesiredAPIVersion) 102 if status.Status != metav1.StatusSuccess { 103 klog.Error(status.String()) 104 return &v1.ConversionResponse{ 105 Result: status, 106 } 107 } 108 convertedCR.SetAPIVersion(convertRequest.DesiredAPIVersion) 109 convertedObjects = append(convertedObjects, runtime.RawExtension{Object: convertedCR}) 110 } 111 return &v1.ConversionResponse{ 112 ConvertedObjects: convertedObjects, 113 Result: statusSucceed(), 114 } 115 } 116 117 func serve(w http.ResponseWriter, r *http.Request, convert convertFunc) { 118 var body []byte 119 if r.Body != nil { 120 if data, err := io.ReadAll(r.Body); err == nil { 121 body = data 122 } 123 } 124 125 contentType := r.Header.Get("Content-Type") 126 serializer := getInputSerializer(contentType) 127 if serializer == nil { 128 msg := fmt.Sprintf("invalid Content-Type header `%s`", contentType) 129 klog.Errorf(msg) 130 http.Error(w, msg, http.StatusBadRequest) 131 return 132 } 133 134 klog.V(2).Infof("handling request: %v", body) 135 obj, gvk, err := serializer.Decode(body, nil, nil) 136 if err != nil { 137 msg := fmt.Sprintf("failed to deserialize body (%v) with error %v", string(body), err) 138 klog.Error(err) 139 http.Error(w, msg, http.StatusBadRequest) 140 return 141 } 142 143 var responseObj runtime.Object 144 switch *gvk { 145 case v1beta1.SchemeGroupVersion.WithKind("ConversionReview"): 146 convertReview, ok := obj.(*v1beta1.ConversionReview) 147 if !ok { 148 msg := fmt.Sprintf("Expected v1beta1.ConversionReview but got: %T", obj) 149 klog.Errorf(msg) 150 http.Error(w, msg, http.StatusBadRequest) 151 return 152 } 153 convertReview.Response = doConversionV1beta1(convertReview.Request, convert) 154 convertReview.Response.UID = convertReview.Request.UID 155 klog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response)) 156 157 // reset the request, it is not needed in a response. 158 convertReview.Request = &v1beta1.ConversionRequest{} 159 responseObj = convertReview 160 case v1.SchemeGroupVersion.WithKind("ConversionReview"): 161 convertReview, ok := obj.(*v1.ConversionReview) 162 if !ok { 163 msg := fmt.Sprintf("Expected v1.ConversionReview but got: %T", obj) 164 klog.Errorf(msg) 165 http.Error(w, msg, http.StatusBadRequest) 166 return 167 } 168 convertReview.Response = doConversionV1(convertReview.Request, convert) 169 convertReview.Response.UID = convertReview.Request.UID 170 klog.V(2).Info(fmt.Sprintf("sending response: %v", convertReview.Response)) 171 172 // reset the request, it is not needed in a response. 173 convertReview.Request = &v1.ConversionRequest{} 174 responseObj = convertReview 175 default: 176 msg := fmt.Sprintf("Unsupported group version kind: %v", gvk) 177 klog.Error(err) 178 http.Error(w, msg, http.StatusBadRequest) 179 return 180 } 181 182 accept := r.Header.Get("Accept") 183 outSerializer := getOutputSerializer(accept) 184 if outSerializer == nil { 185 msg := fmt.Sprintf("invalid accept header `%s`", accept) 186 klog.Errorf(msg) 187 http.Error(w, msg, http.StatusBadRequest) 188 return 189 } 190 err = outSerializer.Encode(responseObj, w) 191 if err != nil { 192 klog.Error(err) 193 http.Error(w, err.Error(), http.StatusInternalServerError) 194 return 195 } 196 } 197 198 // ServeExampleConvert servers endpoint for the example converter defined as convertExampleCRD function. 199 func ServeExampleConvert(w http.ResponseWriter, r *http.Request) { 200 serve(w, r, convertExampleCRD) 201 } 202 203 type mediaType struct { 204 Type, SubType string 205 } 206 207 var scheme = runtime.NewScheme() 208 209 func init() { 210 addToScheme(scheme) 211 } 212 213 func addToScheme(scheme *runtime.Scheme) { 214 utilruntime.Must(v1.AddToScheme(scheme)) 215 utilruntime.Must(v1beta1.AddToScheme(scheme)) 216 } 217 218 var serializers = map[mediaType]runtime.Serializer{ 219 {"application", "json"}: json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{Pretty: false}), 220 {"application", "yaml"}: json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{Yaml: true}), 221 } 222 223 func getInputSerializer(contentType string) runtime.Serializer { 224 parts := strings.SplitN(contentType, "/", 2) 225 if len(parts) != 2 { 226 return nil 227 } 228 return serializers[mediaType{parts[0], parts[1]}] 229 } 230 231 func getOutputSerializer(accept string) runtime.Serializer { 232 if len(accept) == 0 { 233 return serializers[mediaType{"application", "json"}] 234 } 235 236 clauses := goautoneg.ParseAccept(accept) 237 for _, clause := range clauses { 238 for k, v := range serializers { 239 switch { 240 case clause.Type == k.Type && clause.SubType == k.SubType, 241 clause.Type == k.Type && clause.SubType == "*", 242 clause.Type == "*" && clause.SubType == "*": 243 return v 244 } 245 } 246 } 247 248 return nil 249 }