diff --git a/.changes/unreleased/NOTES-20260304-134617.yaml b/.changes/unreleased/NOTES-20260304-134617.yaml new file mode 100644 index 00000000000..20e22f7e4c4 --- /dev/null +++ b/.changes/unreleased/NOTES-20260304-134617.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: 'This release moves the [resource configuration generation](https://developer.hashicorp.com/terraform/language/import/generating-configuration) logic for the `-generate-config-out` flag from +Terraform Core to the SDKv2. There should be no functionality changes for resource configuration generation in this release.' +time: 2026-03-04T13:46:17.250227-05:00 +custom: + Issue: "1559" diff --git a/go.mod b/go.mod index e704b35fe30..f771c6c28d9 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.25.0 github.com/hashicorp/terraform-json v0.27.2 - github.com/hashicorp/terraform-plugin-go v0.30.0 + github.com/hashicorp/terraform-plugin-go v0.30.1-0.20260224203057-7d789423e31f github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index 00f05c01d87..197b5b55d9e 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoK github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-go v0.30.0 h1:VmEiD0n/ewxbvV5VI/bYwNtlSEAXtHaZlSnyUUuQK6k= github.com/hashicorp/terraform-plugin-go v0.30.0/go.mod h1:8d523ORAW8OHgA9e8JKg0ezL3XUO84H0A25o4NY/jRo= +github.com/hashicorp/terraform-plugin-go v0.30.1-0.20260224203057-7d789423e31f h1:Gn9urczv0KsRi0YrjI7kdzHNUT2zQ/HLi5lfiLY6oxY= +github.com/hashicorp/terraform-plugin-go v0.30.1-0.20260224203057-7d789423e31f/go.mod h1:8d523ORAW8OHgA9e8JKg0ezL3XUO84H0A25o4NY/jRo= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index ccd9aa85eb5..634a9982477 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -76,6 +76,7 @@ func (s *GRPCProviderServer) StopContext(ctx context.Context) context.Context { func (s *GRPCProviderServer) serverCapabilities() *tfprotov5.ServerCapabilities { return &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + GenerateResourceConfig: true, } } @@ -1785,6 +1786,109 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } +func (s *GRPCProviderServer) GenerateResourceConfig(ctx context.Context, req *tfprotov5.GenerateResourceConfigRequest) (*tfprotov5.GenerateResourceConfigResponse, error) { + ctx = logging.InitContext(ctx) + + resp := &tfprotov5.GenerateResourceConfigResponse{} + + schemaBlock := s.getResourceSchemaBlock(req.TypeName) + + stateVal, err := msgpack.Unmarshal(req.State.MsgPack, schemaBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + if stateVal.IsNull() { + resp.Diagnostics = []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unexpected Generate Config Request", + Detail: "An unexpected error was encountered when generating resource configuration. The current state was missing.\n\n" + + "This is always a problem with Terraform or terraform-plugin-sdk. Please report this to the provider developer.", + }, + } + + } + + newConfigVal := stateVal + + // Handle top level attributes and defaults + newConfigVal, err = cty.Transform(newConfigVal, func(path cty.Path, val cty.Value) (cty.Value, error) { + if val.IsNull() { + return val, nil + } + + if len(path) == 0 { + return val, nil + } + + ty := val.Type() + null := cty.NullVal(ty) + + // find the attribute or block schema representing the value + attr := schemaBlock.AttributeByPath(path) + block := schemaBlock.BlockByPath(path) + switch { + case attr != nil: + // deprecated attributes + if attr.Deprecated { + return null, nil + } + + // read-only attributes are not written in the configuration + if attr.Computed && !attr.Optional { + return null, nil + } + + // The legacy SDK adds an Optional+Computed "id" attribute to the + // resource schema even if not defined in provider code. + // During validation, however, the presence of an extraneous "id" + // attribute in config will cause an error. + // Remove this attribute so we do not generate an "id" attribute + // where there is a risk that it is not in the real resource schema. + if path.Equals(cty.GetAttrPath("id")) && attr.Computed && attr.Optional { + return null, nil + } + + // If we have "" for an optional value, assume it is actually null + // due to the legacy SDK. + if ty == cty.String { + if !val.IsNull() && attr.Optional && len(val.AsString()) == 0 { + return null, nil + } + } + return val, nil + + case block != nil: + if block.Deprecated { + return null, nil + } + } + + return val, nil + }) + if err != nil { + configErr := fmt.Errorf("An unexpected error occurred while trying to generate resource configuration. "+ + "This is an error in terraform-plugin-sdk used by the provider. "+ + "Please report the following to the provider developers.\n\n"+ + "Original Error: %s", err.Error()) + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, configErr) + return resp, nil + } + + newConfigMP, err := msgpack.Marshal(newConfigVal, schemaBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + resp.Config = &tfprotov5.DynamicValue{ + MsgPack: newConfigMP, + } + + return resp, nil +} + func (s *GRPCProviderServer) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { if req == nil { return nil, fmt.Errorf("MoveResourceState request is nil") diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 71a2f67e29b..6b1e9bb30d8 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3767,6 +3767,7 @@ func TestGRPCProviderServerGetMetadata(t *testing.T) { Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + GenerateResourceConfig: true, }, }, }, @@ -3804,6 +3805,7 @@ func TestGRPCProviderServerGetMetadata(t *testing.T) { }, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + GenerateResourceConfig: true, }, }, }, @@ -3830,6 +3832,7 @@ func TestGRPCProviderServerGetMetadata(t *testing.T) { }, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + GenerateResourceConfig: true, }, }, }, @@ -11342,6 +11345,405 @@ func TestPrepareProviderConfig(t *testing.T) { } } +func TestGenerateResourceConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *GRPCProviderServer + req *tfprotov5.GenerateResourceConfigRequest + expected *tfprotov5.GenerateResourceConfigResponse + ExpectError string + }{ + "null-state": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Computed: true, + Optional: true, + }, + }, + }, + }, + }), + req: &tfprotov5.GenerateResourceConfigRequest{ + TypeName: "test", + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + }), + cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + ), + }, + }, + ExpectError: "Unexpected Generate Config Request", + }, + "simple-resource": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Computed: true, + Optional: true, + }, + "test_computed": { + Type: TypeString, + Computed: true, + }, + "test_optional": { + Type: TypeString, + Optional: true, + }, + "test_required": { + Type: TypeString, + Required: true, + }, + "test_deprecated": { + Type: TypeList, + Optional: true, + Elem: &Schema{ + Type: TypeString, + }, + Deprecated: "deprecated", + }, + "test_false_bool": { + Type: TypeBool, + Optional: true, + }, + "test_empty_string": { + Type: TypeString, + Optional: true, + }, + "test_deprecated_block": { + Type: TypeList, + Optional: true, + Deprecated: "deprecated", + Elem: &Resource{ + Schema: map[string]*Schema{ + "test_nested_block_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + "test_nested_block": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "test_nested_nested_block": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "test_computed": { + Type: TypeString, + Computed: true, + }, + "test_optional": { + Type: TypeString, + Optional: true, + }, + "test_required": { + Type: TypeString, + Required: true, + }, + "test_deprecated": { + Type: TypeList, + Optional: true, + Elem: &Schema{ + Type: TypeString, + }, + Deprecated: "deprecated", + }, + }, + }, + }, + }, + }, + }, + "test_nested_deprecated_block": { + Type: TypeList, + Optional: true, + Elem: &Resource{ + Schema: map[string]*Schema{ + "test_nested_nested_block": { + Type: TypeList, + Optional: true, + Deprecated: "deprecated", + Elem: &Resource{ + Schema: map[string]*Schema{ + "test_nested_nested_block_attr": { + Type: TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + req: &tfprotov5.GenerateResourceConfigRequest{ + TypeName: "test", + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_computed": cty.String, + "test_optional": cty.String, + "test_required": cty.String, + "test_deprecated": cty.List(cty.String), + "test_false_bool": cty.Bool, + "test_empty_string": cty.String, + "test_deprecated_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_block_attr": cty.String, + })), + "test_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_computed": cty.String, + "test_optional": cty.String, + "test_required": cty.String, + "test_deprecated": cty.List(cty.String), + })), + })), + "test_nested_deprecated_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block_attr": cty.String, + })), + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("id-val"), + "test_computed": cty.StringVal("computed-val"), + "test_optional": cty.StringVal("optional-val"), + "test_required": cty.StringVal("required-val"), + "test_deprecated": cty.ListVal([]cty.Value{ + cty.StringVal("hello"), + cty.StringVal("world"), + }), + "test_false_bool": cty.BoolVal(false), + "test_empty_string": cty.StringVal(""), + "test_deprecated_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_block_attr": cty.StringVal("val-a"), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_block_attr": cty.StringVal("val-b"), + }), + }), + "test_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.StringVal("computed-val-a"), + "test_optional": cty.StringVal("optional-val-a"), + "test_required": cty.StringVal("required-val-a"), + "test_deprecated": cty.ListVal([]cty.Value{ + cty.StringVal("hello-a"), + cty.StringVal("world-a"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.StringVal("computed-val-b"), + "test_optional": cty.StringVal("optional-val-b"), + "test_required": cty.StringVal("required-val-b"), + "test_deprecated": cty.ListVal([]cty.Value{ + cty.StringVal("hello-b"), + cty.StringVal("world-b"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.StringVal("computed-val-a"), + "test_optional": cty.StringVal("optional-val-a"), + "test_required": cty.StringVal("required-val-a"), + "test_deprecated": cty.ListVal([]cty.Value{ + cty.StringVal("hello-a"), + cty.StringVal("world-a"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.StringVal("computed-val-b"), + "test_optional": cty.StringVal("optional-val-b"), + "test_required": cty.StringVal("required-val-b"), + "test_deprecated": cty.ListVal([]cty.Value{ + cty.StringVal("hello-b"), + cty.StringVal("world-b"), + }), + }), + }), + }), + }), + "test_nested_deprecated_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block_attr": cty.StringVal("val-a"), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block_attr": cty.StringVal("val-b"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block_attr": cty.StringVal("val-a"), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block_attr": cty.StringVal("val-b"), + }), + }), + }), + }), + }), + ), + }, + }, + expected: &tfprotov5.GenerateResourceConfigResponse{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_computed": cty.String, + "test_optional": cty.String, + "test_required": cty.String, + "test_deprecated": cty.List(cty.String), + "test_false_bool": cty.Bool, + "test_empty_string": cty.String, + "test_deprecated_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_block_attr": cty.String, + })), + "test_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_computed": cty.String, + "test_optional": cty.String, + "test_required": cty.String, + "test_deprecated": cty.List(cty.String), + })), + })), + "test_nested_deprecated_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block": cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block_attr": cty.String, + })), + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test_computed": cty.NullVal(cty.String), + "test_optional": cty.StringVal("optional-val"), + "test_required": cty.StringVal("required-val"), + "test_deprecated": cty.NullVal(cty.List(cty.String)), + "test_false_bool": cty.BoolVal(false), + "test_empty_string": cty.NullVal(cty.String), + "test_deprecated_block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "test_nested_block_attr": cty.String, + }))), + "test_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.NullVal(cty.String), + "test_optional": cty.StringVal("optional-val-a"), + "test_required": cty.StringVal("required-val-a"), + "test_deprecated": cty.NullVal(cty.List(cty.String)), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.NullVal(cty.String), + "test_optional": cty.StringVal("optional-val-b"), + "test_required": cty.StringVal("required-val-b"), + "test_deprecated": cty.NullVal(cty.List(cty.String)), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.NullVal(cty.String), + "test_optional": cty.StringVal("optional-val-a"), + "test_required": cty.StringVal("required-val-a"), + "test_deprecated": cty.NullVal(cty.List(cty.String)), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_computed": cty.NullVal(cty.String), + "test_optional": cty.StringVal("optional-val-b"), + "test_required": cty.StringVal("required-val-b"), + "test_deprecated": cty.NullVal(cty.List(cty.String)), + }), + }), + }), + }), + "test_nested_deprecated_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block_attr": cty.String, + }))), + }), + cty.ObjectVal(map[string]cty.Value{ + "test_nested_nested_block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "test_nested_nested_block_attr": cty.String, + }))), + }), + }), + }), + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + resp, err := testCase.server.GenerateResourceConfig(context.Background(), testCase.req) + + if err != nil { + t.Fatal(err) + } + + if testCase.ExpectError != "" && len(resp.Diagnostics) > 0 { + for _, d := range resp.Diagnostics { + if !strings.Contains(d.Summary, testCase.ExpectError) { + t.Fatalf("Unexpected error: %s/%s", d.Summary, d.Detail) + } + } + return + } + + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() + + if resp != nil && resp.Config != nil { + t.Logf("resp.Config.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.Config.MsgPack)) + } + + if testCase.expected != nil && testCase.expected.Config != nil { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.Config.MsgPack)) + } + + t.Error(diff) + } + }) + } +} + func TestGetSchemaTimeouts(t *testing.T) { r := &Resource{ SchemaVersion: 4, diff --git a/internal/configs/configschema/path.go b/internal/configs/configschema/path.go new file mode 100644 index 00000000000..c0e1d03dfa1 --- /dev/null +++ b/internal/configs/configschema/path.go @@ -0,0 +1,54 @@ +// Copyright IBM Corp. 2019, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package configschema + +import ( + "github.com/hashicorp/go-cty/cty" +) + +// AttributeByPath looks up the Attribute schema which corresponds to the given +// cty.Path. A nil value is returned if the given path does not correspond to a +// specific attribute. +func (b *Block) AttributeByPath(path cty.Path) *Attribute { + block := b + for i, step := range path { + switch step := step.(type) { + case cty.GetAttrStep: + if attr := block.Attributes[step.Name]; attr != nil { + if i < len(path)-1 { // There's more to the path, but not more to this Attribute. + return nil + } + return attr + } + + if nestedBlock := block.BlockTypes[step.Name]; nestedBlock != nil { + block = &nestedBlock.Block + continue + } + + return nil + } + } + return nil +} + +// BlockByPath looks up the Block schema which corresponds to the given +// cty.Path. A nil value is returned if the given path does not correspond to a +// specific block. +func (b *Block) BlockByPath(path cty.Path) *Block { + for i, step := range path { + switch step := step.(type) { + case cty.GetAttrStep: + if blockType := b.BlockTypes[step.Name]; blockType != nil { + if len(blockType.Block.BlockTypes) > 0 && i < len(path)-1 { + return blockType.Block.BlockByPath(path[i+1:]) + } else if i < len(path)-1 { + return nil + } + return &blockType.Block + } + } + } + return nil +} diff --git a/internal/configs/configschema/path_test.go b/internal/configs/configschema/path_test.go new file mode 100644 index 00000000000..805ed2cb1c9 --- /dev/null +++ b/internal/configs/configschema/path_test.go @@ -0,0 +1,214 @@ +// Copyright IBM Corp. 2019, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package configschema + +import ( + "fmt" + "testing" + + "github.com/hashicorp/go-cty/cty" +) + +func TestAttributeByPath(t *testing.T) { + schema := &Block{ + Attributes: map[string]*Attribute{ + "a1": {Description: "a1"}, + "a2": {Description: "a2"}, + }, + BlockTypes: map[string]*NestedBlock{ + "b1": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "a3": {Description: "a3"}, + "a4": {Description: "a4"}, + }, + BlockTypes: map[string]*NestedBlock{ + "b2": { + Nesting: NestingMap, + Block: Block{ + Attributes: map[string]*Attribute{ + "a5": {Description: "a5"}, + "a6": {Description: "a6"}, + }, + }, + }, + }, + }, + }, + "b3": { + Nesting: NestingMap, + Block: Block{ + Attributes: map[string]*Attribute{ + "a7": {Description: "a7"}, + "a8": {Description: "a8"}, + }, + BlockTypes: map[string]*NestedBlock{ + "b4": { + Nesting: NestingSet, + Block: Block{ + Attributes: map[string]*Attribute{ + "a9": {Description: "a9"}, + "a10": {Description: "a10"}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range []struct { + path cty.Path + attrDescription string + exists bool + }{ + { + cty.GetAttrPath("a2"), + "a2", + true, + }, + { + cty.GetAttrPath("a3").IndexInt(1).GetAttr("b2").IndexString("foo").GetAttr("no"), + "missing", + false, + }, + { + cty.GetAttrPath("b1"), + "block", + false, + }, + { + cty.GetAttrPath("b1").IndexInt(1).GetAttr("a3"), + "a3", + true, + }, + { + cty.GetAttrPath("b1").IndexInt(1).GetAttr("b2").IndexString("foo").GetAttr("a7"), + "missing", + false, + }, + { + cty.GetAttrPath("b1").IndexInt(1).GetAttr("b2").IndexString("foo").GetAttr("a6"), + "a6", + true, + }, + { + cty.GetAttrPath("b3").IndexString("foo").GetAttr("b2").IndexString("foo").GetAttr("a7"), + "missing_block", + false, + }, + { + cty.GetAttrPath("b3").IndexString("foo").GetAttr("a7"), + "a7", + true, + }, + { + // Index steps don't apply to the schema, so the set Index value doesn't matter. + cty.GetAttrPath("b3").IndexString("foo").GetAttr("b4").Index(cty.EmptyObjectVal).GetAttr("a9"), + "a9", + true, + }, + } { + t.Run(tc.attrDescription, func(t *testing.T) { + attr := schema.AttributeByPath(tc.path) + if !tc.exists && attr == nil { + return + } + + if attr == nil { + t.Fatalf("missing attribute from path %#v\n", tc.path) + } + + if attr.Description != tc.attrDescription { + t.Fatalf("expected Attribute for %q, got %#v\n", tc.attrDescription, attr) + } + }) + } +} + +func TestBlockByPath(t *testing.T) { + schema := &Block{ + BlockTypes: map[string]*NestedBlock{ + "b1": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "a3": {Description: "a3"}, + "a4": {Description: "a4"}, + }, + BlockTypes: map[string]*NestedBlock{ + "b2": { + Nesting: NestingMap, + Block: Block{ + Attributes: map[string]*Attribute{ + "a5": {Description: "a5"}, + "a6": {Description: "a6"}, + }, + }, + }, + }, + }, + }, + "b3": { + Nesting: NestingMap, + Block: Block{ + Attributes: map[string]*Attribute{ + "a7": {Description: "a7"}, + "a8": {Description: "a8"}, + }, + BlockTypes: map[string]*NestedBlock{ + "b4": { + Nesting: NestingSet, + Block: Block{ + Attributes: map[string]*Attribute{ + "a9": {Description: "a9"}, + "a10": {Description: "a10"}, + }, + }, + }, + }, + }, + }, + }, + } + + for i, tc := range []struct { + path cty.Path + exists bool + }{ + { + cty.GetAttrPath("b1").IndexInt(1).GetAttr("b2"), + true, + }, + { + cty.GetAttrPath("b1"), + true, + }, + { + cty.GetAttrPath("b2"), + false, + }, + { + cty.GetAttrPath("b3").IndexString("foo").GetAttr("b2"), + false, + }, + { + cty.GetAttrPath("b3").IndexString("foo").GetAttr("b4"), + true, + }, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + block := schema.BlockByPath(tc.path) + if !tc.exists && block == nil { + return + } + + if block == nil { + t.Fatalf("missing block from path %#v\n", tc.path) + } + }) + } +}