| /* |
| Copyright 2018 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package conversion |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| |
| "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/runtime/schema" |
| "k8s.io/apimachinery/pkg/util/uuid" |
| "k8s.io/apiserver/pkg/util/webhook" |
| "k8s.io/client-go/rest" |
| |
| internal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" |
| "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" |
| ) |
| |
| type webhookConverterFactory struct { |
| clientManager webhook.ClientManager |
| } |
| |
| func newWebhookConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*webhookConverterFactory, error) { |
| clientManager, err := webhook.NewClientManager(v1beta1.SchemeGroupVersion, v1beta1.AddToScheme) |
| if err != nil { |
| return nil, err |
| } |
| authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver("") |
| if err != nil { |
| return nil, err |
| } |
| // Set defaults which may be overridden later. |
| clientManager.SetAuthenticationInfoResolver(authInfoResolver) |
| clientManager.SetAuthenticationInfoResolverWrapper(authResolverWrapper) |
| clientManager.SetServiceResolver(serviceResolver) |
| return &webhookConverterFactory{clientManager}, nil |
| } |
| |
| // webhookConverter is a converter that calls an external webhook to do the CR conversion. |
| type webhookConverter struct { |
| validVersions map[schema.GroupVersion]bool |
| clientManager webhook.ClientManager |
| restClient *rest.RESTClient |
| name string |
| nopConverter nopConverter |
| } |
| |
| func webhookClientConfigForCRD(crd *internal.CustomResourceDefinition) *webhook.ClientConfig { |
| apiConfig := crd.Spec.Conversion.WebhookClientConfig |
| ret := webhook.ClientConfig{ |
| Name: fmt.Sprintf("conversion_webhook_for_%s", crd.Name), |
| CABundle: apiConfig.CABundle, |
| } |
| if apiConfig.URL != nil { |
| ret.URL = *apiConfig.URL |
| } |
| if apiConfig.Service != nil { |
| ret.Service = &webhook.ClientConfigService{ |
| Name: apiConfig.Service.Name, |
| Namespace: apiConfig.Service.Namespace, |
| } |
| if apiConfig.Service.Path != nil { |
| ret.Service.Path = *apiConfig.Service.Path |
| } |
| } |
| return &ret |
| } |
| |
| var _ runtime.ObjectConvertor = &webhookConverter{} |
| |
| func (f *webhookConverterFactory) NewWebhookConverter(validVersions map[schema.GroupVersion]bool, crd *internal.CustomResourceDefinition) (*webhookConverter, error) { |
| restClient, err := f.clientManager.HookClient(*webhookClientConfigForCRD(crd)) |
| if err != nil { |
| return nil, err |
| } |
| return &webhookConverter{ |
| clientManager: f.clientManager, |
| validVersions: validVersions, |
| restClient: restClient, |
| name: crd.Name, |
| nopConverter: nopConverter{validVersions: validVersions}, |
| }, nil |
| } |
| |
| func (webhookConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) { |
| return "", "", errors.New("unstructured cannot convert field labels") |
| } |
| |
| func (c *webhookConverter) Convert(in, out, context interface{}) error { |
| unstructIn, ok := in.(*unstructured.Unstructured) |
| if !ok { |
| return fmt.Errorf("input type %T in not valid for unstructured conversion", in) |
| } |
| |
| unstructOut, ok := out.(*unstructured.Unstructured) |
| if !ok { |
| return fmt.Errorf("output type %T in not valid for unstructured conversion", out) |
| } |
| |
| outGVK := unstructOut.GroupVersionKind() |
| if !c.validVersions[outGVK.GroupVersion()] { |
| return fmt.Errorf("request to convert CR from an invalid group/version: %s", outGVK.String()) |
| } |
| inGVK := unstructIn.GroupVersionKind() |
| if !c.validVersions[inGVK.GroupVersion()] { |
| return fmt.Errorf("request to convert CR to an invalid group/version: %s", inGVK.String()) |
| } |
| |
| converted, err := c.ConvertToVersion(unstructIn, outGVK.GroupVersion()) |
| if err != nil { |
| return err |
| } |
| unstructuredConverted, ok := converted.(runtime.Unstructured) |
| if !ok { |
| // this should not happened |
| return fmt.Errorf("CR conversion failed") |
| } |
| unstructOut.SetUnstructuredContent(unstructuredConverted.UnstructuredContent()) |
| return nil |
| } |
| |
| func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.ConversionReview { |
| listObj, isList := obj.(*unstructured.UnstructuredList) |
| var objects []runtime.RawExtension |
| if isList { |
| for i := 0; i < len(listObj.Items); i++ { |
| // Only sent item for conversion, if the apiVersion is different |
| if listObj.Items[i].GetAPIVersion() != apiVersion { |
| objects = append(objects, runtime.RawExtension{Object: &listObj.Items[i]}) |
| } |
| } |
| } else { |
| if obj.GetObjectKind().GroupVersionKind().GroupVersion().String() != apiVersion { |
| objects = []runtime.RawExtension{{Object: obj}} |
| } |
| } |
| return &v1beta1.ConversionReview{ |
| Request: &v1beta1.ConversionRequest{ |
| Objects: objects, |
| DesiredAPIVersion: apiVersion, |
| UID: uuid.NewUUID(), |
| }, |
| Response: &v1beta1.ConversionResponse{}, |
| } |
| } |
| |
| func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) { |
| if rx.Object != nil { |
| return rx.Object, nil |
| } |
| u := unstructured.Unstructured{} |
| err := u.UnmarshalJSON(rx.Raw) |
| if err != nil { |
| return nil, err |
| } |
| return &u, nil |
| } |
| |
| // getTargetGroupVersion returns group/version which should be used to convert in objects to. |
| // String version of the return item is APIVersion. |
| func getTargetGroupVersion(in runtime.Object, target runtime.GroupVersioner) (schema.GroupVersion, error) { |
| fromGVK := in.GetObjectKind().GroupVersionKind() |
| toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK}) |
| if !ok { |
| // TODO: should this be a typed error? |
| return schema.GroupVersion{}, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target) |
| } |
| return toGVK.GroupVersion(), nil |
| } |
| |
| func (c *webhookConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) { |
| // In general, the webhook should not do any defaulting or validation. A special case of that is an empty object |
| // conversion that must result an empty object and practically is the same as nopConverter. |
| // A smoke test in API machinery calls the converter on empty objects. As this case happens consistently |
| // it special cased here not to call webhook converter. The test initiated here: |
| // https://github.com/kubernetes/kubernetes/blob/dbb448bbdcb9e440eee57024ffa5f1698956a054/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L201 |
| if isEmptyUnstructuredObject(in) { |
| return c.nopConverter.ConvertToVersion(in, target) |
| } |
| |
| toGV, err := getTargetGroupVersion(in, target) |
| if err != nil { |
| return nil, err |
| } |
| if !c.validVersions[toGV] { |
| return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", toGV.String()) |
| } |
| fromGV := in.GetObjectKind().GroupVersionKind().GroupVersion() |
| if !c.validVersions[fromGV] { |
| return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGV.String()) |
| } |
| listObj, isList := in.(*unstructured.UnstructuredList) |
| if isList { |
| for i, item := range listObj.Items { |
| fromGV := item.GroupVersionKind().GroupVersion() |
| if !c.validVersions[fromGV] { |
| return nil, fmt.Errorf("input list has invalid group/version `%v` at `%v` index", fromGV, i) |
| } |
| } |
| } |
| |
| request := createConversionReview(in, toGV.String()) |
| if len(request.Request.Objects) == 0 { |
| if !isList { |
| return in, nil |
| } |
| out := listObj.DeepCopy() |
| out.SetAPIVersion(toGV.String()) |
| return out, nil |
| } |
| response := &v1beta1.ConversionReview{} |
| // TODO: Figure out if adding one second timeout make sense here. |
| ctx := context.TODO() |
| r := c.restClient.Post().Context(ctx).Body(request).Do() |
| if err := r.Into(response); err != nil { |
| // TODO: Return a webhook specific error to be able to convert it to meta.Status |
| return nil, fmt.Errorf("calling to conversion webhook failed for %s: %v", c.name, err) |
| } |
| |
| if response.Response == nil { |
| // TODO: Return a webhook specific error to be able to convert it to meta.Status |
| return nil, fmt.Errorf("conversion webhook response was absent for %s", c.name) |
| } |
| |
| if response.Response.Result.Status != v1.StatusSuccess { |
| // TODO return status message as error |
| return nil, fmt.Errorf("conversion request failed for %v, Response: %v", in.GetObjectKind(), response) |
| } |
| |
| if len(response.Response.ConvertedObjects) != len(request.Request.Objects) { |
| return nil, fmt.Errorf("expected %v converted objects, got %v", len(request.Request.Objects), len(response.Response.ConvertedObjects)) |
| } |
| |
| if isList { |
| convertedList := listObj.DeepCopy() |
| // Collection of items sent for conversion is different than list items |
| // because only items that needed conversion has been sent. |
| convertedIndex := 0 |
| for i := 0; i < len(listObj.Items); i++ { |
| if listObj.Items[i].GetAPIVersion() == toGV.String() { |
| // This item has not been sent for conversion, skip it. |
| continue |
| } |
| converted, err := getRawExtensionObject(response.Response.ConvertedObjects[convertedIndex]) |
| convertedIndex++ |
| original := listObj.Items[i] |
| if err != nil { |
| return nil, fmt.Errorf("invalid converted object at index %v: %v", convertedIndex, err) |
| } |
| if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a { |
| return nil, fmt.Errorf("invalid converted object at index %v: invalid groupVersion, e=%v, a=%v", convertedIndex, e, a) |
| } |
| if e, a := original.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a { |
| return nil, fmt.Errorf("invalid converted object at index %v: invalid kind, e=%v, a=%v", convertedIndex, e, a) |
| } |
| unstructConverted, ok := converted.(*unstructured.Unstructured) |
| if !ok { |
| // this should not happened |
| return nil, fmt.Errorf("CR conversion failed") |
| } |
| if err := validateConvertedObject(&listObj.Items[i], unstructConverted); err != nil { |
| return nil, fmt.Errorf("invalid converted object at index %v: %v", convertedIndex, err) |
| } |
| convertedList.Items[i] = *unstructConverted |
| } |
| convertedList.SetAPIVersion(toGV.String()) |
| return convertedList, nil |
| } |
| |
| if len(response.Response.ConvertedObjects) != 1 { |
| // This should not happened |
| return nil, fmt.Errorf("CR conversion failed") |
| } |
| converted, err := getRawExtensionObject(response.Response.ConvertedObjects[0]) |
| if err != nil { |
| return nil, err |
| } |
| if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a { |
| return nil, fmt.Errorf("invalid converted object: invalid groupVersion, e=%v, a=%v", e, a) |
| } |
| if e, a := in.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a { |
| return nil, fmt.Errorf("invalid converted object: invalid kind, e=%v, a=%v", e, a) |
| } |
| unstructConverted, ok := converted.(*unstructured.Unstructured) |
| if !ok { |
| // this should not happened |
| return nil, fmt.Errorf("CR conversion failed") |
| } |
| unstructIn, ok := in.(*unstructured.Unstructured) |
| if !ok { |
| // this should not happened |
| return nil, fmt.Errorf("CR conversion failed") |
| } |
| if err := validateConvertedObject(unstructIn, unstructConverted); err != nil { |
| return nil, fmt.Errorf("invalid converted object: %v", err) |
| } |
| return converted, nil |
| } |
| |
| func validateConvertedObject(unstructIn, unstructOut *unstructured.Unstructured) error { |
| if e, a := unstructIn.GetKind(), unstructOut.GetKind(); e != a { |
| return fmt.Errorf("must have the same kind: %v != %v", e, a) |
| } |
| if e, a := unstructIn.GetName(), unstructOut.GetName(); e != a { |
| return fmt.Errorf("must have the same name: %v != %v", e, a) |
| } |
| if e, a := unstructIn.GetNamespace(), unstructOut.GetNamespace(); e != a { |
| return fmt.Errorf("must have the same namespace: %v != %v", e, a) |
| } |
| if e, a := unstructIn.GetUID(), unstructOut.GetUID(); e != a { |
| return fmt.Errorf("must have the same UID: %v != %v", e, a) |
| } |
| return nil |
| } |
| |
| // isEmptyUnstructuredObject returns true if in is an empty unstructured object, i.e. an unstructured object that does |
| // not have any field except apiVersion and kind. |
| func isEmptyUnstructuredObject(in runtime.Object) bool { |
| u, ok := in.(*unstructured.Unstructured) |
| if !ok { |
| return false |
| } |
| if len(u.Object) != 2 { |
| return false |
| } |
| if _, ok := u.Object["kind"]; !ok { |
| return false |
| } |
| if _, ok := u.Object["apiVersion"]; !ok { |
| return false |
| } |
| return true |
| } |