diff --git a/pkg/config/conversion/conversions.go b/pkg/config/conversion/conversions.go index d57fc1a9..f1b03edc 100644 --- a/pkg/config/conversion/conversions.go +++ b/pkg/config/conversion/conversions.go @@ -8,29 +8,32 @@ 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 = "*" ) -const ( - pathObjectMeta = "ObjectMeta" -) - type Conversion interface { - GetSourceVersion() string - GetTargetVersion() string + Applicable(src, dst runtime.Object) bool } type PavedConversion interface { Conversion - ConvertPaved(src, target fieldpath.Paved) error + // 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) } -type ManagedConversion interface { +type TerraformedConversion interface { Conversion - ConvertTerraformed(src, target resource.Managed) + // ConvertTerraformed 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. + ConvertTerraformed(src, target resource.Managed) (bool, error) } type baseConversion struct { @@ -45,12 +48,9 @@ func newBaseConversion(sourceVersion, targetVersion string) baseConversion { } } -func (c *baseConversion) GetSourceVersion() string { - return c.sourceVersion -} - -func (c *baseConversion) GetTargetVersion() string { - return c.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 { @@ -59,20 +59,22 @@ type fieldCopy struct { targetField string } -func (f *fieldCopy) ConvertPaved(src, target fieldpath.Paved) error { +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) - if err != nil { - return errors.Wrapf(err, "failed to get the field %q from the conversion source object", 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 } - return errors.Wrapf(target.SetValue(f.targetField, v), "failed to set the field %q of the conversion target object", f.targetField) -} - -func NewObjectMetaConversion() Conversion { - return &fieldCopy{ - baseConversion: newBaseConversion(AllVersions, AllVersions), - sourceField: pathObjectMeta, - targetField: pathObjectMeta, + 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) } func NewFieldRenameConversion(sourceVersion, sourceField, targetVersion, targetField string) Conversion { diff --git a/pkg/controller/conversion/functions.go b/pkg/controller/conversion/functions.go index 44446522..0bdea0eb 100644 --- a/pkg/controller/conversion/functions.go +++ b/pkg/controller/conversion/functions.go @@ -5,22 +5,57 @@ 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. -func RoundTrip(dst, src runtime.Object) error { +// representation of the `src` object and applies the registered webhook +// conversion functions. +func RoundTrip(dst, src resource.Terraformed) error { srcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(src) if err != nil { - return errors.Wrap(err, "cannot convert the conversion source object into the map[string]interface{} representation") + 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]interface{} representation to the conversion target object") + 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 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 GetConversions(dst) { + if tc, ok := c.(conversion.TerraformedConversion); ok { + if _, err := tc.ConvertTerraformed(src, dst); err != nil { + return errors.Wrapf(err, "cannot apply the TerraformedConversion for the %q object", dst.GetTerraformResourceType()) + } + } + } + return nil } diff --git a/pkg/controller/conversion/registry.go b/pkg/controller/conversion/registry.go new file mode 100644 index 00000000..6412c082 --- /dev/null +++ b/pkg/controller/conversion/registry.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package conversion + +import ( + "github.com/pkg/errors" + + "github.com/crossplane/upjet/pkg/config" + "github.com/crossplane/upjet/pkg/config/conversion" + "github.com/crossplane/upjet/pkg/resource" +) + +const ( + errAlreadyRegistered = "conversion functions are already registered" +) + +var instance *registry + +// registry represents the conversion hook registry for a provider. +type registry struct { + provider *config.Provider +} + +// RegisterConversions registers the API version conversions from the specified +// provider configuration. +func RegisterConversions(provider *config.Provider) error { + if instance != nil { + return errors.New(errAlreadyRegistered) + } + instance = ®istry{ + provider: provider, + } + return nil +} + +// GetConversions returns the conversion.Conversions registered for the +// Terraformed resource. +func GetConversions(tr resource.Terraformed) []conversion.Conversion { + t := tr.GetTerraformResourceType() + if instance == nil || instance.provider == nil || instance.provider.Resources[t] == nil { + return nil + } + return instance.provider.Resources[t].Conversions +} diff --git a/pkg/pipeline/templates/conversion_spoke.go.tmpl b/pkg/pipeline/templates/conversion_spoke.go.tmpl index 2dfbabb1..c76577a3 100644 --- a/pkg/pipeline/templates/conversion_spoke.go.tmpl +++ b/pkg/pipeline/templates/conversion_spoke.go.tmpl @@ -10,6 +10,7 @@ package {{ .APIVersion }} import ( ujconversion "github.com/crossplane/upjet/pkg/controller/conversion" + "github.com/crossplane/upjet/pkg/resource" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/conversion" ) @@ -17,16 +18,16 @@ import ( {{ range .Resources }} // ConvertTo converts this {{ .CRD.Kind }} to the hub type. func (tr *{{ .CRD.Kind }}) ConvertTo(dstRaw conversion.Hub) error { - if err := ujconversion.RoundTrip(dstRaw, tr); err != nil { - return errors.Wrap(err, "cannot convert a spoke version to the hub version") + if err := ujconversion.RoundTrip(dstRaw.(resource.Terraformed), tr); err != nil { + return errors.Wrapf(err, "cannot convert from the spoke version %q to the hub version %q", tr.GetObjectKind().GroupVersionKind().Version, dstRaw.GetObjectKind().GroupVersionKind().Version) } return nil } // ConvertFrom converts from the hub type to the {{ .CRD.Kind }} type. func (tr *{{ .CRD.Kind }}) ConvertFrom(srcRaw conversion.Hub) error { - if err := ujconversion.RoundTrip(tr, srcRaw); err != nil { - return errors.Wrap(err, "cannot convert the hub version to a spoke version") + if err := ujconversion.RoundTrip(tr, srcRaw.(resource.Terraformed)); err != nil { + return errors.Wrapf(err, "cannot convert from the hub version %q to the spoke version %q", srcRaw.GetObjectKind().GroupVersionKind().Version, tr.GetObjectKind().GroupVersionKind().Version) } return nil }