Skip to content

Commit

Permalink
SDKv2 cross-tests: allow inferring Pulumi values (#2505)
Browse files Browse the repository at this point in the history
This PR expands SDKv2 cross-tests by adding the
`crosstest.InferPulumiValue()` marker value, which can be used to tell
the cross-testing framework to figure out the Pulumi value.

It removes the cumbersome `convertConfigValueForYamlProperties`
function, since it is no longer necessary, tests can just
`InferPulumiValue()`.
  • Loading branch information
iwahbe authored Oct 23, 2024
1 parent 93162c7 commit 35b21fc
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 185 deletions.
125 changes: 125 additions & 0 deletions pkg/internal/tests/cross-tests/adapt.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,138 @@
package crosstests

import (
"context"
"fmt"
"math/big"

"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/logging"
)

// InferPulumiValue is a special marker value that tells cross-tests to infer the Pulumi
// value from the TF value.
func InferPulumiValue() resource.PropertyMap {
return resource.PropertyMap{
inferPulumiValueKey: resource.NewProperty(&inferPulumiValueSecret),
}
}

const inferPulumiValueKey = "inferPulumiValue"

var inferPulumiValueSecret = resource.Secret{}

func isInferPulumiMarker(m resource.PropertyMap) bool {
v, ok := m[inferPulumiValueKey]
return ok && v.IsSecret() && v.SecretValue() == &inferPulumiValueSecret
}

// inferPulumiValue generates a Pulumi value that is semantically equivalent to v.
//
// inferPulumiValue takes into account schema information. [InferPulumiValue] is the
// marker value that instructs [crosstests] to invoke [inferPulumiValue].
func inferPulumiValue(t T, schema shim.SchemaMap, infos map[string]*info.Schema, v cty.Value) resource.PropertyMap {
if v.IsNull() {
return nil
}
decoder, err := convert.NewObjectDecoder(convert.ObjectSchema{
SchemaMap: schema,
SchemaInfos: infos,
})
require.NoError(t, err)

ctx := logging.InitLogging(context.Background(), logging.LogOptions{})
// There is not yet a way to opt out of marking schema secrets, so the resulting map might have secrets marked.
pm, err := convert.DecodePropertyMap(ctx, decoder, ctyToTftypes(v))
require.NoError(t, err)
return pm
}

func ctyToTftypes(v cty.Value) tftypes.Value {
typ := v.Type()
if !v.IsKnown() {
return tftypes.NewValue(ctyTypeToTfType(typ), tftypes.UnknownValue)
}
if v.IsNull() {
return tftypes.NewValue(ctyTypeToTfType(typ), nil)
}
switch {
case typ.Equals(cty.String):
return tftypes.NewValue(ctyTypeToTfType(typ), v.AsString())
case typ.Equals(cty.Bool):
return tftypes.NewValue(ctyTypeToTfType(typ), v.True())
case typ.Equals(cty.Number):
return tftypes.NewValue(ctyTypeToTfType(typ), v.AsBigFloat())

case typ.IsListType():
src := v.AsValueSlice()
dst := make([]tftypes.Value, len(src))
for i, v := range src {
dst[i] = ctyToTftypes(v)
}
return tftypes.NewValue(ctyTypeToTfType(typ), dst)
case typ.IsSetType():
src := v.AsValueSet().Values()
dst := make([]tftypes.Value, len(src))
for i, v := range src {
dst[i] = ctyToTftypes(v)
}
return tftypes.NewValue(ctyTypeToTfType(typ), dst)
case typ.IsMapType():
src := v.AsValueMap()
dst := make(map[string]tftypes.Value, len(src))
for k, v := range src {
dst[k] = ctyToTftypes(v)
}
return tftypes.NewValue(ctyTypeToTfType(typ), dst)
case typ.IsObjectType():
src := v.AsValueMap()
dst := make(map[string]tftypes.Value, len(src))
for k, v := range src {
dst[k] = ctyToTftypes(v)
}
return tftypes.NewValue(ctyTypeToTfType(typ), dst)
default:
panic(fmt.Sprintf("unknown type %s", typ.GoString()))
}
}

func ctyTypeToTfType(typ cty.Type) tftypes.Type {
switch {
case typ.Equals(cty.String):
return tftypes.String
case typ.Equals(cty.Bool):
return tftypes.Bool
case typ.Equals(cty.Number):
return tftypes.Number
case typ == cty.DynamicPseudoType:
return tftypes.DynamicPseudoType

case typ.IsListType():
return tftypes.List{ElementType: ctyTypeToTfType(typ.ElementType())}
case typ.IsSetType():
return tftypes.Set{ElementType: ctyTypeToTfType(typ.ElementType())}
case typ.IsMapType():
return tftypes.Map{ElementType: ctyTypeToTfType(typ.ElementType())}
case typ.IsObjectType():
src := typ.AttributeTypes()
dst := make(map[string]tftypes.Type, len(src))
for k, v := range src {
dst[k] = ctyTypeToTfType(v)
}
return tftypes.Object{AttributeTypes: dst}
default:
panic(fmt.Sprintf("unknown type %s", typ.GoString()))
}
}

type typeAdapter struct {
typ tftypes.Type
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/internal/tests/cross-tests/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/pulcheck"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2"
)

// Create validates that a Terraform provider witnesses the same input when:
Expand All @@ -41,11 +42,20 @@ func Create(
t T, resource map[string]*schema.Schema, tfConfig cty.Value, puConfig resource.PropertyMap,
options ...CreateOption,
) {

var opts createOpts
for _, f := range options {
f(&opts)
}

if isInferPulumiMarker(puConfig) {
puConfig = inferPulumiValue(t,
shimv2.NewSchemaMap(resource),
opts.resourceInfo.GetFields(),
tfConfig,
)
}

type result struct {
data *schema.ResourceData
meta any
Expand Down
8 changes: 4 additions & 4 deletions pkg/internal/tests/cross-tests/diff_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ func runDiffCheck(t T, tc diffTestCase) diffResult {
tfResourceName: defRtype,
}

yamlProgram := pd.generateYAML(t, convertConfigValueForYamlProperties(t,
bridgedProvider.P.ResourcesMap().Get(defRtype).Schema(), tc.ObjectType, tc.Config1))
yamlProgram := pd.generateYAML(t, inferPulumiValue(t,
bridgedProvider.P.ResourcesMap().Get(defRtype).Schema(), nil, tfConfig1))
pt := pulcheck.PulCheck(t, bridgedProvider, string(yamlProgram))
pt.Up(t)

yamlProgram = pd.generateYAML(t, convertConfigValueForYamlProperties(t,
bridgedProvider.P.ResourcesMap().Get(defRtype).Schema(), tc.ObjectType, tc.Config2))
yamlProgram = pd.generateYAML(t, inferPulumiValue(t,
bridgedProvider.P.ResourcesMap().Get(defRtype).Schema(), nil, tfConfig2))
err := os.WriteFile(filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml"), yamlProgram, 0o600)
require.NoErrorf(t, err, "writing Pulumi.yaml")
x := pt.Up(t)
Expand Down
4 changes: 1 addition & 3 deletions pkg/internal/tests/cross-tests/input_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ package crosstests
import (
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2"
)

// Adapted from diff_check.go
Expand All @@ -41,7 +39,7 @@ func runCreateInputCheck(t T, tc inputTestCase) {
Create(t,
tc.Resource.Schema,
coalesceInputs(t, tc.Resource.Schema, tc.Config),
convertConfigValueForYamlProperties(t, shimv2.NewResource(tc.Resource).Schema(), tc.ObjectType, tc.Config),
InferPulumiValue(),
CreateStateUpgrader(tc.Resource.SchemaVersion, tc.Resource.StateUpgraders),
CreateTimeout(tc.Resource.Timeouts),
)
Expand Down
87 changes: 0 additions & 87 deletions pkg/internal/tests/cross-tests/input_cross_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,93 +103,6 @@ func TestInputsEqualObjectBasic(t *testing.T) {
}
}

func TestInputsConfigModeEqual(t *testing.T) {
// Regression test for [pulumi/pulumi-terraform-bridge#1762]
t2 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{
"x": tftypes.String,
}}

t1 := tftypes.List{ElementType: t2}
t0 := tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"f0": t1,
},
}
t3 := tftypes.Object{}

emptyConfig := tftypes.NewValue(
t3,
map[string]tftypes.Value{},
)

emptyListConfig := tftypes.NewValue(
t0,
map[string]tftypes.Value{
"f0": tftypes.NewValue(t1, []tftypes.Value{}),
},
)

nonEmptyConfig := tftypes.NewValue(
t0,
map[string]tftypes.Value{
"f0": tftypes.NewValue(t1, []tftypes.Value{
tftypes.NewValue(t2, map[string]tftypes.Value{
"x": tftypes.NewValue(tftypes.String, "val"),
}),
}),
},
)

for _, tc := range []struct {
name string
config tftypes.Value
maxItems int
configMode schema.SchemaConfigMode
}{
{"MaxItems: 0, ConfigMode: Auto, Empty", emptyConfig, 0, schema.SchemaConfigModeAuto},
{"MaxItems: 0, ConfigMode: Auto, EmptyList", emptyListConfig, 0, schema.SchemaConfigModeAuto},
{"MaxItems: 0, ConfigMode: Auto, NonEmpty", nonEmptyConfig, 0, schema.SchemaConfigModeAuto},
{"MaxItems: 0, ConfigMode: Block, Empty", emptyConfig, 0, schema.SchemaConfigModeBlock},
{"MaxItems: 0, ConfigMode: Block, EmptyList", emptyListConfig, 0, schema.SchemaConfigModeBlock},
{"MaxItems: 0, ConfigMode: Block, NonEmpty", nonEmptyConfig, 0, schema.SchemaConfigModeBlock},
{"MaxItems: 0, ConfigMode: Attr, Empty", emptyConfig, 0, schema.SchemaConfigModeAttr},
{"MaxItems: 0, ConfigMode: Attr, EmptyList", emptyListConfig, 0, schema.SchemaConfigModeAttr},
{"MaxItems: 0, ConfigMode: Attr, NonEmpty", nonEmptyConfig, 0, schema.SchemaConfigModeAttr},
{"MaxItems: 1, ConfigMode: Auto, Empty", emptyConfig, 1, schema.SchemaConfigModeAuto},
{"MaxItems: 1, ConfigMode: Auto, EmptyList", emptyListConfig, 1, schema.SchemaConfigModeAuto},
{"MaxItems: 1, ConfigMode: Auto, NonEmpty", nonEmptyConfig, 1, schema.SchemaConfigModeAuto},
{"MaxItems: 1, ConfigMode: Block, Empty", emptyConfig, 1, schema.SchemaConfigModeBlock},
{"MaxItems: 1, ConfigMode: Block, EmptyList", emptyListConfig, 1, schema.SchemaConfigModeBlock},
{"MaxItems: 1, ConfigMode: Block, NonEmpty", nonEmptyConfig, 1, schema.SchemaConfigModeBlock},
{"MaxItems: 1, ConfigMode: Attr, Empty", emptyConfig, 1, schema.SchemaConfigModeAttr},
// TODO[pulumi/pulumi-terraform-bridge#2025]
// This is not expressible in pulumi after the ConfigModeOne flattening.
// {"MaxItems: 1, ConfigMode: Attr, EmptyList", emptyListConfig, 1, schema.SchemaConfigModeAttr},
{"MaxItems: 1, ConfigMode: Attr, NonEmpty", nonEmptyConfig, 1, schema.SchemaConfigModeAttr},
} {
t.Run(tc.name, func(t *testing.T) {
runCreateInputCheck(t, inputTestCase{
Resource: &schema.Resource{
Schema: map[string]*schema.Schema{
"f0": {
Optional: true,
Type: schema.TypeList,
MaxItems: tc.maxItems,
ConfigMode: tc.configMode,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"x": {Optional: true, Type: schema.TypeString},
},
},
},
},
},
Config: tc.config,
})
})
}
}

// Isolated from rapid-generated tests
func TestInputsEmptyString(t *testing.T) {
runCreateInputCheck(t, inputTestCase{
Expand Down
Loading

0 comments on commit 35b21fc

Please sign in to comment.