Skip to content

Commit

Permalink
Add conversion.singletonConverter conversion function for CRD API con…
Browse files Browse the repository at this point in the history
…versions between

singleton list & embedded object API versions.

- Export conversion.Convert to be reused in embedded singleton list webhook API conversions.

Signed-off-by: Alper Rifat Ulucinar <[email protected]>
  • Loading branch information
ulucinar committed Apr 18, 2024
1 parent 7f036c3 commit 017f4f2
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 31 deletions.
1 change: 1 addition & 0 deletions pkg/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func DefaultResource(name string, terraformSchema *schema.Resource, terraformPlu
SchemaElementOptions: make(SchemaElementOptions),
ServerSideApplyMergeStrategies: make(ServerSideApplyMergeStrategies),
MarkStorageVersion: true,
listConversionPaths: make(map[string]string),
}
for _, f := range opts {
f(r)
Expand Down
46 changes: 46 additions & 0 deletions pkg/config/conversion/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package conversion

import (
"fmt"

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

const (
pathForProvider = "spec.forProvider"
)

// 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.,
Expand Down Expand Up @@ -68,6 +74,10 @@ type baseConversion struct {
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 +151,39 @@ 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 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)
}
return true, errors.Wrapf(target.SetValue(pathForProvider, m), "failed to set the %s value for conversion in mode %q", pathForProvider, s.mode)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package controller
package conversion

import (
"slices"
Expand All @@ -13,19 +13,28 @@ import (
"github.com/pkg/errors"
)

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

const (
toEmbeddedObject conversionMode = iota
toSingletonList
// 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
// 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.
ToSingletonList
)

// String returns a string representation of the conversion mode.
func (m conversionMode) String() string {
func (m Mode) String() string {
switch m {
case toSingletonList:
case ToSingletonList:
return "toSingletonList"
case toEmbeddedObject:
case ToEmbeddedObject:
return "toEmbeddedObject"
default:
return "unknown"
Expand Down Expand Up @@ -57,18 +66,18 @@ func setValue(pv *fieldpath.Paved, v any, fp string) error {
return nil
}

// convert performs conversion between singleton lists and embedded objects
// Convert performs conversion between singleton lists and embedded objects
// while passing the CRD parameters to the Terraform layer and while reading
// state from the Terraform layer at runtime. The paths where the conversion
// will be performed are specified using paths and the conversion mode (whether
// 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 conversionMode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit
func Convert(params map[string]any, paths []string, mode Mode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit
switch mode {
case toSingletonList:
case ToSingletonList:
slices.Sort(paths)
case toEmbeddedObject:
case ToEmbeddedObject:
sort.Slice(paths, func(i, j int) bool {
return paths[i] > paths[j]
})
Expand All @@ -89,11 +98,11 @@ func convert(params map[string]any, paths []string, mode conversionMode) (map[st
return nil, errors.Wrapf(err, "cannot get the value at the field path %s with the conversion mode set to %q", exp[0], mode)
}
switch mode {
case toSingletonList:
case ToSingletonList:
if err := setValue(pv, []any{v}, exp[0]); err != nil {
return nil, errors.Wrapf(err, "cannot set the singleton list's value at the field path %s", exp[0])
}
case toEmbeddedObject:
case ToEmbeddedObject:
s, ok := v.([]any)
if !ok || len(s) > 1 {
// if len(s) is 0, then it's not a slice
Expand Down
36 changes: 24 additions & 12 deletions pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,14 +441,14 @@ type Resource struct {
// version is by default the storage version.
MarkStorageVersion bool

// listConversionPaths is the Terraform field paths of embedded objects that
// need to be converted into singleton lists (lists of at most one element)
// at runtime.
// listConversionPaths maps the Terraform field paths of embedded objects
// that need to be converted into singleton lists (lists of
// at most one element) at runtime, to the corresponding CRD paths.
// Such fields are lists in the Terraform schema, however upjet generates
// them as nested objects, and we need to convert them back to lists
// at runtime before passing them to the Terraform stack and lists into
// embedded objects after reading the state from the Terraform stack.
listConversionPaths []string
listConversionPaths map[string]string

// TerraformConfigurationInjector allows a managed resource to inject
// configuration values in the Terraform configuration map obtained by
Expand Down Expand Up @@ -556,16 +556,28 @@ func (m SchemaElementOptions) AddToObservation(el string) bool {
return m[el] != nil && m[el].AddToObservation
}

// ListConversionPaths returns the Resource's runtime Terraform list
// TFListConversionPaths returns the Resource's runtime Terraform list
// conversion paths in fieldpath syntax.
func (r *Resource) ListConversionPaths() []string {
l := make([]string, len(r.listConversionPaths))
copy(l, r.listConversionPaths)
func (r *Resource) TFListConversionPaths() []string {
l := make([]string, 0, len(r.listConversionPaths))
for k := range r.listConversionPaths {
l = append(l, k)
}
return l
}

// CRDListConversionPaths returns the Resource's runtime CRD list
// conversion paths in fieldpath syntax.
func (r *Resource) CRDListConversionPaths() []string {
l := make([]string, 0, len(r.listConversionPaths))
for _, v := range r.listConversionPaths {
l = append(l, v)
}
return l
}

// AddSingletonListConversion configures the list at the specified CRD
// field path and the specified Terraform field path as an embedded object.
// AddSingletonListConversion configures the list at the specified Terraform
// field path and the specified CRD field path as an embedded object.
// crdPath is the field path expression for the CRD schema and tfPath is
// the field path expression for the Terraform schema corresponding to the
// singleton list to be converted to an embedded object.
Expand All @@ -574,14 +586,14 @@ func (r *Resource) ListConversionPaths() []string {
// The specified fieldpath expression must be a wildcard expression such as
// `conditions[*]` or a 0-indexed expression such as `conditions[0]`. Other
// index values are not allowed as this function deals with singleton lists.
func (r *Resource) AddSingletonListConversion(tfPath string) {
func (r *Resource) AddSingletonListConversion(tfPath, crdPath string) {
// SchemaElementOptions.SetEmbeddedObject does not expect the indices and
// because we are dealing with singleton lists here, we only expect wildcards
// or the zero-index.
nPath := strings.ReplaceAll(tfPath, "[*]", "")
nPath = strings.ReplaceAll(nPath, "[0]", "")
r.SchemaElementOptions.SetEmbeddedObject(nPath)
r.listConversionPaths = append(r.listConversionPaths, tfPath)
r.listConversionPaths[tfPath] = crdPath
}

// SetEmbeddedObject sets the EmbeddedObject for the specified key.
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/schema_conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ func (l *SingletonListEmbedder) VisitResource(r *traverser.ResourceNode) error {
if r.Schema.MaxItems != 1 {
return nil
}
l.r.AddSingletonListConversion(traverser.FieldPathWithWildcard(r.TFPath))
l.r.AddSingletonListConversion(traverser.FieldPathWithWildcard(r.TFPath), traverser.FieldPathWithWildcard(r.CRDPath))
return nil
}
9 changes: 5 additions & 4 deletions pkg/controller/external_tfpluginsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/crossplane/upjet/pkg/config"
"github.com/crossplane/upjet/pkg/config/conversion"
"github.com/crossplane/upjet/pkg/metrics"
"github.com/crossplane/upjet/pkg/resource"
"github.com/crossplane/upjet/pkg/resource/json"
Expand Down Expand Up @@ -155,7 +156,7 @@ func getExtendedParameters(ctx context.Context, tr resource.Terraformed, externa
params["tags_all"] = params["tags"]
}
}
return convert(params, config.ListConversionPaths(), toSingletonList)
return conversion.Convert(params, config.TFListConversionPaths(), conversion.ToSingletonList)
}

func (c *TerraformPluginSDKConnector) processParamsWithHCLParser(schemaMap map[string]*schema.Schema, params map[string]any) map[string]any {
Expand Down Expand Up @@ -255,7 +256,7 @@ func (c *TerraformPluginSDKConnector) Connect(ctx context.Context, mg xpresource
if err != nil {
return nil, errors.Wrap(err, "failed to get the observation")
}
tfState, err = convert(tfState, c.config.ListConversionPaths(), toSingletonList)
tfState, err = conversion.Convert(tfState, c.config.TFListConversionPaths(), conversion.ToSingletonList)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -515,7 +516,7 @@ func (n *terraformPluginSDKExternal) Observe(ctx context.Context, mg xpresource.
}
mg.SetConditions(xpv1.Available())

stateValueMap, err = convert(stateValueMap, n.config.ListConversionPaths(), toEmbeddedObject)
stateValueMap, err = conversion.Convert(stateValueMap, n.config.TFListConversionPaths(), conversion.ToEmbeddedObject)
if err != nil {
return managed.ExternalObservation{}, err
}
Expand Down Expand Up @@ -631,7 +632,7 @@ func (n *terraformPluginSDKExternal) Create(ctx context.Context, mg xpresource.M
if _, err := n.setExternalName(mg, stateValueMap); err != nil {
return managed.ExternalCreation{}, errors.Wrap(err, "failed to set the external-name of the managed resource during create")
}
stateValueMap, err = convert(stateValueMap, n.config.ListConversionPaths(), toEmbeddedObject)
stateValueMap, err = conversion.Convert(stateValueMap, n.config.TFListConversionPaths(), conversion.ToEmbeddedObject)
if err != nil {
return managed.ExternalCreation{}, err
}
Expand Down

0 comments on commit 017f4f2

Please sign in to comment.