1
16
17 package conversion
18
19 import (
20 "context"
21 "errors"
22 "fmt"
23 "time"
24
25 "go.opentelemetry.io/otel/attribute"
26
27 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
29 apivalidation "k8s.io/apimachinery/pkg/api/validation"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
33 "k8s.io/apimachinery/pkg/runtime"
34 "k8s.io/apimachinery/pkg/runtime/schema"
35 "k8s.io/apimachinery/pkg/types"
36 "k8s.io/apimachinery/pkg/util/uuid"
37 "k8s.io/apimachinery/pkg/util/validation/field"
38 "k8s.io/apiserver/pkg/util/webhook"
39 "k8s.io/client-go/rest"
40 "k8s.io/component-base/tracing"
41 )
42
43 type webhookConverterFactory struct {
44 clientManager webhook.ClientManager
45 }
46
47 func newWebhookConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*webhookConverterFactory, error) {
48 clientManager, err := webhook.NewClientManager(
49 []schema.GroupVersion{v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion},
50 v1beta1.AddToScheme,
51 v1.AddToScheme,
52 )
53 if err != nil {
54 return nil, err
55 }
56 authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver("")
57 if err != nil {
58 return nil, err
59 }
60
61 clientManager.SetAuthenticationInfoResolver(authInfoResolver)
62 clientManager.SetAuthenticationInfoResolverWrapper(authResolverWrapper)
63 clientManager.SetServiceResolver(serviceResolver)
64 return &webhookConverterFactory{clientManager}, nil
65 }
66
67
68 type webhookConverter struct {
69 clientManager webhook.ClientManager
70 restClient *rest.RESTClient
71 name string
72 nopConverter nopConverter
73
74 conversionReviewVersions []string
75 }
76
77 func webhookClientConfigForCRD(crd *v1.CustomResourceDefinition) *webhook.ClientConfig {
78 apiConfig := crd.Spec.Conversion.Webhook.ClientConfig
79 ret := webhook.ClientConfig{
80 Name: fmt.Sprintf("conversion_webhook_for_%s", crd.Name),
81 CABundle: apiConfig.CABundle,
82 }
83 if apiConfig.URL != nil {
84 ret.URL = *apiConfig.URL
85 }
86 if apiConfig.Service != nil {
87 ret.Service = &webhook.ClientConfigService{
88 Name: apiConfig.Service.Name,
89 Namespace: apiConfig.Service.Namespace,
90 Port: *apiConfig.Service.Port,
91 }
92 if apiConfig.Service.Path != nil {
93 ret.Service.Path = *apiConfig.Service.Path
94 }
95 }
96 return &ret
97 }
98
99 var _ crConverterInterface = &webhookConverter{}
100
101 func (f *webhookConverterFactory) NewWebhookConverter(crd *v1.CustomResourceDefinition) (*webhookConverter, error) {
102 restClient, err := f.clientManager.HookClient(*webhookClientConfigForCRD(crd))
103 if err != nil {
104 return nil, err
105 }
106 return &webhookConverter{
107 clientManager: f.clientManager,
108 restClient: restClient,
109 name: crd.Name,
110 nopConverter: nopConverter{},
111
112 conversionReviewVersions: crd.Spec.Conversion.Webhook.ConversionReviewVersions,
113 }, nil
114 }
115
116
117
118
119
120 func getObjectsToConvert(obj runtime.Object, apiVersion string) []runtime.RawExtension {
121 listObj, isList := obj.(*unstructured.UnstructuredList)
122 var objects []runtime.RawExtension
123 if isList {
124 for i := range listObj.Items {
125
126 if listObj.Items[i].GetAPIVersion() != apiVersion {
127 objects = append(objects, runtime.RawExtension{Object: &listObj.Items[i]})
128 }
129 }
130 } else {
131 if obj.GetObjectKind().GroupVersionKind().GroupVersion().String() != apiVersion {
132 objects = []runtime.RawExtension{{Object: obj}}
133 }
134 }
135 return objects
136 }
137
138
139 func createConversionReviewObjects(conversionReviewVersions []string, objects []runtime.RawExtension, apiVersion string, requestUID types.UID) (request, response runtime.Object, err error) {
140 for _, version := range conversionReviewVersions {
141 switch version {
142 case v1beta1.SchemeGroupVersion.Version:
143 return &v1beta1.ConversionReview{
144 Request: &v1beta1.ConversionRequest{
145 Objects: objects,
146 DesiredAPIVersion: apiVersion,
147 UID: requestUID,
148 },
149 Response: &v1beta1.ConversionResponse{},
150 }, &v1beta1.ConversionReview{}, nil
151 case v1.SchemeGroupVersion.Version:
152 return &v1.ConversionReview{
153 Request: &v1.ConversionRequest{
154 Objects: objects,
155 DesiredAPIVersion: apiVersion,
156 UID: requestUID,
157 },
158 Response: &v1.ConversionResponse{},
159 }, &v1.ConversionReview{}, nil
160 }
161 }
162 return nil, nil, fmt.Errorf("no supported conversion review versions")
163 }
164
165 func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
166 if rx.Object != nil {
167 return rx.Object, nil
168 }
169 u := unstructured.Unstructured{}
170 err := u.UnmarshalJSON(rx.Raw)
171 if err != nil {
172 return nil, err
173 }
174 return &u, nil
175 }
176
177
178
179
180 func getConvertedObjectsFromResponse(expectedUID types.UID, response runtime.Object) (convertedObjects []runtime.RawExtension, err error) {
181 switch response := response.(type) {
182 case *v1.ConversionReview:
183
184 v1GVK := v1.SchemeGroupVersion.WithKind("ConversionReview")
185 if response.GroupVersionKind() != v1GVK {
186 return nil, fmt.Errorf("expected webhook response of %v, got %v", v1GVK.String(), response.GroupVersionKind().String())
187 }
188
189 if response.Response == nil {
190 return nil, fmt.Errorf("no response provided")
191 }
192
193
194 if response.Response.UID != expectedUID {
195 return nil, fmt.Errorf("expected response.uid=%q, got %q", expectedUID, response.Response.UID)
196 }
197
198 if response.Response.Result.Status != metav1.StatusSuccess {
199
200 if len(response.Response.Result.Message) > 0 {
201 return nil, errors.New(response.Response.Result.Message)
202 }
203 return nil, fmt.Errorf("response.result.status was '%s', not 'Success'", response.Response.Result.Status)
204 }
205
206 return response.Response.ConvertedObjects, nil
207
208 case *v1beta1.ConversionReview:
209
210
211 if response.Response == nil {
212 return nil, fmt.Errorf("no response provided")
213 }
214
215 if response.Response.Result.Status != metav1.StatusSuccess {
216
217 if len(response.Response.Result.Message) > 0 {
218 return nil, errors.New(response.Response.Result.Message)
219 }
220 return nil, fmt.Errorf("response.result.status was '%s', not 'Success'", response.Response.Result.Status)
221 }
222
223 return response.Response.ConvertedObjects, nil
224
225 default:
226 return nil, fmt.Errorf("unrecognized response type: %T", response)
227 }
228 }
229
230 func (c *webhookConverter) Convert(in runtime.Object, toGV schema.GroupVersion) (runtime.Object, error) {
231 ctx := context.TODO()
232
233
234
235
236
237 if isEmptyUnstructuredObject(in) {
238 return c.nopConverter.Convert(in, toGV)
239 }
240 t := time.Now()
241 listObj, isList := in.(*unstructured.UnstructuredList)
242
243 requestUID := uuid.NewUUID()
244 desiredAPIVersion := toGV.String()
245 objectsToConvert := getObjectsToConvert(in, desiredAPIVersion)
246 request, response, err := createConversionReviewObjects(c.conversionReviewVersions, objectsToConvert, desiredAPIVersion, requestUID)
247 if err != nil {
248 return nil, err
249 }
250
251 objCount := len(objectsToConvert)
252 if objCount == 0 {
253 Metrics.ObserveConversionWebhookSuccess(ctx, time.Since(t))
254
255 if !isList {
256
257 return in, nil
258 }
259
260 out := listObj.DeepCopy()
261 out.SetAPIVersion(toGV.String())
262 return out, nil
263 }
264
265 ctx, span := tracing.Start(ctx, "Call conversion webhook",
266 attribute.String("custom-resource-definition", c.name),
267 attribute.String("desired-api-version", desiredAPIVersion),
268 attribute.Int("object-count", objCount),
269 attribute.String("UID", string(requestUID)))
270
271
272
273 defer span.End(time.Duration(50+8*objCount) * time.Millisecond)
274
275
276 r := c.restClient.Post().Body(request).Do(ctx)
277 if err := r.Into(response); err != nil {
278
279 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookCallFailure)
280 return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
281 }
282 span.AddEvent("Request completed")
283
284 convertedObjects, err := getConvertedObjectsFromResponse(requestUID, response)
285 if err != nil {
286 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookMalformedResponseFailure)
287 return nil, fmt.Errorf("conversion webhook for %v failed: %v", in.GetObjectKind().GroupVersionKind(), err)
288 }
289
290 if len(convertedObjects) != len(objectsToConvert) {
291 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookPartialResponseFailure)
292 return nil, fmt.Errorf("conversion webhook for %v returned %d objects, expected %d", in.GetObjectKind().GroupVersionKind(), len(convertedObjects), len(objectsToConvert))
293 }
294
295 if isList {
296
297
298 convertedList := listObj.DeepCopy()
299 convertedIndex := 0
300 for i := range convertedList.Items {
301 original := &convertedList.Items[i]
302 if original.GetAPIVersion() == toGV.String() {
303
304
305 continue
306 }
307 converted, err := getRawExtensionObject(convertedObjects[convertedIndex])
308 if err != nil {
309 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
310 return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
311 }
312 if expected, got := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); expected != got {
313 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
314 return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid groupVersion (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
315 }
316 if expected, got := original.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; expected != got {
317 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
318 return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid kind (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), convertedIndex, expected, got)
319 }
320 unstructConverted, ok := converted.(*unstructured.Unstructured)
321 if !ok {
322
323 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
324 return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: invalid type, expected=Unstructured, got=%T", in.GetObjectKind().GroupVersionKind(), convertedIndex, converted)
325 }
326 if err := validateConvertedObject(original, unstructConverted); err != nil {
327 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
328 return nil, fmt.Errorf("conversion webhook for %v returned invalid converted object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
329 }
330 if err := restoreObjectMeta(original, unstructConverted); err != nil {
331 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
332 return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata in object at index %v: %v", in.GetObjectKind().GroupVersionKind(), convertedIndex, err)
333 }
334 convertedIndex++
335 convertedList.Items[i] = *unstructConverted
336 }
337 convertedList.SetAPIVersion(toGV.String())
338 Metrics.ObserveConversionWebhookSuccess(ctx, time.Since(t))
339 return convertedList, nil
340 }
341
342 if len(convertedObjects) != 1 {
343
344 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookNoObjectsReturnedFailure)
345 return nil, fmt.Errorf("conversion webhook for %v failed, no objects returned", in.GetObjectKind())
346 }
347 converted, err := getRawExtensionObject(convertedObjects[0])
348 if err != nil {
349 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
350 return nil, err
351 }
352 if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a {
353 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
354 return nil, fmt.Errorf("conversion webhook for %v returned invalid object at index 0: invalid groupVersion (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), e, a)
355 }
356 if e, a := in.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a {
357 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
358 return nil, fmt.Errorf("conversion webhook for %v returned invalid object at index 0: invalid kind (expected %v, received %v)", in.GetObjectKind().GroupVersionKind(), e, a)
359 }
360 unstructConverted, ok := converted.(*unstructured.Unstructured)
361 if !ok {
362
363 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
364 return nil, fmt.Errorf("conversion webhook for %v failed, unexpected type %T at index 0", in.GetObjectKind().GroupVersionKind(), converted)
365 }
366 unstructIn, ok := in.(*unstructured.Unstructured)
367 if !ok {
368
369 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
370 return nil, fmt.Errorf("conversion webhook for %v failed unexpected input type %T", in.GetObjectKind().GroupVersionKind(), in)
371 }
372 if err := validateConvertedObject(unstructIn, unstructConverted); err != nil {
373 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
374 return nil, fmt.Errorf("conversion webhook for %v returned invalid object: %v", in.GetObjectKind().GroupVersionKind(), err)
375 }
376 if err := restoreObjectMeta(unstructIn, unstructConverted); err != nil {
377 Metrics.ObserveConversionWebhookFailure(ctx, time.Since(t), ConversionWebhookInvalidConvertedObjectFailure)
378 return nil, fmt.Errorf("conversion webhook for %v returned invalid metadata: %v", in.GetObjectKind().GroupVersionKind(), err)
379 }
380 Metrics.ObserveConversionWebhookSuccess(ctx, time.Since(t))
381 return converted, nil
382 }
383
384
385
386 func validateConvertedObject(in, out *unstructured.Unstructured) error {
387 if e, a := in.GetKind(), out.GetKind(); e != a {
388 return fmt.Errorf("must have the same kind: %v != %v", e, a)
389 }
390 if e, a := in.GetName(), out.GetName(); e != a {
391 return fmt.Errorf("must have the same name: %v != %v", e, a)
392 }
393 if e, a := in.GetNamespace(), out.GetNamespace(); e != a {
394 return fmt.Errorf("must have the same namespace: %v != %v", e, a)
395 }
396 if e, a := in.GetUID(), out.GetUID(); e != a {
397 return fmt.Errorf("must have the same UID: %v != %v", e, a)
398 }
399 return nil
400 }
401
402
403 func restoreObjectMeta(original, converted *unstructured.Unstructured) error {
404 obj, found := converted.Object["metadata"]
405 if !found {
406 return fmt.Errorf("missing metadata in converted object")
407 }
408 responseMetaData, ok := obj.(map[string]interface{})
409 if !ok {
410 return fmt.Errorf("invalid metadata of type %T in converted object", obj)
411 }
412
413 if _, ok := original.Object["metadata"]; !ok {
414
415
416 converted.Object["metadata"] = map[string]interface{}{}
417 } else {
418 converted.Object["metadata"] = runtime.DeepCopyJSONValue(original.Object["metadata"])
419 }
420
421 obj = converted.Object["metadata"]
422 convertedMetaData, ok := obj.(map[string]interface{})
423 if !ok {
424 return fmt.Errorf("invalid metadata of type %T in input object", obj)
425 }
426
427 for _, fld := range []string{"labels", "annotations"} {
428 obj, found := responseMetaData[fld]
429 if !found || obj == nil {
430 delete(convertedMetaData, fld)
431 continue
432 }
433 responseField, ok := obj.(map[string]interface{})
434 if !ok {
435 return fmt.Errorf("invalid metadata.%s of type %T in converted object", fld, obj)
436 }
437
438 originalField, ok := convertedMetaData[fld].(map[string]interface{})
439 if !ok && convertedMetaData[fld] != nil {
440 return fmt.Errorf("invalid metadata.%s of type %T in original object", fld, convertedMetaData[fld])
441 }
442
443 somethingChanged := len(originalField) != len(responseField)
444 for k, v := range responseField {
445 if _, ok := v.(string); !ok {
446 return fmt.Errorf("metadata.%s[%s] must be a string, but is %T in converted object", fld, k, v)
447 }
448 if originalField[k] != interface{}(v) {
449 somethingChanged = true
450 }
451 }
452
453 if somethingChanged {
454 stringMap := make(map[string]string, len(responseField))
455 for k, v := range responseField {
456 stringMap[k] = v.(string)
457 }
458 var errs field.ErrorList
459 if fld == "labels" {
460 errs = metav1validation.ValidateLabels(stringMap, field.NewPath("metadata", "labels"))
461 } else {
462 errs = apivalidation.ValidateAnnotations(stringMap, field.NewPath("metadata", "annotation"))
463 }
464 if len(errs) > 0 {
465 return errs.ToAggregate()
466 }
467 }
468
469 convertedMetaData[fld] = responseField
470 }
471
472 return nil
473 }
474
475
476
477 func isEmptyUnstructuredObject(in runtime.Object) bool {
478 u, ok := in.(*unstructured.Unstructured)
479 if !ok {
480 return false
481 }
482 if len(u.Object) != 2 {
483 return false
484 }
485 if _, ok := u.Object["kind"]; !ok {
486 return false
487 }
488 if _, ok := u.Object["apiVersion"]; !ok {
489 return false
490 }
491 return true
492 }
493
View as plain text