1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package conversion
18
19import (
20	"fmt"
21
22	autoscalingv1 "k8s.io/api/autoscaling/v1"
23	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25	"k8s.io/apimachinery/pkg/runtime"
26	"k8s.io/apimachinery/pkg/runtime/schema"
27	"k8s.io/apiserver/pkg/util/webhook"
28	typedscheme "k8s.io/client-go/kubernetes/scheme"
29)
30
31// CRConverterFactory is the factory for all CR converters.
32type CRConverterFactory struct {
33	// webhookConverterFactory is the factory for webhook converters.
34	// This field should not be used if CustomResourceWebhookConversion feature is disabled.
35	webhookConverterFactory *webhookConverterFactory
36}
37
38// converterMetricFactorySingleton protects us from reregistration of metrics on repeated
39// apiextensions-apiserver runs.
40var converterMetricFactorySingleton = newConverterMertricFactory()
41
42// NewCRConverterFactory creates a new CRConverterFactory
43func NewCRConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*CRConverterFactory, error) {
44	converterFactory := &CRConverterFactory{}
45	webhookConverterFactory, err := newWebhookConverterFactory(serviceResolver, authResolverWrapper)
46	if err != nil {
47		return nil, err
48	}
49	converterFactory.webhookConverterFactory = webhookConverterFactory
50	return converterFactory, nil
51}
52
53// NewConverter returns a new CR converter based on the conversion settings in crd object.
54func (m *CRConverterFactory) NewConverter(crd *apiextensionsv1.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor, err error) {
55	validVersions := map[schema.GroupVersion]bool{}
56	for _, version := range crd.Spec.Versions {
57		validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
58	}
59
60	var converter crConverterInterface
61	switch crd.Spec.Conversion.Strategy {
62	case apiextensionsv1.NoneConverter:
63		converter = &nopConverter{}
64	case apiextensionsv1.WebhookConverter:
65		converter, err = m.webhookConverterFactory.NewWebhookConverter(crd)
66		if err != nil {
67			return nil, nil, err
68		}
69		converter, err = converterMetricFactorySingleton.addMetrics("webhook", crd.Name, converter)
70		if err != nil {
71			return nil, nil, err
72		}
73	default:
74		return nil, nil, fmt.Errorf("unknown conversion strategy %q for CRD %s", crd.Spec.Conversion.Strategy, crd.Name)
75	}
76
77	// Determine whether we should expect to be asked to "convert" autoscaling/v1 Scale types
78	convertScale := false
79	for _, version := range crd.Spec.Versions {
80		if version.Subresources != nil && version.Subresources.Scale != nil {
81			convertScale = true
82		}
83	}
84
85	unsafe = &crConverter{
86		convertScale:  convertScale,
87		validVersions: validVersions,
88		clusterScoped: crd.Spec.Scope == apiextensionsv1.ClusterScoped,
89		converter:     converter,
90	}
91	return &safeConverterWrapper{unsafe}, unsafe, nil
92}
93
94// crConverterInterface is the interface all cr converters must implement
95type crConverterInterface interface {
96	// Convert converts in object to the given gvk and returns the converted object.
97	// Note that the function may mutate in object and return it. A safe wrapper will make sure
98	// a safe converter will be returned.
99	Convert(in runtime.Object, targetGVK schema.GroupVersion) (runtime.Object, error)
100}
101
102// crConverter extends the delegate converter with generic CR conversion behaviour. The delegate will implement the
103// user defined conversion strategy given in the CustomResourceDefinition.
104type crConverter struct {
105	convertScale  bool
106	converter     crConverterInterface
107	validVersions map[schema.GroupVersion]bool
108	clusterScoped bool
109}
110
111func (c *crConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
112	// We currently only support metadata.namespace and metadata.name.
113	switch {
114	case label == "metadata.name":
115		return label, value, nil
116	case !c.clusterScoped && label == "metadata.namespace":
117		return label, value, nil
118	default:
119		return "", "", fmt.Errorf("field label not supported: %s", label)
120	}
121}
122
123func (c *crConverter) Convert(in, out, context interface{}) error {
124	// Special-case typed scale conversion if this custom resource supports a scale endpoint
125	if c.convertScale {
126		_, isInScale := in.(*autoscalingv1.Scale)
127		_, isOutScale := out.(*autoscalingv1.Scale)
128		if isInScale || isOutScale {
129			return typedscheme.Scheme.Convert(in, out, context)
130		}
131	}
132
133	unstructIn, ok := in.(*unstructured.Unstructured)
134	if !ok {
135		return fmt.Errorf("input type %T in not valid for unstructured conversion to %T", in, out)
136	}
137
138	unstructOut, ok := out.(*unstructured.Unstructured)
139	if !ok {
140		return fmt.Errorf("output type %T in not valid for unstructured conversion from %T", out, in)
141	}
142
143	outGVK := unstructOut.GroupVersionKind()
144	converted, err := c.ConvertToVersion(unstructIn, outGVK.GroupVersion())
145	if err != nil {
146		return err
147	}
148	unstructuredConverted, ok := converted.(runtime.Unstructured)
149	if !ok {
150		// this should not happened
151		return fmt.Errorf("CR conversion failed")
152	}
153	unstructOut.SetUnstructuredContent(unstructuredConverted.UnstructuredContent())
154	return nil
155}
156
157// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
158// The in object can be a single object or a UnstructuredList. CRD storage implementation creates an
159// UnstructuredList with the request's GV, populates it from storage, then calls conversion to convert
160// the individual items. This function assumes it never gets a v1.List.
161func (c *crConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
162	fromGVK := in.GetObjectKind().GroupVersionKind()
163	toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK})
164	if !ok {
165		// TODO: should this be a typed error?
166		return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target)
167	}
168	if !c.validVersions[toGVK.GroupVersion()] {
169		return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", toGVK.GroupVersion().String())
170	}
171	// Note that even if the request is for a list, the GV of the request UnstructuredList is what
172	// is expected to convert to. As mentioned in the function's document, it is not expected to
173	// get a v1.List.
174	if !c.validVersions[fromGVK.GroupVersion()] {
175		return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGVK.GroupVersion().String())
176	}
177	// Check list item's apiVersion
178	if list, ok := in.(*unstructured.UnstructuredList); ok {
179		for i := range list.Items {
180			expectedGV := list.Items[i].GroupVersionKind().GroupVersion()
181			if !c.validVersions[expectedGV] {
182				return nil, fmt.Errorf("request to convert CR list failed, list index %d has invalid group/version: %s", i, expectedGV.String())
183			}
184		}
185	}
186	return c.converter.Convert(in, toGVK.GroupVersion())
187}
188
189// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
190type safeConverterWrapper struct {
191	unsafe runtime.ObjectConvertor
192}
193
194var _ runtime.ObjectConvertor = &safeConverterWrapper{}
195
196// ConvertFieldLabel delegate the call to the unsafe converter.
197func (c *safeConverterWrapper) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
198	return c.unsafe.ConvertFieldLabel(gvk, label, value)
199}
200
201// Convert makes a copy of in object and then delegate the call to the unsafe converter.
202func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
203	inObject, ok := in.(runtime.Object)
204	if !ok {
205		return fmt.Errorf("input type %T in not valid for object conversion", in)
206	}
207	return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
208}
209
210// ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
211func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
212	return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
213}
214