Skip to content

Commit

Permalink
Merge pull request #387 from ulucinar/embed-singleton
Browse files Browse the repository at this point in the history
Generate singleton lists as embedded objects
  • Loading branch information
ulucinar authored May 8, 2024
2 parents b5d344b + 44c139e commit 03a207b
Show file tree
Hide file tree
Showing 25 changed files with 1,910 additions and 279 deletions.
8 changes: 6 additions & 2 deletions pkg/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/crossplane/upjet/pkg/config/conversion"
"github.com/crossplane/upjet/pkg/registry"
tjname "github.com/crossplane/upjet/pkg/types/name"
)
Expand Down Expand Up @@ -91,6 +92,9 @@ func DefaultResource(name string, terraformSchema *schema.Resource, terraformPlu
UseAsync: true,
SchemaElementOptions: make(SchemaElementOptions),
ServerSideApplyMergeStrategies: make(ServerSideApplyMergeStrategies),
Conversions: []conversion.Conversion{conversion.NewIdentityConversionExpandPaths(conversion.AllVersions, conversion.AllVersions, nil)},
OverrideFieldNames: map[string]string{},
listConversionPaths: make(map[string]string),
}
for _, f := range opts {
f(r)
Expand Down Expand Up @@ -137,8 +141,8 @@ func (r *Resource) MarkAsRequired(fieldpaths ...string) {
// Deprecated: Use Resource.MarkAsRequired instead.
// This function will be removed in future versions.
func MarkAsRequired(sch *schema.Resource, fieldpaths ...string) {
for _, fieldpath := range fieldpaths {
if s := GetSchema(sch, fieldpath); s != nil {
for _, fp := range fieldpaths {
if s := GetSchema(sch, fp); s != nil {
s.Computed = false
s.Optional = false
}
Expand Down
19 changes: 16 additions & 3 deletions pkg/config/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
package config

import (
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/crossplane/upjet/pkg/config/conversion"
"github.com/crossplane/upjet/pkg/registry"
)

func TestDefaultResource(t *testing.T) {
identityConversion := conversion.NewIdentityConversionExpandPaths(conversion.AllVersions, conversion.AllVersions, nil)

type args struct {
name string
sch *schema.Resource
Expand Down Expand Up @@ -45,6 +49,8 @@ func TestDefaultResource(t *testing.T) {
UseAsync: true,
SchemaElementOptions: SchemaElementOptions{},
ServerSideApplyMergeStrategies: ServerSideApplyMergeStrategies{},
Conversions: []conversion.Conversion{identityConversion},
OverrideFieldNames: map[string]string{},
},
},
"TwoSectionsName": {
Expand All @@ -63,6 +69,8 @@ func TestDefaultResource(t *testing.T) {
UseAsync: true,
SchemaElementOptions: SchemaElementOptions{},
ServerSideApplyMergeStrategies: ServerSideApplyMergeStrategies{},
Conversions: []conversion.Conversion{identityConversion},
OverrideFieldNames: map[string]string{},
},
},
"NameWithPrefixAcronym": {
Expand All @@ -81,6 +89,8 @@ func TestDefaultResource(t *testing.T) {
UseAsync: true,
SchemaElementOptions: SchemaElementOptions{},
ServerSideApplyMergeStrategies: ServerSideApplyMergeStrategies{},
Conversions: []conversion.Conversion{identityConversion},
OverrideFieldNames: map[string]string{},
},
},
"NameWithSuffixAcronym": {
Expand All @@ -99,6 +109,8 @@ func TestDefaultResource(t *testing.T) {
UseAsync: true,
SchemaElementOptions: SchemaElementOptions{},
ServerSideApplyMergeStrategies: ServerSideApplyMergeStrategies{},
Conversions: []conversion.Conversion{identityConversion},
OverrideFieldNames: map[string]string{},
},
},
"NameWithMultipleAcronyms": {
Expand All @@ -117,6 +129,8 @@ func TestDefaultResource(t *testing.T) {
UseAsync: true,
SchemaElementOptions: SchemaElementOptions{},
ServerSideApplyMergeStrategies: ServerSideApplyMergeStrategies{},
Conversions: []conversion.Conversion{identityConversion},
OverrideFieldNames: map[string]string{},
},
},
}
Expand All @@ -126,9 +140,8 @@ func TestDefaultResource(t *testing.T) {
cmpopts.IgnoreFields(Sensitive{}, "fieldPaths", "AdditionalConnectionDetailsFn"),
cmpopts.IgnoreFields(LateInitializer{}, "ignoredCanonicalFieldPaths"),
cmpopts.IgnoreFields(ExternalName{}, "SetIdentifierArgumentFn", "GetExternalNameFn", "GetIDFn"),
cmpopts.IgnoreFields(Resource{}, "useTerraformPluginSDKClient"),
cmpopts.IgnoreFields(Resource{}, "useTerraformPluginFrameworkClient"),
cmpopts.IgnoreFields(Resource{}, "requiredFields"),
cmpopts.IgnoreUnexported(Resource{}),
cmpopts.IgnoreUnexported(reflect.ValueOf(identityConversion).Elem().Interface()),
}

for name, tc := range cases {
Expand Down
168 changes: 167 additions & 1 deletion pkg/config/conversion/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
package conversion

import (
"fmt"
"slices"

"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/pkg/errors"
Expand All @@ -19,12 +22,29 @@ const (
AllVersions = "*"
)

const (
pathForProvider = "spec.forProvider"
)

var (
_ PrioritizedManagedConversion = &identityConversion{}
_ PavedConversion = &fieldCopy{}
_ PavedConversion = &singletonListConverter{}
)

// 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.
// these two versions. All PavedConversions are run in their registration
// order before the ManagedConversions. Conversions are run in three stages:
// 1. PrioritizedManagedConversions are run.
// 2. The source and destination objects are paved and the PavedConversions are
// run in chain without unpaving the unstructured representation between
// conversions.
// 3. The destination paved object is converted back to a managed resource and
// ManagedConversions are run in the order they are registered.
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.
Expand Down Expand Up @@ -63,11 +83,23 @@ type ManagedConversion interface {
ConvertManaged(src, target resource.Managed) (bool, error)
}

// PrioritizedManagedConversion is a ManagedConversion that take precedence
// over all the other converters. PrioritizedManagedConversions are run,
// in their registration order, before the PavedConversions.
type PrioritizedManagedConversion interface {
ManagedConversion
Prioritized()
}

type baseConversion struct {
sourceVersion string
targetVersion string
}

func (c *baseConversion) String() string {
return fmt.Sprintf("source API version %q, target API version %q", c.sourceVersion, c.targetVersion)
}

func newBaseConversion(sourceVersion, targetVersion string) baseConversion {
return baseConversion{
sourceVersion: sourceVersion,
Expand Down Expand Up @@ -141,3 +173,137 @@ func NewCustomConverter(sourceVersion, targetVersion string, converter func(src,
customConverter: converter,
}
}

type singletonListConverter struct {
baseConversion
crdPaths []string
mode Mode
}

// NewSingletonListConversion returns a new Conversion from the specified
// sourceVersion of an API to the specified targetVersion and uses the
// CRD field paths given in crdPaths to convert between the singleton
// lists and embedded objects in the given conversion mode.
func NewSingletonListConversion(sourceVersion, targetVersion string, crdPaths []string, mode Mode) Conversion {
return &singletonListConverter{
baseConversion: newBaseConversion(sourceVersion, targetVersion),
crdPaths: crdPaths,
mode: mode,
}
}

func (s *singletonListConverter) ConvertPaved(src, target *fieldpath.Paved) (bool, error) {
if !s.Applicable(&unstructured.Unstructured{Object: src.UnstructuredContent()},
&unstructured.Unstructured{Object: target.UnstructuredContent()}) {
return false, nil
}
if len(s.crdPaths) == 0 {
return false, nil
}
v, err := src.GetValue(pathForProvider)
if err != nil {
return true, errors.Wrapf(err, "failed to read the %s value for conversion in mode %q", pathForProvider, s.mode)
}
m, ok := v.(map[string]any)
if !ok {
return true, errors.Errorf("value at path %s is not a map[string]any", pathForProvider)
}
if _, err := Convert(m, s.crdPaths, s.mode); err != nil {
return true, errors.Wrapf(err, "failed to convert the source map in mode %q with %s", s.mode, s.baseConversion.String())
}
return true, errors.Wrapf(target.SetValue(pathForProvider, m), "failed to set the %s value for conversion in mode %q", pathForProvider, s.mode)
}

type identityConversion struct {
baseConversion
excludePaths []string
}

func (i *identityConversion) ConvertManaged(src, target resource.Managed) (bool, error) {
if !i.Applicable(src, target) {
return false, nil
}

srcCopy := src.DeepCopyObject()
srcRaw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(srcCopy)
if err != nil {
return false, errors.Wrap(err, "cannot convert the source managed resource into an unstructured representation")
}

// remove excluded fields
if len(i.excludePaths) > 0 {
pv := fieldpath.Pave(srcRaw)
for _, ex := range i.excludePaths {
exPaths, err := pv.ExpandWildcards(ex)
if err != nil {
return false, errors.Wrapf(err, "cannot expand wildcards in the fieldpath expression %s", ex)
}
for _, p := range exPaths {
if err := pv.DeleteField(p); err != nil {
return false, errors.Wrapf(err, "cannot delete a field in the conversion source object")
}
}
}
}

// copy the remaining fields
gvk := target.GetObjectKind().GroupVersionKind()
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(srcRaw, target); err != nil {
return true, 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
target.GetObjectKind().SetGroupVersionKind(gvk)
return true, nil
}

func (i *identityConversion) Prioritized() {}

// newIdentityConversion returns a new Conversion from the specified
// sourceVersion of an API to the specified targetVersion, which copies the
// identical paths from the source to the target. excludePaths can be used
// to ignore certain field paths while copying.
func newIdentityConversion(sourceVersion, targetVersion string, excludePaths ...string) Conversion {
return &identityConversion{
baseConversion: newBaseConversion(sourceVersion, targetVersion),
excludePaths: excludePaths,
}
}

// NewIdentityConversionExpandPaths returns a new Conversion from the specified
// sourceVersion of an API to the specified targetVersion, which copies the
// identical paths from the source to the target. excludePaths can be used
// to ignore certain field paths while copying. Exclude paths must be specified
// in standard crossplane-runtime fieldpath library syntax, i.e., with proper
// indices for traversing map and slice types (e.g., a.b[*].c).
// The field paths in excludePaths are sorted in lexical order and are prefixed
// with each of the path prefixes specified with pathPrefixes. So if an
// exclude path "x" is specified with the prefix slice ["a", "b"], then
// paths a.x and b.x will both be skipped while copying fields from a source to
// a target.
func NewIdentityConversionExpandPaths(sourceVersion, targetVersion string, pathPrefixes []string, excludePaths ...string) Conversion {
return newIdentityConversion(sourceVersion, targetVersion, ExpandParameters(pathPrefixes, excludePaths...)...)
}

// ExpandParameters sorts and expands the given list of field path suffixes
// with the given prefixes.
func ExpandParameters(prefixes []string, excludePaths ...string) []string {
slices.Sort(excludePaths)
if len(prefixes) == 0 {
return excludePaths
}

r := make([]string, 0, len(prefixes)*len(excludePaths))
for _, p := range prefixes {
for _, ex := range excludePaths {
r = append(r, fmt.Sprintf("%s.%s", p, ex))
}
}
return r
}

// DefaultPathPrefixes returns the list of the default path prefixes for
// excluding paths in the identity conversion. The returned value is
// ["spec.forProvider", "spec.initProvider", "status.atProvider"].
func DefaultPathPrefixes() []string {
return []string{"spec.forProvider", "spec.initProvider", "status.atProvider"}
}
Loading

0 comments on commit 03a207b

Please sign in to comment.