-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #321 from ulucinar/multiversion-crds
Multiversion CRDs & Conversion Webhooks
- Loading branch information
Showing
17 changed files
with
834 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package conversion | ||
|
||
import ( | ||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath" | ||
"github.com/crossplane/crossplane-runtime/pkg/resource" | ||
"github.com/pkg/errors" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
const ( | ||
// AllVersions denotes that a Conversion is applicable for all versions | ||
// of an API with which the Conversion is registered. It can be used for | ||
// both the conversion source or target API versions. | ||
AllVersions = "*" | ||
) | ||
|
||
// Conversion is the interface for the API version converters. | ||
// Conversion implementations registered for a source, target | ||
// pair are called in chain so Conversion implementations can be modular, e.g., | ||
// a Conversion implementation registered for a specific source and target | ||
// versions does not have to contain all the needed API conversions between | ||
// these two versions. | ||
type Conversion interface { | ||
// Applicable should return true if this Conversion is applicable while | ||
// converting the API of the `src` object to the API of the `dst` object. | ||
Applicable(src, dst runtime.Object) bool | ||
} | ||
|
||
// PavedConversion is an optimized Conversion between two fieldpath.Paved | ||
// objects. PavedConversion implementations for a specific source and target | ||
// version pair are chained together and the source and the destination objects | ||
// are paved once at the beginning of the chained PavedConversion.ConvertPaved | ||
// calls. The target fieldpath.Paved object is then converted into the original | ||
// resource.Terraformed object at the end of the chained calls. This prevents | ||
// the intermediate conversions between fieldpath.Paved and | ||
// the resource.Terraformed representations of the same object, and the | ||
// fieldpath.Paved representation is convenient for writing generic | ||
// Conversion implementations not bound to a specific type. | ||
type PavedConversion interface { | ||
Conversion | ||
// ConvertPaved converts from the `src` paved object to the `dst` | ||
// paved object and returns `true` if the conversion has been done, | ||
// `false` otherwise, together with any errors encountered. | ||
ConvertPaved(src, target *fieldpath.Paved) (bool, error) | ||
} | ||
|
||
// ManagedConversion defines a Conversion from a specific source | ||
// resource.Managed type to a target one. Generic Conversion | ||
// implementations may prefer to implement the PavedConversion interface. | ||
// Implementations of ManagedConversion can do type assertions to | ||
// specific source and target types, and so, they are expected to be | ||
// strongly typed. | ||
type ManagedConversion interface { | ||
Conversion | ||
// ConvertManaged converts from the `src` managed resource to the `dst` | ||
// managed resource and returns `true` if the conversion has been done, | ||
// `false` otherwise, together with any errors encountered. | ||
ConvertManaged(src, target resource.Managed) (bool, error) | ||
} | ||
|
||
type baseConversion struct { | ||
sourceVersion string | ||
targetVersion string | ||
} | ||
|
||
func newBaseConversion(sourceVersion, targetVersion string) baseConversion { | ||
return baseConversion{ | ||
sourceVersion: sourceVersion, | ||
targetVersion: targetVersion, | ||
} | ||
} | ||
|
||
func (c *baseConversion) Applicable(src, dst runtime.Object) bool { | ||
return (c.sourceVersion == AllVersions || c.sourceVersion == src.GetObjectKind().GroupVersionKind().Version) && | ||
(c.targetVersion == AllVersions || c.targetVersion == dst.GetObjectKind().GroupVersionKind().Version) | ||
} | ||
|
||
type fieldCopy struct { | ||
baseConversion | ||
sourceField string | ||
targetField string | ||
} | ||
|
||
func (f *fieldCopy) ConvertPaved(src, target *fieldpath.Paved) (bool, error) { | ||
if !f.Applicable(&unstructured.Unstructured{Object: src.UnstructuredContent()}, | ||
&unstructured.Unstructured{Object: target.UnstructuredContent()}) { | ||
return false, nil | ||
} | ||
v, err := src.GetValue(f.sourceField) | ||
// TODO: the field might actually exist in the schema and | ||
// missing in the object. Or, it may not exist in the schema. | ||
// For a field that does not exist in the schema, we had better error. | ||
if fieldpath.IsNotFound(err) { | ||
return false, nil | ||
} | ||
if err != nil { | ||
return false, errors.Wrapf(err, "failed to get the field %q from the conversion source object", f.sourceField) | ||
} | ||
return true, errors.Wrapf(target.SetValue(f.targetField, v), "failed to set the field %q of the conversion target object", f.targetField) | ||
} | ||
|
||
// NewFieldRenameConversion returns a new Conversion that implements a | ||
// field renaming conversion from the specified `sourceVersion` to the specified | ||
// `targetVersion` of an API. The field's name in the `sourceVersion` is given | ||
// with the `sourceField` parameter and its name in the `targetVersion` is | ||
// given with `targetField` parameter. | ||
func NewFieldRenameConversion(sourceVersion, sourceField, targetVersion, targetField string) Conversion { | ||
return &fieldCopy{ | ||
baseConversion: newBaseConversion(sourceVersion, targetVersion), | ||
sourceField: sourceField, | ||
targetField: targetField, | ||
} | ||
} | ||
|
||
type customConverter func(src, target resource.Managed) error | ||
|
||
type customConversion struct { | ||
baseConversion | ||
customConverter customConverter | ||
} | ||
|
||
func (cc *customConversion) ConvertManaged(src, target resource.Managed) (bool, error) { | ||
if !cc.Applicable(src, target) || cc.customConverter == nil { | ||
return false, nil | ||
} | ||
return true, errors.Wrap(cc.customConverter(src, target), "failed to apply the converter function") | ||
} | ||
|
||
// NewCustomConverter returns a new Conversion from the specified | ||
// `sourceVersion` of an API to the specified `targetVersion` and invokes | ||
// the specified converter function to perform the conversion on the | ||
// managed resources. | ||
func NewCustomConverter(sourceVersion, targetVersion string, converter func(src, target resource.Managed) error) Conversion { | ||
return &customConversion{ | ||
baseConversion: newBaseConversion(sourceVersion, targetVersion), | ||
customConverter: converter, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package conversion | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath" | ||
"github.com/crossplane/crossplane-runtime/pkg/test" | ||
"github.com/google/go-cmp/cmp" | ||
"k8s.io/utils/ptr" | ||
) | ||
|
||
const ( | ||
sourceVersion = "v1beta1" | ||
sourceField = "testSourceField" | ||
targetVersion = "v1beta2" | ||
targetField = "testTargetField" | ||
) | ||
|
||
func TestConvertPaved(t *testing.T) { | ||
type args struct { | ||
sourceVersion string | ||
sourceField string | ||
targetVersion string | ||
targetField string | ||
sourceObj *fieldpath.Paved | ||
targetObj *fieldpath.Paved | ||
} | ||
type want struct { | ||
converted bool | ||
err error | ||
targetObj *fieldpath.Paved | ||
} | ||
tests := map[string]struct { | ||
reason string | ||
args args | ||
want want | ||
}{ | ||
"SuccessfulConversion": { | ||
reason: "Source field in source version is successfully converted to the target field in target version.", | ||
args: args{ | ||
sourceVersion: sourceVersion, | ||
sourceField: sourceField, | ||
targetVersion: targetVersion, | ||
targetField: targetField, | ||
sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
want: want{ | ||
converted: true, | ||
targetObj: getPaved(targetVersion, targetField, ptr.To("testValue")), | ||
}, | ||
}, | ||
"SuccessfulConversionAllVersions": { | ||
reason: "Source field in source version is successfully converted to the target field in target version when the conversion specifies wildcard version for both of the source and the target.", | ||
args: args{ | ||
sourceVersion: AllVersions, | ||
sourceField: sourceField, | ||
targetVersion: AllVersions, | ||
targetField: targetField, | ||
sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
want: want{ | ||
converted: true, | ||
targetObj: getPaved(targetVersion, targetField, ptr.To("testValue")), | ||
}, | ||
}, | ||
"SourceVersionMismatch": { | ||
reason: "Conversion is not done if the source version of the object does not match the conversion's source version.", | ||
args: args{ | ||
sourceVersion: "mismatch", | ||
sourceField: sourceField, | ||
targetVersion: AllVersions, | ||
targetField: targetField, | ||
sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
want: want{ | ||
converted: false, | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
}, | ||
"TargetVersionMismatch": { | ||
reason: "Conversion is not done if the target version of the object does not match the conversion's target version.", | ||
args: args{ | ||
sourceVersion: AllVersions, | ||
sourceField: sourceField, | ||
targetVersion: "mismatch", | ||
targetField: targetField, | ||
sourceObj: getPaved(sourceVersion, sourceField, ptr.To("testValue")), | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
want: want{ | ||
converted: false, | ||
targetObj: getPaved(targetVersion, targetField, nil), | ||
}, | ||
}, | ||
"SourceFieldNotFound": { | ||
reason: "Conversion is not done if the source field is not found in the source object.", | ||
args: args{ | ||
sourceVersion: sourceVersion, | ||
sourceField: sourceField, | ||
targetVersion: targetVersion, | ||
targetField: targetField, | ||
sourceObj: getPaved(sourceVersion, sourceField, nil), | ||
targetObj: getPaved(targetVersion, targetField, ptr.To("test")), | ||
}, | ||
want: want{ | ||
converted: false, | ||
targetObj: getPaved(targetVersion, targetField, ptr.To("test")), | ||
}, | ||
}, | ||
} | ||
for name, tc := range tests { | ||
t.Run(name, func(t *testing.T) { | ||
c := NewFieldRenameConversion(tc.args.sourceVersion, tc.args.sourceField, tc.args.targetVersion, tc.args.targetField) | ||
converted, err := c.(*fieldCopy).ConvertPaved(tc.args.sourceObj, tc.args.targetObj) | ||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { | ||
t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantErr, +gotErr:\n%s", tc.reason, diff) | ||
} | ||
if tc.want.err != nil { | ||
return | ||
} | ||
if diff := cmp.Diff(tc.want.converted, converted); diff != "" { | ||
t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantConverted, +gotConverted:\n%s", tc.reason, diff) | ||
} | ||
if diff := cmp.Diff(tc.want.targetObj.UnstructuredContent(), tc.args.targetObj.UnstructuredContent()); diff != "" { | ||
t.Errorf("\n%s\nConvertPaved(sourceObj, targetObj): -wantTargetObj, +gotTargetObj:\n%s", tc.reason, diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func getPaved(version, field string, value *string) *fieldpath.Paved { | ||
m := map[string]any{ | ||
"apiVersion": fmt.Sprintf("mockgroup/%s", version), | ||
"kind": "mockkind", | ||
} | ||
if value != nil { | ||
m[field] = *value | ||
} | ||
return fieldpath.Pave(m) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package conversion | ||
|
||
import ( | ||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath" | ||
"github.com/pkg/errors" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
|
||
"github.com/crossplane/upjet/pkg/config/conversion" | ||
"github.com/crossplane/upjet/pkg/resource" | ||
) | ||
|
||
// RoundTrip round-trips from `src` to `dst` via an unstructured map[string]any | ||
// representation of the `src` object and applies the registered webhook | ||
// conversion functions of this registry. | ||
func (r *registry) RoundTrip(dst, src resource.Terraformed) error { //nolint:gocyclo // considered breaking this according to the converters and I did not like it | ||
srcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(src) | ||
if err != nil { | ||
return errors.Wrap(err, "cannot convert the conversion source object into the map[string]any representation") | ||
} | ||
gvk := dst.GetObjectKind().GroupVersionKind() | ||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(srcMap, dst); err != nil { | ||
return errors.Wrap(err, "cannot convert the map[string]any representation of the source object to the conversion target object") | ||
} | ||
// restore the original GVK for the conversion destination | ||
dst.GetObjectKind().SetGroupVersionKind(gvk) | ||
|
||
// now we will try to run the registered webhook conversions | ||
dstMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(dst) | ||
if err != nil { | ||
return errors.Wrap(err, "cannot convert the conversion destination object into the map[string]any representation") | ||
} | ||
srcPaved := fieldpath.Pave(srcMap) | ||
dstPaved := fieldpath.Pave(dstMap) | ||
for _, c := range r.GetConversions(dst) { | ||
if pc, ok := c.(conversion.PavedConversion); ok { | ||
if _, err := pc.ConvertPaved(srcPaved, dstPaved); err != nil { | ||
return errors.Wrapf(err, "cannot apply the PavedConversion for the %q object", dst.GetTerraformResourceType()) | ||
} | ||
} | ||
} | ||
// convert the map[string]any representation of the conversion target back to | ||
// the original type. | ||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(dstMap, dst); err != nil { | ||
return errors.Wrap(err, "cannot convert the map[string]any representation of the conversion target object to the target object") | ||
} | ||
|
||
for _, c := range r.GetConversions(dst) { | ||
if tc, ok := c.(conversion.ManagedConversion); ok { | ||
if _, err := tc.ConvertManaged(src, dst); err != nil { | ||
return errors.Wrapf(err, "cannot apply the TerraformedConversion for the %q object", dst.GetTerraformResourceType()) | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// RoundTrip round-trips from `src` to `dst` via an unstructured map[string]any | ||
// representation of the `src` object and applies the registered webhook | ||
// conversion functions. | ||
func RoundTrip(dst, src resource.Terraformed) error { | ||
return instance.RoundTrip(dst, src) | ||
} |
Oops, something went wrong.