Skip to content

Commit

Permalink
Make singleton-list-to-embedded-object API conversions optional
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 Apr 17, 2024
1 parent d078959 commit d5c4313
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 44 deletions.
1 change: 1 addition & 0 deletions pkg/config/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ func TestDefaultResource(t *testing.T) {
cmpopts.IgnoreFields(Resource{}, "useTerraformPluginSDKClient"),
cmpopts.IgnoreFields(Resource{}, "useTerraformPluginFrameworkClient"),
cmpopts.IgnoreFields(Resource{}, "requiredFields"),
cmpopts.IgnoreFields(Resource{}, "listConversionPaths"),
}

for name, tc := range cases {
Expand Down
50 changes: 24 additions & 26 deletions pkg/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ import (
"fmt"
"regexp"

"github.com/crossplane/upjet/pkg/schema/traverser"

tfjson "github.com/hashicorp/terraform-json"
fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"

"github.com/crossplane/upjet/pkg/registry"
"github.com/crossplane/upjet/pkg/schema/traverser"
conversiontfjson "github.com/crossplane/upjet/pkg/types/conversion/tfjson"
)

Expand Down Expand Up @@ -157,6 +156,12 @@ type Provider struct {
// resourceConfigurators is a map holding resource configurators where key
// is Terraform resource name.
resourceConfigurators map[string]ResourceConfiguratorChain

// schemaTraversers is a chain of schema traversers to be used with
// this Provider configuration. Schema traversers can be used to inspect or
// modify the Provider configuration based on the underlying Terraform
// resource schemas.
schemaTraversers []traverser.SchemaTraverser
}

// ReferenceInjector injects cross-resource references across the resources
Expand Down Expand Up @@ -259,19 +264,32 @@ func WithFeaturesPackage(s string) ProviderOption {
}
}

// WithMainTemplate configures the provider family main module file's path.
// This template file will be used to generate the main modules of the
// family's members.
func WithMainTemplate(template string) ProviderOption {
return func(p *Provider) {
p.MainTemplate = template
}
}

// WithSchemaTraversers configures a chain of schema traversers to be used with
// this Provider configuration. Schema traversers can be used to inspect or
// modify the Provider configuration based on the underlying Terraform
// resource schemas.
func WithSchemaTraversers(traversers ...traverser.SchemaTraverser) ProviderOption {
return func(p *Provider) {
p.schemaTraversers = traversers
}
}

