Skip to content

Commit

Permalink
Refactor SDKv2 and PF HCL utilities to expose a shared interface (#2589)
Browse files Browse the repository at this point in the history
This change refactors the HCL writing utilities to expose a shared
interface on which we can implement the writing capabilities.

I've opted to make a new HCL shim layer because:
 - We shouldn't complicate the runtime behaviour with test code
- The HCL writing is simpler than the runtime behaviour so we can get
away with a much simpler interface which only cares about Attributes,
Blocks and nesting
 

We did discuss that this is not necessary in
#2577 but I then
hit the need to implement TF lifecycle methods for PF which is already
done for SDKv2, so this makes it possible to implement such shared
concerns once.
  • Loading branch information
VenelinMartinov authored Nov 6, 2024
1 parent e38d894 commit e6ffc06
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 247 deletions.
167 changes: 167 additions & 0 deletions pkg/internal/tests/cross-tests/impl/hclwrite/hclwrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// hclwrite is a shared interface for writing HCL files for cross-tests.
// Both the Terraform Plugin SDK bridge and the Pulumi Framework bridge implement this interface.
package hclwrite

import (
"fmt"
"io"
"sort"

"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/zclconf/go-cty/cty"
)

// Nesting is the nesting mode of a block.
type Nesting string

const (
NestingInvalid Nesting = "NestingInvalid"
NestingSingle Nesting = "NestingSingle"
NestingList Nesting = "NestingList"
NestingSet Nesting = "NestingSet"
)

// ShimHCLAttribute is an attribute used to write a value to an HCL file.
type ShimHCLAttribute struct{}

// ShimHCLBlock is a block used to write a value to an HCL file.
type ShimHCLBlock interface {
GetNestingMode() Nesting
GetAttributes() map[string]ShimHCLAttribute
GetBlocks() map[string]ShimHCLBlock
}

// ShimHCLSchema is the schema used to write a value to an HCL file.
type ShimHCLSchema interface {
GetAttributes() map[string]ShimHCLAttribute
GetBlocks() map[string]ShimHCLBlock
}

// WriteProvider writes a provider declaration to an HCL file.
//
// Note that unknowns are not yet supported in cty.Value, it will error out if found.
func WriteProvider(w io.Writer, schema ShimHCLSchema, providerType string, config map[string]cty.Value) error {
if !cty.ObjectVal(config).IsWhollyKnown() {
return fmt.Errorf("WriteProvider cannot yet write unknowns")
}
f := hclwrite.NewEmptyFile()
block := f.Body().AppendNewBlock("provider", []string{providerType})
writeBlock(block.Body(), schema, config)
_, err := f.WriteTo(w)
return err
}

type lifecycleArgs struct {
CreateBeforeDestroy bool
}

type writeResourceOptions struct {
lifecycleArgs lifecycleArgs
}

// WriteResourceOption is an option for WriteResource.
type WriteResourceOption func(*writeResourceOptions)

// WithCreateBeforeDestroy is an option to set the create_before_destroy attribute on a resource.
func WithCreateBeforeDestroy(createBeforeDestroy bool) WriteResourceOption {
return func(o *writeResourceOptions) {
o.lifecycleArgs.CreateBeforeDestroy = createBeforeDestroy
}
}

// WriteResource writes a resource declaration to an HCL file.
//
// Note that unknowns are not yet supported in cty.Value, it will error out if found.
func WriteResource(
w io.Writer, schema ShimHCLSchema, resourceType, resourceName string, config map[string]cty.Value,
opts ...WriteResourceOption,
) error {
if !cty.ObjectVal(config).IsWhollyKnown() {
return fmt.Errorf("WriteResource cannot yet write unknowns")
}
o := &writeResourceOptions{}
for _, opt := range opts {
opt(o)
}

if config == nil {
config = map[string]cty.Value{}
}

f := hclwrite.NewEmptyFile()
block := f.Body().AppendNewBlock("resource", []string{resourceType, resourceName})
writeBlock(block.Body(), schema, config)

// lifecycle block
contract.Assertf(config["lifecycle"].IsNull(), "lifecycle block should be specified with a lifecycle option")
lifecycle := map[string]cty.Value{}
if o.lifecycleArgs.CreateBeforeDestroy {
lifecycle["create_before_destroy"] = cty.True
}
if len(lifecycle) > 0 {
newBlock := block.Body().AppendNewBlock("lifecycle", nil)
writeBlock(newBlock.Body(), &lifecycleBlock{}, lifecycle)
}
_, err := f.WriteTo(w)
return err
}

type lifecycleBlock struct{}

var _ ShimHCLBlock = &lifecycleBlock{}

func (b *lifecycleBlock) GetNestingMode() Nesting {
return NestingSingle
}

func (b *lifecycleBlock) GetAttributes() map[string]ShimHCLAttribute {
return map[string]ShimHCLAttribute{
"create_before_destroy": {},
}
}

func (b *lifecycleBlock) GetBlocks() map[string]ShimHCLBlock {
return map[string]ShimHCLBlock{}
}

func writeBlock(body *hclwrite.Body, schema ShimHCLSchema, config map[string]cty.Value) {
attributeList := make([]string, 0, len(schema.GetAttributes()))
for key := range schema.GetAttributes() {
attributeList = append(attributeList, key)
}
sort.Strings(attributeList)
for _, key := range attributeList {
v, ok := config[key]
if !ok {
continue
}
body.SetAttributeValue(key, v)
}

blockList := make([]string, 0, len(schema.GetBlocks()))
for key := range schema.GetBlocks() {
blockList = append(blockList, key)
}
sort.Strings(blockList)

for _, key := range blockList {
block := schema.GetBlocks()[key]
if v, ok := config[key]; !ok || v.IsNull() {
continue
}

switch block.GetNestingMode() {
case NestingSingle:
newBlock := body.AppendNewBlock(key, nil)
writeBlock(newBlock.Body(), block, config[key].AsValueMap())
case NestingList, NestingSet:
for _, elem := range config[key].AsValueSlice() {
newBlock := body.AppendNewBlock(key, nil)
writeBlock(newBlock.Body(), block, elem.AsValueMap())
}
default:
contract.Failf("unexpected nesting mode %v", block.GetNestingMode())
}
}
}
19 changes: 8 additions & 11 deletions pkg/internal/tests/cross-tests/tf_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/zclconf/go-cty/cty"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests/impl/hclwrite"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/tfcheck"
sdkv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2"
)
Expand Down Expand Up @@ -105,32 +106,28 @@ func (d *TfResDriver) write(
lifecycle lifecycleArgs,
) {
var buf bytes.Buffer
opts := []hclwrite.WriteResourceOption{}
if lifecycle.CreateBeforeDestroy {
ctyMap := config.AsValueMap()
if ctyMap == nil {
ctyMap = map[string]cty.Value{}
}
ctyMap["lifecycle"] = cty.ObjectVal(map[string]cty.Value{
"create_before_destroy": cty.True,
})
config = cty.ObjectVal(ctyMap)
opts = append(opts, hclwrite.WithCreateBeforeDestroy(true))
}
err := writeResource(&buf, resourceSchema, resourceType, resourceName, config)
sch := hclSchemaSDKv2(resourceSchema)
err := hclwrite.WriteResource(&buf, sch, resourceType, resourceName, config.AsValueMap(), opts...)
require.NoError(t, err)
t.Logf("HCL: \n%s\n", buf.String())
d.driver.Write(t, buf.String())
}

