diff --git a/pf/tests/schemashim_test.go b/pf/tests/schemashim_test.go index c621b0d4b..8fb1658b7 100644 --- a/pf/tests/schemashim_test.go +++ b/pf/tests/schemashim_test.go @@ -14,64 +14,794 @@ package tfbridgetests +// Test how various PF-based schemata translate to the shim.Schema layer. Excerpts of the resulting Pulumi Package +// Schema are included for reasoning convenience. +// +// References: +// +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/attributes +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/blocks + import ( "context" "encoding/json" "io" "testing" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hexops/autogold/v2" - "github.com/pulumi/pulumi-terraform-bridge/pf/internal/schemashim" - pb "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen" - "github.com/pulumi/pulumi/sdk/v3/go/common/diag" - "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" - "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" - "github.com/stretchr/testify/require" -) + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hexops/autogold/v2" + "github.com/pulumi/pulumi-terraform-bridge/pf/internal/schemashim" + pb "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/stretchr/testify/require" +) + +func TestShimBoolAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool_attr": schema.BoolAttribute{Optional: true}, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "bool_attr": { + "optional": true, + "type": 1 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "boolAttr": { + "type": "boolean" + } + }, + "inputProperties": { + "boolAttr": { + "type": "boolean" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "boolAttr": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimStringAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "str_attr": schema.StringAttribute{Optional: true}, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "str_attr": { + "optional": true, + "type": 4 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "strAttr": { + "type": "string" + } + }, + "inputProperties": { + "strAttr": { + "type": "string" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "strAttr": { + "type": "string" + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimNumberAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "num_attr": schema.NumberAttribute{Optional: true}, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "num_attr": { + "optional": true, + "type": 3 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "numAttr": { + "type": "number" + } + }, + "inputProperties": { + "numAttr": { + "type": "number" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "numAttr": { + "type": "number" + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimListOfStringAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_attr": schema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "list_attr": { + "element": { + "schema": { + "type": 4 + } + }, + "optional": true, + "type": 5 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "listAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "inputProperties": { + "listAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "listAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimMapOfStringAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "map_attr": schema.MapAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "map_attr": { + "element": { + "schema": { + "type": 4 + } + }, + "optional": true, + "type": 6 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "mapAttr": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "inputProperties": { + "mapAttr": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "mapAttr": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimSetOfStringAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "set_attr": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "set_attr": { + "element": { + "schema": { + "type": 4 + } + }, + "optional": true, + "type": 7 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "setAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "inputProperties": { + "setAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "setAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} + +func TestShimListNestedAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "list_nested_attr": schema.ListNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "x": schema.StringAttribute{Optional: true}, + }, + }, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "list_nested_attr": { + "element": { + "schema": { + "element": { + "resource": { + "x": { + "optional": true, + "type": 4 + } + } + }, + "type": 6 + } + }, + "optional": true, + "type": 5 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "listNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1ListNestedAttr:R1ListNestedAttr" + } + } + }, + "inputProperties": { + "listNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1ListNestedAttr:R1ListNestedAttr" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "listNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1ListNestedAttr:R1ListNestedAttr" + } + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/R1ListNestedAttr:R1ListNestedAttr": { + "properties": { + "x": { + "type": "string" + } + }, + "type": "object" + } + } +}`), + }) +} + +func TestShimSetNestedAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "set_nested_attr": schema.SetNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "x": schema.StringAttribute{Optional: true}, + }, + }, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "set_nested_attr": { + "element": { + "schema": { + "element": { + "resource": { + "x": { + "optional": true, + "type": 4 + } + } + }, + "type": 6 + } + }, + "optional": true, + "type": 7 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "setNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1SetNestedAttr:R1SetNestedAttr" + } + } + }, + "inputProperties": { + "setNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1SetNestedAttr:R1SetNestedAttr" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "setNestedAttrs": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1SetNestedAttr:R1SetNestedAttr" + } + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/R1SetNestedAttr:R1SetNestedAttr": { + "properties": { + "x": { + "type": "string" + } + }, + "type": "object" + } + } +}`), + }) +} + +func TestShimMapNestedAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "map_nested_attr": schema.MapNestedAttribute{ + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "x": schema.StringAttribute{Optional: true}, + }, + }, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "map_nested_attr": { + "element": { + "schema": { + "element": { + "resource": { + "x": { + "optional": true, + "type": 4 + } + } + }, + "type": 6 + } + }, + "optional": true, + "type": 6 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "mapNestedAttr": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprov:index/R1MapNestedAttr:R1MapNestedAttr" + } + } + }, + "inputProperties": { + "mapNestedAttr": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprov:index/R1MapNestedAttr:R1MapNestedAttr" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "mapNestedAttr": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprov:index/R1MapNestedAttr:R1MapNestedAttr" + } + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/R1MapNestedAttr:R1MapNestedAttr": { + "properties": { + "x": { + "type": "string" + } + }, + "type": "object" + } + } +}`), + }) +} + +func TestShimSingleNestedAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "single_nested_attr": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "x": schema.StringAttribute{Optional: true}, + }, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "single_nested_attr": { + "element": { + "resource": { + "x": { + "optional": true, + "type": 4 + } + } + }, + "optional": true, + "type": 6 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "singleNestedAttr": { + "$ref": "#/types/testprov:index/R1SingleNestedAttr:R1SingleNestedAttr" + } + }, + "inputProperties": { + "singleNestedAttr": { + "$ref": "#/types/testprov:index/R1SingleNestedAttr:R1SingleNestedAttr" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "singleNestedAttr": { + "$ref": "#/types/testprov:index/R1SingleNestedAttr:R1SingleNestedAttr" + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/R1SingleNestedAttr:R1SingleNestedAttr": { + "properties": { + "x": { + "type": "string" + } + }, + "type": "object" + } + } +}`), + }) +} -// Test how various PF-based schemata translate to the shim.Schema layer. Excerpts of the resulting Pulumi Package -// Schema are included for reasoning convenience. -func TestSchemaShimRepresentations(t *testing.T) { +func TestShimObjectAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "obj_attr": schema.ObjectAttribute{ + Optional: true, + AttributeTypes: map[string]attr.Type{ + "x": types.StringType, + }, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "obj_attr": { + "element": { + "resource": { + "x": { + "type": 4 + } + } + }, + "optional": true, + "type": 6 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "objAttr": { + "$ref": "#/types/testprov:index/R1ObjAttr:R1ObjAttr" + } + }, + "inputProperties": { + "objAttr": { + "$ref": "#/types/testprov:index/R1ObjAttr:R1ObjAttr" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "objAttr": { + "$ref": "#/types/testprov:index/R1ObjAttr:R1ObjAttr" + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/R1ObjAttr:R1ObjAttr": { + "properties": { + "x": { + "type": "string" + } + }, + "type": "object", + "required": [ + "x" + ] + } + } +}`), + }) +} - type testCase struct { - name string - provider provider.Provider - expect autogold.Value // expected prettified shim.Schema representation - expectSchema autogold.Value // expected corresponding Pulumi Package Schema extract - } +func TestShimDynamicAttr(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Attributes: map[string]schema.Attribute{ + "obj_attr": schema.DynamicAttribute{ + Optional: true, + }, + }, + }), + autogold.Expect(`{ + "resources": { + "testprov_r1": { + "obj_attr": { + "optional": true, + "type": 8 + } + } + } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "objAttr": { + "$ref": "pulumi.json#/Any" + } + }, + "inputProperties": { + "objAttr": { + "$ref": "pulumi.json#/Any" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering R1 resources.\n", + "properties": { + "objAttr": { + "$ref": "pulumi.json#/Any" + } + }, + "type": "object" + } + }, + "types": {} +}`), + }) +} - testCases := []testCase{ - //------------------------------------------------------------------------------------------------------ - { - "single-nested-block", - &pb.Provider{ - TypeName: "testprov", - AllResources: []pb.Resource{{ - Name: "r1", - ResourceSchema: schema.Schema{ - Blocks: map[string]schema.Block{ - "single_nested_block": schema.SingleNestedBlock{ - Attributes: map[string]schema.Attribute{ - "a1": schema.Float64Attribute{ - Optional: true, - }, - }, - }, - }, +func TestShimSingleNestedBlock(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Blocks: map[string]schema.Block{ + "blk": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "a1": schema.Float64Attribute{Optional: true}, }, - }}, + }, }, - autogold.Expect(`{ + }), + autogold.Expect(`{ "resources": { "testprov_r1": { - "single_nested_block": { + "blk": { "element": { "resource": { "a1": { @@ -86,30 +816,30 @@ func TestSchemaShimRepresentations(t *testing.T) { } } }`), - autogold.Expect(`{ + autogold.Expect(`{ "resource": { "properties": { - "singleNestedBlock": { - "$ref": "#/types/testprov:index/R1SingleNestedBlock:R1SingleNestedBlock" + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "inputProperties": { - "singleNestedBlock": { - "$ref": "#/types/testprov:index/R1SingleNestedBlock:R1SingleNestedBlock" + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "stateInputs": { "description": "Input properties used for looking up and filtering R1 resources.\n", "properties": { - "singleNestedBlock": { - "$ref": "#/types/testprov:index/R1SingleNestedBlock:R1SingleNestedBlock" + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "type": "object" } }, "types": { - "testprov:index/R1SingleNestedBlock:R1SingleNestedBlock": { + "testprov:index/R1Blk:R1Blk": { "properties": { "a1": { "type": "number" @@ -119,33 +849,26 @@ func TestSchemaShimRepresentations(t *testing.T) { } } }`), - }, - //------------------------------------------------------------------------------------------------------ - { - "list-nested-block", - &pb.Provider{ - TypeName: "testprov", - AllResources: []pb.Resource{{ - Name: "r1", - ResourceSchema: schema.Schema{ - Blocks: map[string]schema.Block{ - "list_nested_block": schema.ListNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "a1": schema.Float64Attribute{ - Optional: true, - }, - }, - }, - }, + }) +} + +func TestShimListNestedBlock(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Blocks: map[string]schema.Block{ + "blk": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "a1": schema.Float64Attribute{Optional: true}, }, }, - }}, + }, }, - autogold.Expect(`{ + }), + autogold.Expect(`{ "resources": { "testprov_r1": { - "list_nested_block": { + "blk": { "element": { "resource": { "a1": { @@ -160,31 +883,31 @@ func TestSchemaShimRepresentations(t *testing.T) { } } }`), - autogold.Expect(`{ + autogold.Expect(`{ "resource": { "properties": { - "listNestedBlocks": { + "blks": { "type": "array", "items": { - "$ref": "#/types/testprov:index/R1ListNestedBlock:R1ListNestedBlock" + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } } }, "inputProperties": { - "listNestedBlocks": { + "blks": { "type": "array", "items": { - "$ref": "#/types/testprov:index/R1ListNestedBlock:R1ListNestedBlock" + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } } }, "stateInputs": { "description": "Input properties used for looking up and filtering R1 resources.\n", "properties": { - "listNestedBlocks": { + "blks": { "type": "array", "items": { - "$ref": "#/types/testprov:index/R1ListNestedBlock:R1ListNestedBlock" + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } } }, @@ -192,7 +915,7 @@ func TestSchemaShimRepresentations(t *testing.T) { } }, "types": { - "testprov:index/R1ListNestedBlock:R1ListNestedBlock": { + "testprov:index/R1Blk:R1Blk": { "properties": { "a1": { "type": "number" @@ -202,218 +925,218 @@ func TestSchemaShimRepresentations(t *testing.T) { } } }`), - }, - //------------------------------------------------------------------------------------------------------ - { - "map-nested-attribute", - &pb.Provider{ - TypeName: "testprov", - AllResources: []pb.Resource{{ - Name: "r1", - ResourceSchema: schema.Schema{ + }) +} + +// The bridge attempts some heuristics to infer listvalidator.SizeAtMost(1) and apply flattening. It is unclear how +// often it is used but there are non-0 actual examples, such as data_storage on elasticache serverless_cache in AWS. +func TestShimListNestedFlattenedBlock(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Blocks: map[string]schema.Block{ + "blk": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "map_nested_attribute": schema.MapNestedAttribute{ - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "a1": schema.StringAttribute{ - Optional: true, - }, - }, - }, - }, + "a1": schema.Float64Attribute{Optional: true}, }, }, - }}, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, }, - autogold.Expect(`{ + }), + autogold.Expect(`{ "resources": { "testprov_r1": { - "map_nested_attribute": { + "blk": { "element": { - "schema": { - "element": { - "resource": { - "a1": { - "optional": true, - "type": 4 - } - } - }, - "type": 6 + "resource": { + "a1": { + "optional": true, + "type": 3 + } } }, + "maxItems": 1, "optional": true, - "type": 6 + "type": 5 } } } }`), - autogold.Expect(`{ + autogold.Expect(`{ "resource": { "properties": { - "mapNestedAttribute": { - "type": "object", - "additionalProperties": { - "$ref": "#/types/testprov:index/R1MapNestedAttribute:R1MapNestedAttribute" - } + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "inputProperties": { - "mapNestedAttribute": { - "type": "object", - "additionalProperties": { - "$ref": "#/types/testprov:index/R1MapNestedAttribute:R1MapNestedAttribute" - } + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "stateInputs": { "description": "Input properties used for looking up and filtering R1 resources.\n", "properties": { - "mapNestedAttribute": { - "type": "object", - "additionalProperties": { - "$ref": "#/types/testprov:index/R1MapNestedAttribute:R1MapNestedAttribute" - } + "blk": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" } }, "type": "object" } }, "types": { - "testprov:index/R1MapNestedAttribute:R1MapNestedAttribute": { + "testprov:index/R1Blk:R1Blk": { "properties": { "a1": { - "type": "string" + "type": "number" } }, "type": "object" } } }`), - }, - //------------------------------------------------------------------------------------------------------ - { - "object-attribute", - &pb.Provider{ - TypeName: "testprov", - AllResources: []pb.Resource{{ - Name: "r1", - ResourceSchema: schema.Schema{ + }) +} + +func TestShimSetNestedBlock(t *testing.T) { + checkShim(t, shimTestCase{ + stdProvider(schema.Schema{ + Blocks: map[string]schema.Block{ + "blk": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "object_attribute": schema.ObjectAttribute{ - Optional: true, - AttributeTypes: map[string]attr.Type{ - "a1": types.StringType, - }, - }, + "a1": schema.Float64Attribute{Optional: true}, }, }, - }}, + }, }, - autogold.Expect(`{ + }), + autogold.Expect(`{ "resources": { "testprov_r1": { - "object_attribute": { + "blk": { "element": { "resource": { "a1": { - "type": 4 + "optional": true, + "type": 3 } } }, "optional": true, - "type": 6 + "type": 7 } } } }`), - autogold.Expect(`{ + autogold.Expect(`{ "resource": { "properties": { - "objectAttribute": { - "$ref": "#/types/testprov:index/R1ObjectAttribute:R1ObjectAttribute" + "blks": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" + } } }, "inputProperties": { - "objectAttribute": { - "$ref": "#/types/testprov:index/R1ObjectAttribute:R1ObjectAttribute" + "blks": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" + } } }, "stateInputs": { "description": "Input properties used for looking up and filtering R1 resources.\n", "properties": { - "objectAttribute": { - "$ref": "#/types/testprov:index/R1ObjectAttribute:R1ObjectAttribute" + "blks": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/R1Blk:R1Blk" + } } }, "type": "object" } }, "types": { - "testprov:index/R1ObjectAttribute:R1ObjectAttribute": { + "testprov:index/R1Blk:R1Blk": { "properties": { "a1": { - "type": "string" + "type": "number" } }, - "type": "object", - "required": [ - "a1" - ] + "type": "object" } } }`), - }, - } + }) +} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - shimmedProvider := schemashim.ShimSchemaOnlyProvider(context.Background(), tc.provider) +type shimTestCase struct { + provider provider.Provider + expect autogold.Value // expected prettified shim.Schema representation + expectSchema autogold.Value // expected corresponding Pulumi Package Schema extract +} - m := tfbridge.MarshalProvider(shimmedProvider) - bytes, err := json.Marshal(m) - require.NoError(t, err) +func checkShim(t *testing.T, tc shimTestCase) { + shimmedProvider := schemashim.ShimSchemaOnlyProvider(context.Background(), tc.provider) - var pretty map[string]any - err = json.Unmarshal(bytes, &pretty) - require.NoError(t, err) + m := tfbridge.MarshalProvider(shimmedProvider) + bytes, err := json.Marshal(m) + require.NoError(t, err) - prettyBytes, err := json.MarshalIndent(pretty, "", " ") - require.NoError(t, err) + var pretty map[string]any + err = json.Unmarshal(bytes, &pretty) + require.NoError(t, err) - tc.expect.Equal(t, string(prettyBytes)) + prettyBytes, err := json.MarshalIndent(pretty, "", " ") + require.NoError(t, err) - rtok := "testprov:index:R1" + tc.expect.Equal(t, string(prettyBytes)) - info := info.Provider{ - Name: "testprov", - P: shimmedProvider, - Resources: map[string]*info.Resource{ - "testprov_r1": { - Tok: tokens.Type(rtok), - }, - }, - } + rtok := "testprov:index:R1" - nilSink := diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{Color: colors.Never}) - pSpec, err := tfgen.GenerateSchema(info, nilSink) - require.NoError(t, err) + info := info.Provider{ + Name: "testprov", + P: shimmedProvider, + Resources: map[string]*info.Resource{ + "testprov_r1": { + Tok: tokens.Type(rtok), + }, + }, + } + + nilSink := diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{Color: colors.Never}) + pSpec, err := tfgen.GenerateSchema(info, nilSink) + require.NoError(t, err) - type miniSpec struct { - Resource any `json:"resource"` - Types any `json:"types"` - } + type miniSpec struct { + Resource any `json:"resource"` + Types any `json:"types"` + } + + ms := miniSpec{ + Resource: pSpec.Resources[rtok], + Types: pSpec.Types, + } - ms := miniSpec{ - Resource: pSpec.Resources[rtok], - Types: pSpec.Types, - } + prettySpec, err := json.MarshalIndent(ms, "", " ") + require.NoError(t, err) - prettySpec, err := json.MarshalIndent(ms, "", " ") - require.NoError(t, err) + tc.expectSchema.Equal(t, string(prettySpec)) +} - tc.expectSchema.Equal(t, string(prettySpec)) - }) +func stdProvider(resourceSchema schema.Schema) *pb.Provider { + return &pb.Provider{ + TypeName: "testprov", + AllResources: []pb.Resource{{ + Name: "r1", + ResourceSchema: resourceSchema, + }}, } } diff --git a/pkg/tfshim/sdk-v2/shim_test.go b/pkg/tfshim/sdk-v2/shim_test.go index b7d132246..b13529f55 100644 --- a/pkg/tfshim/sdk-v2/shim_test.go +++ b/pkg/tfshim/sdk-v2/shim_test.go @@ -16,13 +16,19 @@ package sdkv2 import ( "encoding/json" + "io" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hexops/autogold/v2" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/stretchr/testify/require" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen" ) // Test how various SDKv2-based schemata translate to the shim.Schema layer. @@ -30,7 +36,8 @@ func TestSchemaShimRepresentations(t *testing.T) { type testCase struct { name string resourceSchema map[string]*schema.Schema - expect autogold.Value + expect autogold.Value // expected prettified shim.Schema representation + expectSchema autogold.Value // expected corresponding Pulumi Package Schema extract } testCases := []testCase{ @@ -51,6 +58,30 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "inputProperties": { + "fieldAttr": { + "type": "string" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "type": "object" + } + }, + "types": {} }`), }, { @@ -78,6 +109,39 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "fieldAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "inputProperties": { + "fieldAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "fieldAttrs": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" + } + }, + "types": {} }`), }, { @@ -113,10 +177,52 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "inputProperties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/ResBlockField:ResBlockField": { + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "type": "object" + } + } }`), }, { - "list nested block", + "list block nested", map[string]*schema.Schema{ "block_field": { Type: schema.TypeList, @@ -164,10 +270,63 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "inputProperties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "blockFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/ResBlockField:ResBlockField": { + "properties": { + "nestedFields": { + "type": "array", + "items": { + "$ref": "#/types/testprov:index/ResBlockFieldNestedField:ResBlockFieldNestedField" + } + } + }, + "type": "object" + }, + "testprov:index/ResBlockFieldNestedField:ResBlockFieldNestedField": { + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "type": "object" + } + } }`), }, { - "list attribute max items one", + "list attribute flattened", map[string]*schema.Schema{ "field_attr": { Type: schema.TypeList, @@ -193,10 +352,34 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "inputProperties": { + "fieldAttr": { + "type": "string" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "type": "object" + } + }, + "types": {} }`), }, { - "list block", + "list block flattened", map[string]*schema.Schema{ "block_field": { Type: schema.TypeList, @@ -230,6 +413,39 @@ func TestSchemaShimRepresentations(t *testing.T) { } } } +}`), + autogold.Expect(`{ + "resource": { + "properties": { + "blockField": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + }, + "inputProperties": { + "blockField": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "blockField": { + "$ref": "#/types/testprov:index/ResBlockField:ResBlockField" + } + }, + "type": "object" + } + }, + "types": { + "testprov:index/ResBlockField:ResBlockField": { + "properties": { + "fieldAttr": { + "type": "string" + } + }, + "type": "object" + } + } }`), }, } @@ -258,6 +474,37 @@ func TestSchemaShimRepresentations(t *testing.T) { require.NoError(t, err) tc.expect.Equal(t, string(prettyBytes)) + + rtok := "testprov:index:Res" + + info := info.Provider{ + Name: "testprov", + P: shimmedProvider, + Resources: map[string]*info.Resource{ + "res": { + Tok: tokens.Type(rtok), + }, + }, + } + + nilSink := diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{Color: colors.Never}) + pSpec, err := tfgen.GenerateSchema(info, nilSink) + require.NoError(t, err) + + type miniSpec struct { + Resource any `json:"resource"` + Types any `json:"types"` + } + + ms := miniSpec{ + Resource: pSpec.Resources[rtok], + Types: pSpec.Types, + } + + prettySpec, err := json.MarshalIndent(ms, "", " ") + require.NoError(t, err) + + tc.expectSchema.Equal(t, string(prettySpec)) }) } }