Skip to content

Commit

Permalink
Add webhook conversion registry
Browse files Browse the repository at this point in the history
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
  • Loading branch information
ulucinar committed Jan 9, 2024
1 parent b77ec1e commit a94d692
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 34 deletions.
54 changes: 28 additions & 26 deletions pkg/config/conversion/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
43 changes: 39 additions & 4 deletions pkg/controller/conversion/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
46 changes: 46 additions & 0 deletions pkg/controller/conversion/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
//
// 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 = &registry{
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
}
9 changes: 5 additions & 4 deletions pkg/pipeline/templates/conversion_spoke.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@ 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"
)

{{ 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
}
Expand Down

0 comments on commit a94d692

Please sign in to comment.