func providerHCLProgram(t T, typ string, provider *schema.Provider, config cty.Value) string {
var out bytes.Buffer
require.NoError(t, writeProvider(&out, provider.Schema, typ, config))
sch := hclSchemaSDKv2(provider.Schema)
require.NoError(t, hclwrite.WriteProvider(&out, sch, typ, config.AsValueMap()))

res := provider.Resources()
if l := len(res); l != 1 {
require.FailNow(t, "Expected provider to have 1 resource (found %d), ambiguous resource choice", l)
}

require.NoError(t, writeResource(&out, map[string]*schema.Schema{}, res[0].Name, "res", cty.EmptyObjectVal))
require.NoError(t, hclwrite.WriteResource(&out, sch, res[0].Name, "res", map[string]cty.Value{}))

return out.String()
}
152 changes: 57 additions & 95 deletions pkg/internal/tests/cross-tests/tfwrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,115 +16,77 @@
package crosstests

import (
"fmt"
"io"
"sort"

"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/zclconf/go-cty/cty"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests/impl/hclwrite"
)

// Provider writes a provider declaration to an HCL file.
//
// Note that unknowns are not yet supported in cty.Value, it will error out if found.
func writeProvider(out io.Writer, sch map[string]*schema.Schema, typ string, config cty.Value) error {
if !config.IsWhollyKnown() {
return fmt.Errorf("WriteHCL cannot yet write unknowns")
}
f := hclwrite.NewEmptyFile()
block := f.Body().AppendNewBlock("provider", []string{typ})
writeBlock(block.Body(), sch, config.AsValueMap())
_, err := f.WriteTo(out)
return err
}
// This is a copy of the NestingMode enum in the Terraform Plugin SDK.
// It is duplicated here because the type is not exported.
type sdkV2NestingMode int