// NewProvider builds and returns a new Provider from provider
// tfjson schema, that is generated using Terraform CLI with:
// `terraform providers schema --json`
func NewProvider(schema []byte, prefix string, modulePath string, metadata []byte, opts ...ProviderOption) *Provider { //nolint:gocyclo
ps := tfjson.ProviderSchemas{}
if err := ps.UnmarshalJSON(schema); err != nil {
panic(err)
panic(errors.Wrap(err, "failed to unmarshal the Terraform JSON schema"))
}
if len(ps.Schemas) != 1 {
panic(fmt.Sprintf("there should exactly be 1 provider schema but there are %d", len(ps.Schemas)))
Expand Down Expand Up @@ -354,11 +372,9 @@ func NewProvider(schema []byte, prefix string, modulePath string, metadata []byt
p.Resources[name].useTerraformPluginSDKClient = isTerraformPluginSDK
p.Resources[name].useTerraformPluginFrameworkClient = isPluginFrameworkResource
// traverse the Terraform resource schema to initialize the upjet Resource
// configuration using:
// - listEmbedder: This traverser marks lists of length at most 1
// (singleton lists) as embedded objects.
if err := traverser.Traverse(terraformResource, listEmbedder{r: p.Resources[name]}); err != nil {
panic(err)
// configurations
if err := traverseSchemas(name, terraformResource, p.Resources[name], p.schemaTraversers...); err != nil {
panic(errors.Wrap(err, "failed to execute the Terraform schema traverser chain"))
}
}
for i, refInjector := range p.refInjectors {
Expand Down Expand Up @@ -441,21 +457,3 @@ func terraformPluginFrameworkResourceFunctionsMap(provider fwprovider.Provider)

return resourceFunctionsMap
}

type listEmbedder struct {
traverser.NoopTraverser
r *Resource
}

func (l listEmbedder) VisitResource(r *traverser.ResourceNode) error {
// this visitor only works on sets and lists with the MaxItems constraint
// of 1.
if r.Schema.Type != schema.TypeList && r.Schema.Type != schema.TypeSet {
return nil
}
if r.Schema.MaxItems != 1 {
return nil
}
l.r.AddSingletonListConversion(traverser.FieldPathWithWildcard(r.TFPath))
return nil
}
6 changes: 2 additions & 4 deletions pkg/config/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,8 @@ func (m SchemaElementOptions) AddToObservation(el string) bool {
// ListConversionPaths returns the Resource's runtime Terraform list
// conversion paths in fieldpath syntax.
func (r *Resource) ListConversionPaths() []string {
l := make([]string, 0, len(r.listConversionPaths))
for _, v := range r.listConversionPaths {
l = append(l, v)
}
var l []string
copy(l, r.listConversionPaths)
return l
}

Expand Down
57 changes: 57 additions & 0 deletions pkg/config/schema_conversions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
//
// SPDX-License-Identifier: Apache-2.0

package config

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/crossplane/upjet/pkg/schema/traverser"
)

var _ ResourceSetter = &SingletonListEmbedder{}

// ResourceSetter allows the context Resource to be set for a traverser.
type ResourceSetter interface {
SetResource(r *Resource)
}

func traverseSchemas(tfName string, tfResource *schema.Resource, r *Resource, visitors ...traverser.SchemaTraverser) error {
// set the upjet Resource configuration as context for the visitors that
// satisfy the ResourceSetter interface.
for _, v := range visitors {
if rs, ok := v.(ResourceSetter); ok {
rs.SetResource(r)
}
}
return traverser.Traverse(tfName, tfResource, visitors...)
}

type resourceContext struct {
r *Resource
}

func (rc *resourceContext) SetResource(r *Resource) {
rc.r = r
}

// SingletonListEmbedder is a schema traverser for embedding singleton lists
// in the Terraform schema as objects.
type SingletonListEmbedder struct {
resourceContext
traverser.NoopTraverser
}

func (l *SingletonListEmbedder) VisitResource(r *traverser.ResourceNode) error {
// this visitor only works on sets and lists with the MaxItems constraint
// of 1.
if r.Schema.Type != schema.TypeList && r.Schema.Type != schema.TypeSet {
return nil
}
if r.Schema.MaxItems != 1 {
return nil
}
l.r.AddSingletonListConversion(traverser.FieldPathWithWildcard(r.TFPath))
return nil
}
2 changes: 1 addition & 1 deletion pkg/controller/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,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 conversionMode) (map[string]any, error) {
func convert(params map[string]any, paths []string, mode conversionMode) (map[string]any, error) { //nolint:gocyclo // easier to follow as a unit
switch mode {
case toSingletonList:
slices.Sort(paths)
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/external_tfpluginsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ func (n *terraformPluginSDKExternal) setExternalName(mg xpresource.Managed, stat
return oldName != newName, nil
}

func (n *terraformPluginSDKExternal) Create(ctx context.Context, mg xpresource.Managed) (managed.ExternalCreation, error) {
func (n *terraformPluginSDKExternal) Create(ctx context.Context, mg xpresource.Managed) (managed.ExternalCreation, error) { //nolint:gocyclo // easier to follow as a unit
n.logger.Debug("Creating the external resource")
start := time.Now()
newState, diag := n.resourceSchema.Apply(ctx, n.opTracker.GetTfState(), n.instanceDiff, n.ts.Meta)
Expand Down
22 changes: 14 additions & 8 deletions pkg/schema/traverser/traverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ const (
var _ Element = &SchemaNode{}
var _ Element = &ResourceNode{}

// Traverse traverses the Terraform schema of the given Terraform resource and
// visits each of the specified visitors on the traversed schema's nodes.
// Traverse traverses the Terraform schema of the given Terraform resource
// with the given Terraform resource name and visits each of the specified
// visitors on the traversed schema's nodes.
// If any of the visitors in the chain reports an error,
// it stops the traversal.
func Traverse(tfResource *schema.Resource, visitors ...SchemaTraverser) error {
return traverse(tfResource, Node{}, visitors...)
func Traverse(tfName string, tfResource *schema.Resource, visitors ...SchemaTraverser) error {
if len(visitors) == 0 {
return nil
}
return traverse(tfResource, Node{TFName: tfName}, visitors...)
}

// SchemaTraverser represents a visitor on the schema.Schema and
Expand All @@ -44,6 +48,8 @@ type Element interface {

// Node represents a schema node that's being traversed.
type Node struct {
// TFName is the Terraform resource name
TFName string
// Schema is the Terraform schema associated with the visited node during a
// traversal.
Schema *schema.Schema
Expand Down Expand Up @@ -77,18 +83,18 @@ func (r *ResourceNode) Accept(v SchemaTraverser) error {
return v.VisitResource(r)
}

func traverse(tfResource *schema.Resource, pNode Node, visitors ...SchemaTraverser) error {
func traverse(tfResource *schema.Resource, pNode Node, visitors ...SchemaTraverser) error { //nolint:gocyclo // traverse logic is easier to follow in a unit
m := tfResource.Schema
if m == nil && tfResource.SchemaFunc != nil {
m = tfResource.SchemaFunc()
}
if m == nil {
return nil
}
var node Node
node := Node{TFName: pNode.TFName}
for k, s := range m {
node.CRDPath = append(pNode.CRDPath, name.NewFromSnake(k).LowerCamelComputed)
node.TFPath = append(pNode.TFPath, k)
node.CRDPath = append(pNode.CRDPath, name.NewFromSnake(k).LowerCamelComputed) //nolint:gocritic // the parent node's path must not be modified
node.TFPath = append(pNode.TFPath, k) //nolint:gocritic // the parent node's path must not be modified
node.Schema = s
switch e := s.Elem.(type) {
case *schema.Schema:
Expand Down
3 changes: 1 addition & 2 deletions pkg/types/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import (
"sort"
"strings"

"github.com/crossplane/upjet/pkg/schema/traverser"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
twtypes "github.com/muvaf/typewriter/pkg/types"
"github.com/pkg/errors"
"k8s.io/utils/ptr"

"github.com/crossplane/upjet/pkg/config"
"github.com/crossplane/upjet/pkg/schema/traverser"
)

const (
Expand Down
3 changes: 1 addition & 2 deletions pkg/types/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import (
"sort"
"strings"

"github.com/crossplane/upjet/pkg/schema/traverser"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pkg/errors"
"k8s.io/utils/ptr"

"github.com/crossplane/upjet/pkg"
"github.com/crossplane/upjet/pkg/config"
"github.com/crossplane/upjet/pkg/schema/traverser"
"github.com/crossplane/upjet/pkg/types/comments"
"github.com/crossplane/upjet/pkg/types/name"
)
Expand Down

0 comments on commit d5c4313

Please sign in to comment.