Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Specification of the CRD API Version a Controller Watches & Reconciles #400

Merged
merged 6 commits into from
May 14, 2024
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
Loading