// Resource writes a resource declaration to an HCL file.
//
// Note that unknowns are not yet supported in cty.Value, it will error out if found.
func writeResource(
out io.Writer, sch map[string]*schema.Schema, resourceType, resourceName string, config cty.Value,
) error {
if !config.IsWhollyKnown() {
return fmt.Errorf("WriteHCL cannot yet write unknowns")
const (
sdkV2NestingModeInvalid sdkV2NestingMode = iota
sdkV2NestingModeSingle
sdkV2NestingModeGroup
sdkV2NestingModeList
sdkV2NestingModeSet
sdkV2NestingModeMap
)

func sdkV2NestingToShim(nesting sdkV2NestingMode) hclwrite.Nesting {
switch nesting {
case sdkV2NestingModeSingle:
return hclwrite.NestingSingle
case sdkV2NestingModeList:
return hclwrite.NestingList
case sdkV2NestingModeSet:
return hclwrite.NestingSet
default:
contract.Failf("Unexpected nesting mode: %d for the SDKv2 schema", nesting)
return hclwrite.NestingInvalid
}
f := hclwrite.NewEmptyFile()
block := f.Body().AppendNewBlock("resource", []string{resourceType, resourceName})
writeBlock(block.Body(), sch, config.AsValueMap())
_, err := f.WriteTo(out)
return err
}

func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values map[string]cty.Value) {
internalMap := schema.InternalMap(schemas)
coreConfigSchema := internalMap.CoreConfigSchema()
type hclSchemaSDKv2 map[string]*schema.Schema

blockKeys := make([]string, 0, len(coreConfigSchema.BlockTypes))
for key := range coreConfigSchema.BlockTypes {
blockKeys = append(blockKeys, key)
}
sort.Strings(blockKeys)
var _ hclwrite.ShimHCLSchema = hclSchemaSDKv2{}

for _, key := range blockKeys {
bl := coreConfigSchema.BlockTypes[key]
switch bl.Nesting.String() {
case "NestingSingle":
v, ok := values[key]
if !ok {
continue
}
newBlock := body.AppendNewBlock(key, nil)
res, ok := schemas[key].Elem.(*schema.Resource)
if !ok {
contract.Failf("unexpected schema type %s", key)
}
writeBlock(newBlock.Body(), res.Schema, v.AsValueMap())
case "NestingGroup":
contract.Failf("unexpected NestingGroup for %s with schema %s", key, schemas[key].GoString())
case "NestingList", "NestingSet":
v, ok := values[key]
if !ok {
continue
}
res, ok := schemas[key].Elem.(*schema.Resource)
if !ok {
contract.Failf("unexpected schema type %s", key)
}
for _, elem := range v.AsValueSlice() {
newBlock := body.AppendNewBlock(key, nil)
writeBlock(newBlock.Body(), res.Schema, elem.AsValueMap())
}
case "NestingMap":
contract.Failf("unexpected NestingMap for %s with schema %s", key, schemas[key].GoString())
default:
contract.Failf("unexpected nesting mode %v", bl.Nesting)
}
func (s hclSchemaSDKv2) GetAttributes() map[string]hclwrite.ShimHCLAttribute {
internalMap := schema.InternalMap(s)
coreConfigSchema := internalMap.CoreConfigSchema()
attrMap := make(map[string]hclwrite.ShimHCLAttribute, len(s))
for key := range coreConfigSchema.Attributes {
attrMap[key] = hclwrite.ShimHCLAttribute{}
}
return attrMap
}

// lifecycle block
if _, ok := values["lifecycle"]; ok {
newBlock := body.AppendNewBlock("lifecycle", nil)
lifecycleSchema := map[string]*schema.Schema{
"create_before_destroy": {
Type: schema.TypeBool,
Optional: true,
},
func (s hclSchemaSDKv2) GetBlocks() map[string]hclwrite.ShimHCLBlock {
internalMap := schema.InternalMap(s)
coreConfigSchema := internalMap.CoreConfigSchema()
blockMap := make(map[string]hclwrite.ShimHCLBlock, len(coreConfigSchema.BlockTypes))
for key, block := range coreConfigSchema.BlockTypes {
res := s[key].Elem.(*schema.Resource)
nesting := block.Nesting
blockMap[key] = hclBlockSDKv2{
hclSchemaSDKv2: hclSchemaSDKv2(res.Schema),
nesting: sdkV2NestingToShim(sdkV2NestingMode(nesting)),
}
writeBlock(newBlock.Body(), lifecycleSchema, values["lifecycle"].AsValueMap())
}
return blockMap
}

attrKeys := make([]string, 0, len(coreConfigSchema.Attributes))
for key := range coreConfigSchema.Attributes {
attrKeys = append(attrKeys, key)
}
sort.Strings(attrKeys)
type hclBlockSDKv2 struct {
hclSchemaSDKv2
nesting hclwrite.Nesting
}

for _, key := range attrKeys {
v, ok := values[key]
if !ok {
continue
}
body.SetAttributeValue(key, v)
}
var _ hclwrite.ShimHCLBlock = hclBlockSDKv2{}

func (b hclBlockSDKv2) GetNestingMode() hclwrite.Nesting {
return b.nesting
}

var _ hclwrite.ShimHCLBlock = hclBlockSDKv2{}
Loading

0 comments on commit e6ffc06

Please sign in to comment.