...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion/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  	"fmt"
    21  	"strings"
    22  
    23  	autoscalingv1 "k8s.io/api/autoscaling/v1"
    24  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    25  	apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/util/sets"
    30  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    31  	"k8s.io/apiserver/pkg/util/webhook"
    32  	typedscheme "k8s.io/client-go/kubernetes/scheme"
    33  )
    34  
    35  // CRConverterFactory is the factory for all CR converters.
    36  type CRConverterFactory struct {
    37  	// webhookConverterFactory is the factory for webhook converters.
    38  	// This field should not be used if CustomResourceWebhookConversion feature is disabled.
    39  	webhookConverterFactory *webhookConverterFactory
    40  }
    41  
    42  // converterMetricFactorySingleton protects us from reregistration of metrics on repeated
    43  // apiextensions-apiserver runs.
    44  var converterMetricFactorySingleton = newConverterMetricFactory()
    45  
    46  // NewCRConverterFactory creates a new CRConverterFactory
    47  func NewCRConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*CRConverterFactory, error) {
    48  	converterFactory := &CRConverterFactory{}
    49  	webhookConverterFactory, err := newWebhookConverterFactory(serviceResolver, authResolverWrapper)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	converterFactory.webhookConverterFactory = webhookConverterFactory
    54  	return converterFactory, nil
    55  }
    56  
    57  // NewConverter returns a new CR converter based on the conversion settings in crd object.
    58  func (m *CRConverterFactory) NewConverter(crd *apiextensionsv1.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor, err error) {
    59  	validVersions := map[schema.GroupVersion]bool{}
    60  	for _, version := range crd.Spec.Versions {
    61  		validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
    62  	}
    63  
    64  	var converter crConverterInterface
    65  	switch crd.Spec.Conversion.Strategy {
    66  	case apiextensionsv1.NoneConverter:
    67  		converter = &nopConverter{}
    68  	case apiextensionsv1.WebhookConverter:
    69  		converter, err = m.webhookConverterFactory.NewWebhookConverter(crd)
    70  		if err != nil {
    71  			return nil, nil, err
    72  		}
    73  		converter, err = converterMetricFactorySingleton.addMetrics(crd.Name, converter)
    74  		if err != nil {
    75  			return nil, nil, err
    76  		}
    77  	default:
    78  		return nil, nil, fmt.Errorf("unknown conversion strategy %q for CRD %s", crd.Spec.Conversion.Strategy, crd.Name)
    79  	}
    80  
    81  	// Determine whether we should expect to be asked to "convert" autoscaling/v1 Scale types
    82  	convertScale := false
    83  	selectableFields := map[schema.GroupVersion]sets.Set[string]{}
    84  	for _, version := range crd.Spec.Versions {
    85  		gv := schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}
    86  		if version.Subresources != nil && version.Subresources.Scale != nil {
    87  			convertScale = true
    88  		}
    89  		if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) {
    90  			fieldPaths := sets.New[string]()
    91  			for _, sf := range version.SelectableFields {
    92  				fieldPaths.Insert(strings.TrimPrefix(sf.JSONPath, "."))
    93  			}
    94  			selectableFields[gv] = fieldPaths
    95  		}
    96  	}
    97  
    98  	unsafe = &crConverter{
    99  		convertScale:     convertScale,
   100  		validVersions:    validVersions,
   101  		clusterScoped:    crd.Spec.Scope == apiextensionsv1.ClusterScoped,
   102  		converter:        converter,
   103  		selectableFields: selectableFields,
   104  	}
   105  	return &safeConverterWrapper{unsafe}, unsafe, nil
   106  }
   107  
   108  // crConverterInterface is the interface all cr converters must implement
   109  type crConverterInterface interface {
   110  	// Convert converts in object to the given gvk and returns the converted object.
   111  	// Note that the function may mutate in object and return it. A safe wrapper will make sure
   112  	// a safe converter will be returned.
   113  	Convert(in runtime.Object, targetGVK schema.GroupVersion) (runtime.Object, error)
   114  }
   115  
   116  // crConverter extends the delegate converter with generic CR conversion behaviour. The delegate will implement the
   117  // user defined conversion strategy given in the CustomResourceDefinition.
   118  type crConverter struct {
   119  	convertScale     bool
   120  	converter        crConverterInterface
   121  	validVersions    map[schema.GroupVersion]bool
   122  	clusterScoped    bool
   123  	selectableFields map[schema.GroupVersion]sets.Set[string]
   124  }
   125  
   126  func (c *crConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
   127  	switch {
   128  	case label == "metadata.name":
   129  		return label, value, nil
   130  	case !c.clusterScoped && label == "metadata.namespace":
   131  		return label, value, nil
   132  	default:
   133  		if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceFieldSelectors) {
   134  			groupFields := c.selectableFields[gvk.GroupVersion()]
   135  			if groupFields != nil && groupFields.Has(label) {
   136  				return label, value, nil
   137  			}
   138  		}
   139  		return "", "", fmt.Errorf("field label not supported: %s", label)
   140  	}
   141  }
   142  
   143  func (c *crConverter) Convert(in, out, context interface{}) error {
   144  	// Special-case typed scale conversion if this custom resource supports a scale endpoint
   145  	if c.convertScale {
   146  		_, isInScale := in.(*autoscalingv1.Scale)
   147  		_, isOutScale := out.(*autoscalingv1.Scale)
   148  		if isInScale || isOutScale {
   149  			return typedscheme.Scheme.Convert(in, out, context)
   150  		}
   151  	}
   152  
   153  	unstructIn, ok := in.(*unstructured.Unstructured)
   154  	if !ok {
   155  		return fmt.Errorf("input type %T in not valid for unstructured conversion to %T", in, out)
   156  	}
   157  
   158  	unstructOut, ok := out.(*unstructured.Unstructured)
   159  	if !ok {
   160  		return fmt.Errorf("output type %T in not valid for unstructured conversion from %T", out, in)
   161  	}
   162  
   163  	outGVK := unstructOut.GroupVersionKind()
   164  	converted, err := c.ConvertToVersion(unstructIn, outGVK.GroupVersion())
   165  	if err != nil {
   166  		return err
   167  	}
   168  	unstructuredConverted, ok := converted.(runtime.Unstructured)
   169  	if !ok {
   170  		// this should not happened
   171  		return fmt.Errorf("CR conversion failed")
   172  	}
   173  	unstructOut.SetUnstructuredContent(unstructuredConverted.UnstructuredContent())
   174  	return nil
   175  }
   176  
   177  // ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
   178  // The in object can be a single object or a UnstructuredList. CRD storage implementation creates an
   179  // UnstructuredList with the request's GV, populates it from storage, then calls conversion to convert
   180  // the individual items. This function assumes it never gets a v1.List.
   181  func (c *crConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
   182  	fromGVK := in.GetObjectKind().GroupVersionKind()
   183  	toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK})
   184  	if !ok {
   185  		// TODO: should this be a typed error?
   186  		return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target)
   187  	}
   188  	// Special-case typed scale conversion if this custom resource supports a scale endpoint
   189  	if c.convertScale {
   190  		if _, isInScale := in.(*autoscalingv1.Scale); isInScale {
   191  			return typedscheme.Scheme.ConvertToVersion(in, target)
   192  		}
   193  	}
   194  
   195  	if !c.validVersions[toGVK.GroupVersion()] {
   196  		return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", toGVK.GroupVersion().String())
   197  	}
   198  	// Note that even if the request is for a list, the GV of the request UnstructuredList is what
   199  	// is expected to convert to. As mentioned in the function's document, it is not expected to
   200  	// get a v1.List.
   201  	if !c.validVersions[fromGVK.GroupVersion()] {
   202  		return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGVK.GroupVersion().String())
   203  	}
   204  	// Check list item's apiVersion
   205  	if list, ok := in.(*unstructured.UnstructuredList); ok {
   206  		for i := range list.Items {
   207  			expectedGV := list.Items[i].GroupVersionKind().GroupVersion()
   208  			if !c.validVersions[expectedGV] {
   209  				return nil, fmt.Errorf("request to convert CR list failed, list index %d has invalid group/version: %s", i, expectedGV.String())
   210  			}
   211  		}
   212  	}
   213  	return c.converter.Convert(in, toGVK.GroupVersion())
   214  }
   215  
   216  // safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
   217  type safeConverterWrapper struct {
   218  	unsafe runtime.ObjectConvertor
   219  }
   220  
   221  var _ runtime.ObjectConvertor = &safeConverterWrapper{}
   222  
   223  // ConvertFieldLabel delegate the call to the unsafe converter.
   224  func (c *safeConverterWrapper) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
   225  	return c.unsafe.ConvertFieldLabel(gvk, label, value)
   226  }
   227  
   228  // Convert makes a copy of in object and then delegate the call to the unsafe converter.
   229  func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
   230  	inObject, ok := in.(runtime.Object)
   231  	if !ok {
   232  		return fmt.Errorf("input type %T in not valid for object conversion", in)
   233  	}
   234  	return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
   235  }
   236  
   237  // ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
   238  func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
   239  	return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
   240  }
   241  

View as plain text