Skip to content

Commit

Permalink
Merge pull request #400 from ulucinar/tf-conversion
Browse files Browse the repository at this point in the history
Allow Specification of the CRD API Version a Controller Watches & Reconciles
  • Loading branch information
ulucinar authored May 14, 2024
2 parents 03a207b + 9463489 commit c33a66d
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 45 deletions.
44 changes: 27 additions & 17 deletions pkg/config/conversion/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const (
)

const (
pathForProvider = "spec.forProvider"
pathForProvider = "spec.forProvider"
pathInitProvider = "spec.initProvider"
pathAtProvider = "status.atProvider"
)

var (
Expand All @@ -32,7 +34,7 @@ var (
_ PavedConversion = &singletonListConverter{}
)

// Conversion is the interface for the API version converters.
// Conversion is the interface for the CRD 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
Expand Down Expand Up @@ -176,17 +178,19 @@ func NewCustomConverter(sourceVersion, targetVersion string, converter func(src,

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

// 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 {
func NewSingletonListConversion(sourceVersion, targetVersion string, pathPrefixes []string, crdPaths []string, mode ListConversionMode) Conversion {
return &singletonListConverter{
baseConversion: newBaseConversion(sourceVersion, targetVersion),
pathPrefixes: pathPrefixes,
crdPaths: crdPaths,
mode: mode,
}
Expand All @@ -200,18 +204,24 @@ func (s *singletonListConverter) ConvertPaved(src, target *fieldpath.Paved) (boo
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())

for _, p := range s.pathPrefixes {
v, err := src.GetValue(p)
if err != nil {
return true, errors.Wrapf(err, "failed to read the %s value for conversion in mode %q", p, s.mode)
}
m, ok := v.(map[string]any)
if !ok {
return true, errors.Errorf("value at path %s is not a map[string]any", p)
}
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())
}
if err := target.SetValue(p, m); err != nil {
return true, errors.Wrapf(err, "failed to set the %s value for conversion in mode %q", p, s.mode)
}
}
return true, errors.Wrapf(target.SetValue(pathForProvider, m), "failed to set the %s value for conversion in mode %q", pathForProvider, s.mode)
return true, nil
}

type identityConversion struct {
Expand Down Expand Up @@ -305,5 +315,5 @@ func ExpandParameters(prefixes []string, excludePaths ...string) []string {
// 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"}
return []string{pathForProvider, pathInitProvider, pathAtProvider}
}
14 changes: 7 additions & 7 deletions pkg/config/conversion/conversions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func TestSingletonListConversion(t *testing.T) {
targetVersion string
targetMap map[string]any
crdPaths []string
mode Mode
mode ListConversionMode
}
type want struct {
converted bool
Expand All @@ -383,7 +383,7 @@ func TestSingletonListConversion(t *testing.T) {
sourceVersion: AllVersions,
sourceMap: map[string]any{
"spec": map[string]any{
"forProvider": map[string]any{
"initProvider": map[string]any{
"l": []map[string]any{
{
"k": "v",
Expand All @@ -401,7 +401,7 @@ func TestSingletonListConversion(t *testing.T) {
converted: true,
targetMap: map[string]any{
"spec": map[string]any{
"forProvider": map[string]any{
"initProvider": map[string]any{
"l": map[string]any{
"k": "v",
},
Expand All @@ -416,7 +416,7 @@ func TestSingletonListConversion(t *testing.T) {
sourceVersion: AllVersions,
sourceMap: map[string]any{
"spec": map[string]any{
"forProvider": map[string]any{
"initProvider": map[string]any{
"o": map[string]any{
"k": "v",
},
Expand All @@ -432,7 +432,7 @@ func TestSingletonListConversion(t *testing.T) {
converted: true,
targetMap: map[string]any{
"spec": map[string]any{
"forProvider": map[string]any{
"initProvider": map[string]any{
"o": []map[string]any{
{
"k": "v",
Expand All @@ -449,7 +449,7 @@ func TestSingletonListConversion(t *testing.T) {
sourceVersion: AllVersions,
sourceMap: map[string]any{
"spec": map[string]any{
"forProvider": map[string]any{
"initProvider": map[string]any{
"o": map[string]any{
"k": "v",
},
Expand All @@ -468,7 +468,7 @@ func TestSingletonListConversion(t *testing.T) {
}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
c := NewSingletonListConversion(tc.args.sourceVersion, tc.args.targetVersion, tc.args.crdPaths, tc.args.mode)
c := NewSingletonListConversion(tc.args.sourceVersion, tc.args.targetVersion, []string{pathInitProvider}, tc.args.crdPaths, tc.args.mode)
sourceMap, err := roundTrip(tc.args.sourceMap)
if err != nil {
t.Fatalf("Failed to preprocess tc.args.sourceMap: %v", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import (
"github.com/pkg/errors"
)

// Mode denotes the mode of the runtime API conversion, e.g.,
// ListConversionMode denotes the mode of the list-object API conversion, e.g.,
// conversion of embedded objects into singleton lists.
type Mode int
type ListConversionMode int

const (
// ToEmbeddedObject represents a runtime conversion from a singleton list
// to an embedded object, i.e., the runtime conversions needed while
// reading from the Terraform state and updating the CRD
// (for status, late-initialization, etc.)
ToEmbeddedObject Mode = iota
ToEmbeddedObject ListConversionMode = iota
// ToSingletonList represents a runtime conversion from an embedded object
// to a singleton list, i.e., the runtime conversions needed while passing
// the configuration data to the underlying Terraform layer.
Expand All @@ -36,7 +36,7 @@ const (
)

// String returns a string representation of the conversion mode.
func (m Mode) String() string {
func (m ListConversionMode) String() string {
switch m {
case ToSingletonList:
return "toSingletonList"
Expand Down Expand Up @@ -79,7 +79,7 @@ func setValue(pv *fieldpath.Paved, v any, fp string) error {
// an embedded object will be converted into a singleton list or a singleton
// list will be converted into an embedded object) is determined by the mode
// parameter.
func Convert(params map[string]any, paths []string, mode Mode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit
func Convert(params map[string]any, paths []string, mode ListConversionMode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit
switch mode {
case ToSingletonList:
slices.Sort(paths)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestConvert(t *testing.T) {
type args struct {
params map[string]any
paths []string
mode Mode
mode ListConversionMode
}
type want struct {
err error
Expand Down Expand Up @@ -294,7 +294,7 @@ func TestConvert(t *testing.T) {

func TestModeString(t *testing.T) {
tests := map[string]struct {
m Mode
m ListConversionMode
want string
}{
"ToSingletonList": {
Expand Down
26 changes: 25 additions & 1 deletion pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ type References map[string]Reference
type Reference struct {
// Type is the Go type name of the CRD if it is in the same package or
// <package-path>.<type-name> if it is in a different package.
// Deprecated: Type is deprecated in favor of TerraformName, which provides
// a more stable and less error-prone API compared to Type. TerraformName
// will automatically handle name & version configurations that will affect
// the generated cross-resource reference. This is crucial especially if the
// provider generates multiple versions for its MR APIs.
Type string
// TerraformName is the name of the Terraform resource
// which will be referenced. The supplied resource name is
Expand Down Expand Up @@ -393,9 +398,19 @@ type Resource struct {
// be `ec2.aws.crossplane.io`
ShortGroup string

// Version is the version CRD will have.
// Version is the API version being generated for the corresponding CRD.
Version string

// ControllerReconcileVersion is the CRD API version the associated
// controller will watch & reconcile. If left unspecified,
// defaults to the value of Version. This configuration parameter
// can be used to have a controller use an older
// API version of the generated CRD instead of the API version being
// generated. Because this configuration parameter's value defaults to
// the value of Version, by default the controllers will reconcile the
// currently generated API versions of their associated CRs.
ControllerReconcileVersion string

// Kind is the kind of the CRD.
Kind string

Expand Down Expand Up @@ -477,8 +492,17 @@ type Resource struct {
// index notation (i.e., array/map components do not need indices).
ServerSideApplyMergeStrategies ServerSideApplyMergeStrategies

// Conversions is the list of CRD API conversion functions to be invoked
// in-chain by the installed conversion Webhook for the generated CRD.
// This list of conversion.Conversion registered here are responsible for
// doing the conversions between the hub & spoke CRD API versions.
Conversions []conversion.Conversion

// TerraformConversions is the list of conversions to be invoked when passing
// data from the Crossplane layer to the Terraform layer and when reading
// data (state) from the Terraform layer to be used in the Crossplane layer.
TerraformConversions []TerraformConversion

// useTerraformPluginSDKClient indicates that a plugin SDK external client should
// be generated instead of the Terraform CLI-forking client.
useTerraformPluginSDKClient bool
Expand Down
72 changes: 72 additions & 0 deletions pkg/config/tf_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package config

import (
"github.com/pkg/errors"

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

// Mode denotes the mode of the runtime Terraform conversion, e.g.,
// conversion from Crossplane parameters to Terraform arguments, or
// conversion from Terraform state to Crossplane state.
type Mode int

const (
ToTerraform Mode = iota
FromTerraform
)

// String returns a string representation of the conversion mode.
func (m Mode) String() string {
switch m {
case ToTerraform:
return "toTerraform"
case FromTerraform:
return "fromTerraform"
default:
return "unknown"
}
}

type TerraformConversion interface {
Convert(params map[string]any, r *Resource, mode Mode) (map[string]any, error)
}

// ApplyTFConversions applies the configured Terraform conversions on the
// specified params map in the given mode, i.e., from Crossplane layer to the
// Terraform layer or vice versa.
func (r *Resource) ApplyTFConversions(params map[string]any, mode Mode) (map[string]any, error) {
var err error
for _, c := range r.TerraformConversions {
params, err = c.Convert(params, r, mode)
if err != nil {
return nil, err
}
}
return params, nil
}

type singletonListConversion struct{}

// NewTFSingletonConversion initializes a new TerraformConversion to convert
// between singleton lists and embedded objects in the exchanged data
// at runtime between the Crossplane & Terraform layers.
func NewTFSingletonConversion() TerraformConversion {
return singletonListConversion{}
}

func (s singletonListConversion) Convert(params map[string]any, r *Resource, mode Mode) (map[string]any, error) {
var err error
var m map[string]any
switch mode {
case FromTerraform:
m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToEmbeddedObject)
case ToTerraform:
m, err = conversion.Convert(params, r.TFListConversionPaths(), conversion.ToSingletonList)
}
return m, errors.Wrapf(err, "failed to convert between Crossplane and Terraform layers in mode %q", mode)
}
Loading

0 comments on commit c33a66d

Please sign in to comment.