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 dd1fa08
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 29 deletions.
47 changes: 23 additions & 24 deletions pkg/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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 +265,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 +373,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 +458,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
}
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
}
16 changes: 11 additions & 5 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 @@ -85,7 +91,7 @@ func traverse(tfResource *schema.Resource, pNode Node, visitors ...SchemaTravers
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)
Expand Down

0 comments on commit dd1fa08

Please sign in to comment.