...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/webhook_converter.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver/conversion

     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 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  	// Set defaults which may be overridden later.
    61  	clientManager.SetAuthenticationInfoResolver(authInfoResolver)
    62  	clientManager.SetAuthenticationInfoResolverWrapper(authResolverWrapper)
    63  	clientManager.SetServiceResolver(serviceResolver)
    64  	return &webhookConverterFactory{clientManager}, nil
    65  }
    66  
    67  // webhookConverter is a converter that calls an external webhook to do the CR conversion.
    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  // getObjectsToConvert returns a list of objects requiring conversion.
   117  // if obj is a list, getObjectsToConvert returns a (potentially empty) list of the items that are not already in the desired version.
   118  // if obj is not a list, and is already in the desired version, getObjectsToConvert returns an empty list.
   119  // if obj is not a list, and is not already in the desired version, getObjectsToConvert returns a list containing only obj.
   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  			// Only sent item for conversion, if the apiVersion is different
   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  // createConversionReviewObjects returns ConversionReview request and response objects for the first supported version found in conversionReviewVersions.
   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  // getConvertedObjectsFromResponse validates the response, and returns the converted objects.
   178  // if the response is malformed, an error is returned instead.
   179  // if the response does not indicate success, the error message is returned instead.
   180  func getConvertedObjectsFromResponse(expectedUID types.UID, response runtime.Object) (convertedObjects []runtime.RawExtension, err error) {
   181  	switch response := response.(type) {
   182  	case *v1.ConversionReview:
   183  		// Verify GVK to make sure we decoded what we intended to
   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  		// Verify UID to make sure this response was actually meant for the request we sent
   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  			// TODO: Return a webhook specific error to be able to convert it to meta.Status
   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  		// v1beta1 processing did not verify GVK or UID, so skip those for compatibility
   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  			// TODO: Return a webhook specific error to be able to convert it to meta.Status
   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  	// In general, the webhook should not do any defaulting or validation. A special case of that is an empty object
   233  	// conversion that must result an empty object and practically is the same as nopConverter.
   234  	// A smoke test in API machinery calls the converter on empty objects. As this case happens consistently
   235  	// it special cased here not to call webhook converter. The test initiated here:
   236  	// https://github.com/kubernetes/kubernetes/blob/dbb448bbdcb9e440eee57024ffa5f1698956a054/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L201
   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  		// no objects needed conversion
   255  		if !isList {
   256  			// for a single item, return as-is
   257  			return in, nil
   258  		}
   259  		// for a list, set the version of the top-level list object (all individual objects are already in the correct version)
   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  	// Only log conversion webhook traces that exceed a 8ms per object limit plus a 50ms request overhead allowance.
   271  	// The per object limit uses the SLO for conversion webhooks (~4ms per object) plus time to serialize/deserialize
   272  	// the conversion request on the apiserver side (~4ms per object).
   273  	defer span.End(time.Duration(50+8*objCount) * time.Millisecond)
   274  
   275  	// TODO: Figure out if adding one second timeout make sense here.
   276  	r := c.restClient.Post().Body(request).Do(ctx)
   277  	if err := r.Into(response); err != nil {
   278  		// TODO: Return a webhook specific error to be able to convert it to meta.Status
   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  		// start a deepcopy of the input and fill in the converted objects from the response at the right spots.
   297  		// The response list might be sparse because objects had the right version already.
   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  				// This item has not been sent for conversion, and therefore does not show up in the response.
   304  				// convertedList has the right item already.
   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  				// this should not happened
   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  		// This should not happened
   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  		// this should not happened
   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  		// this should not happened
   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  // validateConvertedObject checks that ObjectMeta fields match, with the exception of
   385  // labels and annotations.
   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  // restoreObjectMeta deep-copies metadata from original into converted, while preserving labels and annotations from converted.
   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  		// the original will always have metadata. But just to be safe, let's clear in converted
   415  		// with an empty object instead of nil, to be able to add labels and annotations below.
   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  // isEmptyUnstructuredObject returns true if in is an empty unstructured object, i.e. an unstructured object that does
   476  // not have any field except apiVersion and kind.
   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