From 09b2909371abca673b973bd5c5782664e5439738 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Wed, 29 Sep 2021 17:17:12 -0400 Subject: [PATCH] tfsdk: Support for List and Set Blocks Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/85 This implementation is analogous to the existing `Attributes` field on `Attribute`. While the framework handles the major differences at the protocol layer during conversion, it also must enforce the constraints of the underlying type system. Some notable features include: * Defining schemas is very similar to `Attributes`. * Accessing and writing `Config`, `Plan`, and `State` data is no different than `Attributes` with the same nesting mode. * Blocks are structural by Terraform's and cty's definition, meaning there is no concept of `Computed`, `Optional`, `Required`, or `Sensitive`. Checks are in place to enforce these constraints. The primary purpose for supporting blocks is to allow previously existing schemas defined by the older Terraform Plugin SDK to not require practitioner breaking changes upon migrating to the framework (except the protocol version and therefore the minimum Terraform CLI version). This also allows this framework to be muxed with the older framework since the provider schema must match. It is expected that over time any schema definitions including `Blocks` will migrate to `Attributes`. Provider developers should always opt for `Attributes` in new schema definitions. --- tfsdk/attribute.go | 589 +++++++++++++++----- tfsdk/attribute_test.go | 912 +++++++++++++++++++++++++++++- tfsdk/config_test.go | 200 +++++++ tfsdk/nested_attributes.go | 11 +- tfsdk/nested_blocks.go | 189 +++++++ tfsdk/plan_test.go | 200 +++++++ tfsdk/schema.go | 107 +++- tfsdk/schema_test.go | 399 ++++++++++++++ tfsdk/serve_provider_test.go | 74 +++ tfsdk/serve_resource_two_test.go | 48 ++ tfsdk/serve_test.go | 914 +++++++++++++++++++++++++++++++ tfsdk/state_test.go | 260 ++++++++- 12 files changed, 3749 insertions(+), 154 deletions(-) create mode 100644 tfsdk/nested_blocks.go diff --git a/tfsdk/attribute.go b/tfsdk/attribute.go index 6a7f64be9..af068a3b9 100644 --- a/tfsdk/attribute.go +++ b/tfsdk/attribute.go @@ -19,16 +19,42 @@ type Attribute struct { // Type indicates what kind of attribute this is. You'll most likely // want to use one of the types in the types package. // - // If Type is set, Attributes cannot be. + // If Type is set, Attributes and Blocks cannot be. Type attr.Type // Attributes can have their own, nested attributes. This nested map of // attributes behaves exactly like the map of attributes on the Schema // type. // - // If Attributes is set, Type cannot be. + // In practitioner configurations, an equals sign (=) is required to set + // the value. See also: + // https://www.terraform.io/docs/language/syntax/configuration.html + // + // If Attributes is set, Blocks and Type cannot be. Attributes are strongly + // preferred over Blocks. Attributes NestedAttributes + // Blocks can have their own, nested attributes. This nested map of + // attributes behaves exactly like the map of attributes on the Schema + // type. + // + // Blocks are by definition, structural, meaning they are implicitly + // required in values. + // + // In practitioner configurations, an equals sign (=) cannot be used to + // set the value. Blocks are instead repeated as necessary, or require + // the use of dynamic block expressions. See also: + // https://www.terraform.io/docs/language/syntax/configuration.html + // https://www.terraform.io/docs/language/expressions/dynamic-blocks.html + // + // If Blocks is set, Attributes, Computed, Optional, Required, Sensitive, + // and Type cannot be. Attributes are strongly preferred over Blocks. + // Blocks should only be used for configuration compatibility with + // previously existing schemas from an older Terraform Plugin SDK. Efforts + // should be made to convert Blocks to Attributes as a breaking change for + // practitioners. + Blocks NestedBlocks + // Description is used in various tooling, like the language server, to // give practitioners more information about what this attribute is, // what it's for, and how it should be used. It should be written as @@ -44,11 +70,18 @@ type Attribute struct { // Required indicates whether the practitioner must enter a value for // this attribute or not. Required and Optional cannot both be true, // and Required and Computed cannot both be true. + // + // Cannot be set with Blocks. Only nested attributes under Blocks may set + // configurability. Use ListNestedBlockOptions with MinItems above 0 to + // mark enforce Block requirement in a practitioner configuration. Required bool // Optional indicates whether the practitioner can choose not to enter // a value for this attribute or not. Optional and Required cannot both // be true. + // + // Cannot be set with Blocks. Only nested attributes under Blocks may set + // configurability. Optional bool // Computed indicates whether the provider may return its own value for @@ -56,6 +89,9 @@ type Attribute struct { // Required and Optional are both false, Computed must be true, and the // attribute will be considered "read only" for the practitioner, with // only the provider able to set its value. + // + // Cannot be set with Blocks. Only nested attributes under Blocks may set + // configurability. Computed bool // Sensitive indicates whether the value of this attribute should be @@ -63,6 +99,9 @@ type Attribute struct { // in CLI output. Sensitive does not impact how values are stored, and // practitioners are encouraged to store their state as if the entire // file is sensitive. + // + // Cannot be set with Blocks. Only nested attributes under Blocks may set + // sensitivity. Sensitive bool // DeprecationMessage defines a message to display to practitioners @@ -88,9 +127,9 @@ type Attribute struct { } // ApplyTerraform5AttributePathStep transparently calls -// ApplyTerraform5AttributePathStep on a.Type or a.Attributes, whichever is -// non-nil. It allows Attributes to be walked using tftypes.Walk and -// tftypes.Transform. +// ApplyTerraform5AttributePathStep on a.Type, a.Attributes or a.Blocks, +// whichever is non-nil. It allows Attributes to be walked using tftypes.Walk +// and tftypes.Transform. func (a Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { if a.Type != nil { return a.Type.ApplyTerraform5AttributePathStep(step) @@ -98,7 +137,10 @@ func (a Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathSt if a.Attributes != nil { return a.Attributes.ApplyTerraform5AttributePathStep(step) } - return nil, errors.New("Attribute has no type or nested attributes") + if a.Blocks != nil { + return a.Blocks.ApplyTerraform5AttributePathStep(step) + } + return nil, errors.New("Attribute has no type, nested attributes, or nested blocks") } // Equal returns true if `a` and `o` should be considered Equal. @@ -117,6 +159,13 @@ func (a Attribute) Equal(o Attribute) bool { } else if a.Attributes != nil && o.Attributes != nil && !a.Attributes.Equal(o.Attributes) { return false } + if a.Blocks == nil && o.Blocks != nil { + return false + } else if a.Blocks != nil && o.Blocks == nil { + return false + } else if a.Blocks != nil && o.Blocks != nil && !a.Blocks.Equal(o.Blocks) { + return false + } if a.Description != o.Description { return false } @@ -141,10 +190,56 @@ func (a Attribute) Equal(o Attribute) bool { return true } -// tfprotov6 returns the *tfprotov6.SchemaAttribute equivalent of an +// definesAttributes returns true if Attribute has a non-empty Blocks definition. +// +// Attribute may also incorrectly have an Attributes and/or Type definition. +func (a Attribute) definesAttributes() bool { + return a.Attributes != nil && len(a.Attributes.GetAttributes()) > 0 +} + +// definesBlocks returns true if Attribute has a non-empty Blocks definition. +// +// Attribute may also incorrectly have an Attributes and/or Type definition. +func (a Attribute) definesBlocks() bool { + return a.Blocks != nil && len(a.Blocks.GetAttributes()) > 0 +} + +// tfprotov6 returns the *tfprotov6.SchemaAttribute or +// *tfprotov6.SchemaNestedBlock equivalent of an +// Attribute. Errors will be tftypes.AttributePathErrors based on +// `path`. `name` is the name of the attribute. +func (a Attribute) tfprotov6(ctx context.Context, name string, path *tftypes.AttributePath) (interface{}, error) { + if !a.definesAttributes() && !a.definesBlocks() && a.Type == nil { + return nil, path.NewErrorf("must have Attributes, Blocks, or Type set") + } + + if a.definesBlocks() { + return a.tfprotov6SchemaNestedBlock(ctx, name, path) + } + + return a.tfprotov6SchemaAttribute(ctx, name, path) +} + +// tfprotov6SchemaAttribute returns the *tfprotov6.SchemaAttribute equivalent of an // Attribute. Errors will be tftypes.AttributePathErrors based on // `path`. `name` is the name of the attribute. func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath) (*tfprotov6.SchemaAttribute, error) { + if a.definesAttributes() && a.definesBlocks() { + return nil, path.NewErrorf("cannot have both Attributes and Blocks set") + } + + if a.definesAttributes() && a.Type != nil { + return nil, path.NewErrorf("cannot have both Attributes and Type set") + } + + if !a.definesAttributes() && a.Type == nil { + return nil, path.NewErrorf("must have Attributes or Type set") + } + + if !a.Required && !a.Optional && !a.Computed { + return nil, path.NewErrorf("must have Required, Optional, or Computed set") + } + schemaAttribute := &tfprotov6.SchemaAttribute{ Name: name, Required: a.Required, @@ -153,10 +248,6 @@ func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, pa Sensitive: a.Sensitive, } - if !a.Required && !a.Optional && !a.Computed { - return nil, path.NewErrorf("must have Required, Optional, or Computed set") - } - if a.DeprecationMessage != "" { schemaAttribute.Deprecated = true } @@ -171,14 +262,6 @@ func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, pa schemaAttribute.DescriptionKind = tfprotov6.StringKindMarkdown } - if a.Attributes != nil && len(a.Attributes.GetAttributes()) > 0 && a.Type != nil { - return nil, path.NewErrorf("can't have both Attributes and Type set") - } - - if (a.Attributes == nil || len(a.Attributes.GetAttributes()) < 1) && a.Type == nil { - return nil, path.NewErrorf("must have Attributes or Type set") - } - if a.Type != nil { schemaAttribute.Type = a.Type.TerraformType(ctx) @@ -204,13 +287,21 @@ func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, pa } for nestedName, nestedA := range a.Attributes.GetAttributes() { - nestedSchemaAttribute, err := nestedA.tfprotov6SchemaAttribute(ctx, nestedName, path.WithAttributeName(nestedName)) + nestedPath := path.WithAttributeName(nestedName) + nestedAProto6Raw, err := nestedA.tfprotov6(ctx, nestedName, nestedPath) if err != nil { return nil, err } - object.Attributes = append(object.Attributes, nestedSchemaAttribute) + switch nestedAProto6 := nestedAProto6Raw.(type) { + case *tfprotov6.SchemaAttribute: + object.Attributes = append(object.Attributes, nestedAProto6) + case *tfprotov6.SchemaNestedBlock: + return nil, nestedPath.NewErrorf("cannot have Blocks inside Attributes") + default: + return nil, nestedPath.NewErrorf("unknown tfprotov6 type %T in Attributes", nestedAProto6Raw) + } } sort.Slice(object.Attributes, func(i, j int) bool { @@ -230,19 +321,134 @@ func (a Attribute) tfprotov6SchemaAttribute(ctx context.Context, name string, pa return schemaAttribute, nil } +// tfprotov6SchemaNestedBlock returns the *tfprotov6.SchemaNestedBlock +// equivalent of an Attribute. Errors will be tftypes.AttributePathErrors based +// on `path`. `name` is the name of the attribute. +func (a Attribute) tfprotov6SchemaNestedBlock(ctx context.Context, name string, path *tftypes.AttributePath) (*tfprotov6.SchemaNestedBlock, error) { + if a.definesAttributes() { + return nil, path.NewErrorf("cannot have both Attributes and Blocks set") + } + + if a.Computed { + return nil, path.NewErrorf("cannot set Block as Computed, mark all nested Attributes instead") + } + + if a.Optional { + return nil, path.NewErrorf("cannot set Block as Optional, mark all nested Attributes instead") + } + + if a.Required { + return nil, path.NewErrorf("cannot set Block as Required, mark all nested Attributes instead") + } + + if a.Sensitive { + return nil, path.NewErrorf("cannot set Block as Sensitive, mark all nested Attributes instead") + } + + if a.Type != nil { + return nil, path.NewErrorf("cannot have both Blocks and Type set") + } + + schemaNestedBlock := &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Deprecated: a.DeprecationMessage != "", + }, + MinItems: a.Blocks.GetMinItems(), + MaxItems: a.Blocks.GetMaxItems(), + TypeName: name, + } + + if a.Description != "" { + schemaNestedBlock.Block.Description = a.Description + schemaNestedBlock.Block.DescriptionKind = tfprotov6.StringKindPlain + } + + if a.MarkdownDescription != "" { + schemaNestedBlock.Block.Description = a.MarkdownDescription + schemaNestedBlock.Block.DescriptionKind = tfprotov6.StringKindMarkdown + } + + nm := a.Blocks.GetNestingMode() + switch nm { + case NestingModeList: + schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeList + case NestingModeSet: + schemaNestedBlock.Nesting = tfprotov6.SchemaNestedBlockNestingModeSet + case NestingModeMap, NestingModeSingle: + // This is intentional to only maintain the previous Terraform Plugin SDK support. + return nil, path.NewErrorf("unsupported Blocks nesting mode: %v", nm) + default: + return nil, path.NewErrorf("unrecognized nesting mode %v", nm) + } + + for nestedName, nestedA := range a.Blocks.GetAttributes() { + nestedPath := path.WithAttributeName(nestedName) + nestedAProto6Raw, err := nestedA.tfprotov6(ctx, nestedName, nestedPath) + + if err != nil { + return nil, err + } + + switch nestedAProto6 := nestedAProto6Raw.(type) { + case *tfprotov6.SchemaAttribute: + schemaNestedBlock.Block.Attributes = append(schemaNestedBlock.Block.Attributes, nestedAProto6) + case *tfprotov6.SchemaNestedBlock: + schemaNestedBlock.Block.BlockTypes = append(schemaNestedBlock.Block.BlockTypes, nestedAProto6) + default: + return nil, nestedPath.NewErrorf("unknown tfprotov6 type %T in Blocks", nestedAProto6Raw) + } + } + + sort.Slice(schemaNestedBlock.Block.Attributes, func(i, j int) bool { + if schemaNestedBlock.Block.Attributes[i] == nil { + return true + } + + if schemaNestedBlock.Block.Attributes[j] == nil { + return false + } + + return schemaNestedBlock.Block.Attributes[i].Name < schemaNestedBlock.Block.Attributes[j].Name + }) + + sort.Slice(schemaNestedBlock.Block.BlockTypes, func(i, j int) bool { + if schemaNestedBlock.Block.BlockTypes[i] == nil { + return true + } + + if schemaNestedBlock.Block.BlockTypes[j] == nil { + return false + } + + return schemaNestedBlock.Block.BlockTypes[i].TypeName < schemaNestedBlock.Block.BlockTypes[j].TypeName + }) + + return schemaNestedBlock, nil +} + // validate performs all Attribute validation. func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { - if (a.Attributes == nil || len(a.Attributes.GetAttributes()) == 0) && a.Type == nil { + if !a.definesAttributes() && !a.definesBlocks() && a.Type == nil { resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Attribute Definition", - "Attribute must define either Attributes or Type. This is always a problem with the provider and should be reported to the provider developer.", + "Attribute must define either Attributes, Blocks, or Type. This is always a problem with the provider and should be reported to the provider developer.", ) return } - if a.Attributes != nil && len(a.Attributes.GetAttributes()) > 0 && a.Type != nil { + if a.definesAttributes() && a.definesBlocks() { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "Attribute cannot define both Attributes and Blocks. This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + + if a.definesAttributes() && a.Type != nil { resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Attribute Definition", @@ -252,7 +458,17 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r return } - if !a.Required && !a.Optional && !a.Computed { + if a.definesBlocks() && a.Type != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Invalid Attribute Definition", + "Attribute cannot define both Blocks and Type. This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + + if !a.definesBlocks() && !a.Required && !a.Optional && !a.Computed { resp.Diagnostics.AddAttributeError( req.AttributePath, "Invalid Attribute Definition", @@ -276,142 +492,147 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r validator.Validate(ctx, req, resp) } - if a.Attributes != nil { - nm := a.Attributes.GetNestingMode() - switch nm { - case NestingModeList: - l, ok := req.AttributeConfig.(types.List) - - if !ok { - err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) - resp.Diagnostics.AddAttributeError( - req.AttributePath, - "Attribute Validation Error", - "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), - ) + a.validateAttributes(ctx, req, resp) + a.validateBlocks(ctx, req, resp) - return - } + if a.DeprecationMessage != "" && attributeConfig != nil { + tfValue, err := attributeConfig.ToTerraformValue(ctx) - for idx := range l.Elems { - for nestedName, nestedAttr := range a.Attributes.GetAttributes() { - nestedAttrReq := ValidateAttributeRequest{ - AttributePath: req.AttributePath.WithElementKeyInt(idx).WithAttributeName(nestedName), - Config: req.Config, - } - nestedAttrResp := &ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + if err != nil { + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot convert value. Report this to the provider developer:\n\n"+err.Error(), + ) - nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + return + } - resp.Diagnostics = nestedAttrResp.Diagnostics - } - } - case NestingModeSet: - s, ok := req.AttributeConfig.(types.Set) + if tfValue != nil { + resp.Diagnostics.AddAttributeWarning( + req.AttributePath, + "Attribute Deprecated", + a.DeprecationMessage, + ) + } + } +} - if !ok { - err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) - resp.Diagnostics.AddAttributeError( - req.AttributePath, - "Attribute Validation Error", - "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), - ) +// validateAttributes performs all nested Attributes validation. +func (a Attribute) validateAttributes(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + if !a.definesAttributes() { + return + } - return - } + nm := a.Attributes.GetNestingMode() + switch nm { + case NestingModeList: + l, ok := req.AttributeConfig.(types.List) - for _, value := range s.Elems { - tfValueRaw, err := value.ToTerraformValue(ctx) + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) - if err != nil { - err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) - resp.Diagnostics.AddAttributeError( - req.AttributePath, - "Attribute Validation Error", - "Attribute validation cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), - ) + return + } - return + for idx := range l.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyInt(idx).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, } - tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) - for nestedName, nestedAttr := range a.Attributes.GetAttributes() { - nestedAttrReq := ValidateAttributeRequest{ - AttributePath: req.AttributePath.WithElementKeyValue(tfValue).WithAttributeName(nestedName), - Config: req.Config, - } - nestedAttrResp := &ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSet: + s, ok := req.AttributeConfig.(types.Set) - nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) - resp.Diagnostics = nestedAttrResp.Diagnostics - } - } - case NestingModeMap: - m, ok := req.AttributeConfig.(types.Map) + return + } + + for _, value := range s.Elems { + tfValueRaw, err := value.ToTerraformValue(ctx) - if !ok { - err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + if err != nil { + err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) resp.Diagnostics.AddAttributeError( req.AttributePath, "Attribute Validation Error", - "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + "Attribute validation cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), ) return } - for key := range m.Elems { - for nestedName, nestedAttr := range a.Attributes.GetAttributes() { - nestedAttrReq := ValidateAttributeRequest{ - AttributePath: req.AttributePath.WithElementKeyString(key).WithAttributeName(nestedName), - Config: req.Config, - } - nestedAttrResp := &ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) - nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) - - resp.Diagnostics = nestedAttrResp.Diagnostics + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyValue(tfValue).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, } - } - case NestingModeSingle: - o, ok := req.AttributeConfig.(types.Object) - if !ok { - err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) - resp.Diagnostics.AddAttributeError( - req.AttributePath, - "Attribute Validation Error", - "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), - ) + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) - return + resp.Diagnostics = nestedAttrResp.Diagnostics } + } + case NestingModeMap: + m, ok := req.AttributeConfig.(types.Map) - if !o.Null && !o.Unknown { - for nestedName, nestedAttr := range a.Attributes.GetAttributes() { - nestedAttrReq := ValidateAttributeRequest{ - AttributePath: req.AttributePath.WithAttributeName(nestedName), - Config: req.Config, - } - nestedAttrResp := &ValidateAttributeResponse{ - Diagnostics: resp.Diagnostics, - } + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) - nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + return + } - resp.Diagnostics = nestedAttrResp.Diagnostics + for key := range m.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyString(key).WithAttributeName(nestedName), + Config: req.Config, } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics } - default: - err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + } + case NestingModeSingle: + o, ok := req.AttributeConfig.(types.Object) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( req.AttributePath, "Attribute Validation Error", @@ -420,28 +641,124 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r return } + + if !o.Null && !o.Unknown { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + default: + err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } +} + +// validateBlocks performs all nested Blocks validation. +func (a Attribute) validateBlocks(ctx context.Context, req ValidateAttributeRequest, resp *ValidateAttributeResponse) { + if !a.definesBlocks() { + return } - if a.DeprecationMessage != "" && attributeConfig != nil { - tfValue, err := attributeConfig.ToTerraformValue(ctx) + nm := a.Blocks.GetNestingMode() + switch nm { + case NestingModeList: + l, ok := req.AttributeConfig.(types.List) - if err != nil { + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( req.AttributePath, "Attribute Validation Error", - "Attribute validation cannot convert value. Report this to the provider developer:\n\n"+err.Error(), + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), ) return } - if tfValue != nil { - resp.Diagnostics.AddAttributeWarning( + for idx := range l.Elems { + for nestedName, nestedAttr := range a.Blocks.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyInt(idx).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSet: + s, ok := req.AttributeConfig.(types.Set) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributeConfig, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( req.AttributePath, - "Attribute Deprecated", - a.DeprecationMessage, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), ) + + return } + + for _, value := range s.Elems { + tfValueRaw, err := value.ToTerraformValue(ctx) + + if err != nil { + err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) + + for nestedName, nestedAttr := range a.Blocks.GetAttributes() { + nestedAttrReq := ValidateAttributeRequest{ + AttributePath: req.AttributePath.WithElementKeyValue(tfValue).WithAttributeName(nestedName), + Config: req.Config, + } + nestedAttrResp := &ValidateAttributeResponse{ + Diagnostics: resp.Diagnostics, + } + + nestedAttr.validate(ctx, nestedAttrReq, nestedAttrResp) + + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + default: + err := fmt.Errorf("unknown attribute validation nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) + resp.Diagnostics.AddAttributeError( + req.AttributePath, + "Attribute Validation Error", + "Attribute validation cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return } } diff --git a/tfsdk/attribute_test.go b/tfsdk/attribute_test.go index c22ee81a5..16a459109 100644 --- a/tfsdk/attribute_test.go +++ b/tfsdk/attribute_test.go @@ -13,6 +13,121 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +func TestAttributeTfprotov6(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr Attribute + path *tftypes.AttributePath + expected interface{} + expectedErr string + } + + tests := map[string]testCase{ + "empty": { + name: "test", + attr: Attribute{}, + path: tftypes.NewAttributePath(), + expectedErr: "must have Attributes, Blocks, or Type set", + }, + "attributes": { + name: "test", + attr: Attribute{ + Attributes: ListNestedAttributes(map[string]Attribute{ + "sub_test": { + Optional: true, + Type: types.StringType, + }, + }, ListNestedAttributesOptions{}), + Optional: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaAttribute{ + Name: "test", + NestedType: &tfprotov6.SchemaObject{ + Nesting: tfprotov6.SchemaObjectNestingModeList, + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Optional: true, + }, + }, + "blocks": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Optional: true, + Type: types.StringType, + }, + }, ListNestedBlocksOptions{}), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "type": { + name: "test", + attr: Attribute{ + Optional: true, + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaAttribute{ + Name: "test", + Optional: true, + Type: tftypes.String, + }, + }, + } + + for name, tc := range tests { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.attr.tfprotov6(context.Background(), tc.name, tc.path) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} + func TestAttributeTfprotov6SchemaAttribute(t *testing.T) { t.Parallel() @@ -635,7 +750,7 @@ func TestAttributeTfprotov6SchemaAttribute(t *testing.T) { Optional: true, }, path: tftypes.NewAttributePath(), - expectedErr: "can't have both Attributes and Type set", + expectedErr: "cannot have both Attributes and Type set", }, "attr-and-nested-attr-unset": { name: "whoops", @@ -694,6 +809,473 @@ func TestAttributeTfprotov6SchemaAttribute(t *testing.T) { } } +func TestAttributeTfprotov6SchemaNestedBlock(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr Attribute + path *tftypes.AttributePath + expected *tfprotov6.SchemaNestedBlock + expectedErr string + } + + tests := map[string]testCase{ + "attributes": { + name: "test", + attr: Attribute{ + Attributes: SingleNestedAttributes(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }), + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot have both Attributes and Blocks set", + }, + "blocks-listnestedblocks": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "blocks-listnestedblocks-max": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{ + MaxItems: 10, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MaxItems: 10, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "blocks-listnestedblocks-maxmin": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{ + MaxItems: 10, + MinItems: 1, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MaxItems: 10, + MinItems: 1, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "blocks-listnestedblocks-min": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{ + MinItems: 10, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MinItems: 10, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "blocks-setnestedblocks": { + name: "test", + attr: Attribute{ + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, SetNestedBlocksOptions{}), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test", + }, + }, + "blocks-setnestedblocks-max": { + name: "test", + attr: Attribute{ + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, SetNestedBlocksOptions{ + MaxItems: 10, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MaxItems: 10, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test", + }, + }, + "blocks-setnestedblocks-maxmin": { + name: "test", + attr: Attribute{ + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, SetNestedBlocksOptions{ + MaxItems: 10, + MinItems: 1, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MaxItems: 10, + MinItems: 1, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test", + }, + }, + "blocks-setnestedblocks-min": { + name: "test", + attr: Attribute{ + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, SetNestedBlocksOptions{ + MinItems: 10, + }), + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + }, + MinItems: 10, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "test", + }, + }, + "computed": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Computed: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot set Block as Computed, mark all nested Attributes instead", + }, + "deprecated": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + DeprecationMessage: "deprecated, use something else instead", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + Deprecated: true, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "description-plain": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Description: "test description", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + Description: "test description", + DescriptionKind: tfprotov6.StringKindPlain, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "description-markdown": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + MarkdownDescription: "test description", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + Description: "test description", + DescriptionKind: tfprotov6.StringKindMarkdown, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "description-both": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Description: "test plain description", + MarkdownDescription: "test markdown description", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "sub_test", + Optional: true, + Type: tftypes.String, + }, + }, + Description: "test markdown description", + DescriptionKind: tfprotov6.StringKindMarkdown, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test", + }, + }, + "optional": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot set Block as Optional, mark all nested Attributes instead", + }, + "required": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Required: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot set Block as Required, mark all nested Attributes instead", + }, + "sensitive": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Sensitive: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot set Block as Sensitive, mark all nested Attributes instead", + }, + "type": { + name: "test", + attr: Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expectedErr: "cannot have both Blocks and Type set", + }, + } + + for name, tc := range tests { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.attr.tfprotov6SchemaNestedBlock(context.Background(), tc.name, tc.path) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} + func TestAttributeModifyPlan(t *testing.T) { t.Parallel() @@ -2054,7 +2636,78 @@ func TestAttributeValidate(t *testing.T) { req ValidateAttributeRequest resp ValidateAttributeResponse }{ - "no-attributes-or-type": { + "no-attributes-blocks-or-type": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Required: true, + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Invalid Attribute Definition", + "Attribute must define either Attributes, Blocks, or Type. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "both-attributes-and-blocks": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "testvalue"), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + }, + }), + Blocks: ListNestedBlocks(map[string]Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("test"), + "Invalid Attribute Definition", + "Attribute cannot define both Attributes and Blocks. This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + }, + }, + "both-attributes-and-type": { req: ValidateAttributeRequest{ AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), Config: Config{ @@ -2068,6 +2721,13 @@ func TestAttributeValidate(t *testing.T) { Schema: Schema{ Attributes: map[string]Attribute{ "test": { + Attributes: SingleNestedAttributes(map[string]Attribute{ + "testing": { + Type: types.StringType, + Optional: true, + }, + }), + Type: types.StringType, Required: true, }, }, @@ -2079,12 +2739,12 @@ func TestAttributeValidate(t *testing.T) { diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("test"), "Invalid Attribute Definition", - "Attribute must define either Attributes or Type. This is always a problem with the provider and should be reported to the provider developer.", + "Attribute cannot define both Attributes and Type. This is always a problem with the provider and should be reported to the provider developer.", ), }, }, }, - "both-attributes-and-type": { + "both-blocks-and-type": { req: ValidateAttributeRequest{ AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), Config: Config{ @@ -2098,12 +2758,12 @@ func TestAttributeValidate(t *testing.T) { Schema: Schema{ Attributes: map[string]Attribute{ "test": { - Attributes: SingleNestedAttributes(map[string]Attribute{ + Blocks: ListNestedBlocks(map[string]Attribute{ "testing": { Type: types.StringType, Optional: true, }, - }), + }, ListNestedBlocksOptions{}), Type: types.StringType, Required: true, }, @@ -2116,7 +2776,7 @@ func TestAttributeValidate(t *testing.T) { diag.NewAttributeErrorDiagnostic( tftypes.NewAttributePath().WithAttributeName("test"), "Invalid Attribute Definition", - "Attribute cannot define both Attributes and Type. This is always a problem with the provider and should be reported to the provider developer.", + "Attribute cannot define both Blocks and Type. This is always a problem with the provider and should be reported to the provider developer.", ), }, }, @@ -2869,6 +3529,244 @@ func TestAttributeValidate(t *testing.T) { }, }, }, + "nested-block-list-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-block-list-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + testErrorDiagnostic1, + }, + }, + }, + "nested-block-set-no-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{}, + }, + "nested-block-set-validation": { + req: ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("test"), + Config: Config{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_attr": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, + ), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "nested_attr": { + Type: types.StringType, + Required: true, + Validators: []AttributeValidator{ + testErrorAttributeValidator{}, + }, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + }, + }, + resp: ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{ + testErrorDiagnostic1, + }, + }, + }, } for name, tc := range testCases { diff --git a/tfsdk/config_test.go b/tfsdk/config_test.go index f3604e9ce..f63920dac 100644 --- a/tfsdk/config_test.go +++ b/tfsdk/config_test.go @@ -381,6 +381,100 @@ func TestConfigGetAttribute(t *testing.T) { path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-ListNestedBlocks-null-WithElementKeyInt-WithAttributeName": { + config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyInt-WithAttributeName": { + config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-Map-null-WithElementKeyString": { config: Config{ Raw: tftypes.NewValue(tftypes.Object{ @@ -799,6 +893,112 @@ func TestConfigGetAttribute(t *testing.T) { })).WithAttributeName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-SetNestedBlocks-null-WithElementKeyValue-WithAttributeName": { + config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + })).WithAttributeName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyValue-WithAttributeName": { + config: Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + })).WithAttributeName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-SingleNestedAttributes-null-WithAttributeName": { config: Config{ Raw: tftypes.NewValue(tftypes.Object{ diff --git a/tfsdk/nested_attributes.go b/tfsdk/nested_attributes.go index 403cbbfb1..8019f31e2 100644 --- a/tfsdk/nested_attributes.go +++ b/tfsdk/nested_attributes.go @@ -8,9 +8,14 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) -// NestingMode is an enum type of the ways nested attributes can be nested. -// They can be a list, a set, or a map (with string keys), or they can be -// nested directly, like an object. +// NestingMode is an enum type of the ways nested attributes can be nested in +// an attribute or a block. They can be a list, a set, a map (with string +// keys), or they can be nested directly, like an object. +// +// While the protocol and theoretically Terraform itself support map, single, +// and group nesting modes, this framework intentionally only supports list +// and set for blocks as those other modes were not typically implemented or +// tested since the older Terraform Plugin SDK did not support them. type NestingMode uint8 const ( diff --git a/tfsdk/nested_blocks.go b/tfsdk/nested_blocks.go new file mode 100644 index 000000000..5b0538b59 --- /dev/null +++ b/tfsdk/nested_blocks.go @@ -0,0 +1,189 @@ +package tfsdk + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// NestedBlocks surfaces a group of attributes to nest beneath another +// attribute as a block, and how that nesting should behave. Block nesting can +// have the following modes: +// +// * ListNestedBlocks are nested attributes as a block that represent a list of +// structs or objects; there can be multiple instances of them beneath that +// specific attribute. +// +// * SetNestedBlocks are nested attributes as a block that represent a set of +// structs or objects; there can be multiple instances of them beneath that +// specific attribute. Unlike ListNestedBlocks, these nested attributes must have +// unique values. +type NestedBlocks interface { + tftypes.AttributePathStepper + AttributeType() attr.Type + GetNestingMode() NestingMode + GetAttributes() map[string]Attribute + GetMinItems() int64 + GetMaxItems() int64 + Equal(NestedBlocks) bool + unimplementable() +} + +// ListNestedBlocks nests `attributes` under another attribute, allowing +// multiple instances of that group of attributes to appear in the +// configuration. Minimum and maximum numbers of times the group can appear in +// the configuration can be set using `opts`. +func ListNestedBlocks(attributes map[string]Attribute, opts ListNestedBlocksOptions) NestedBlocks { + return listNestedBlocks{ + nestedAttributes: nestedAttributes(attributes), + min: opts.MinItems, + max: opts.MaxItems, + } +} + +type listNestedBlocks struct { + nestedAttributes + + min, max int +} + +// ListNestedBlocksOptions captures additional, optional parameters for +// ListNestedBlocks. +type ListNestedBlocksOptions struct { + MinItems int + MaxItems int +} + +func (l listNestedBlocks) GetNestingMode() NestingMode { + return NestingModeList +} + +func (l listNestedBlocks) GetMinItems() int64 { + return int64(l.min) +} + +func (l listNestedBlocks) GetMaxItems() int64 { + return int64(l.max) +} + +// AttributeType returns an attr.Type corresponding to the nested attributes. +func (l listNestedBlocks) AttributeType() attr.Type { + return types.ListType{ + ElemType: l.nestedAttributes.AttributeType(), + } +} + +func (l listNestedBlocks) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyInt) + if !ok { + return nil, fmt.Errorf("can't apply %T to ListNestedBlocks", step) + } + return l.nestedAttributes, nil +} + +func (l listNestedBlocks) Equal(o NestedBlocks) bool { + other, ok := o.(listNestedBlocks) + if !ok { + return false + } + if l.min != other.min { + return false + } + if l.max != other.max { + return false + } + if len(other.nestedAttributes) != len(l.nestedAttributes) { + return false + } + for k, v := range l.nestedAttributes { + otherV, ok := other.nestedAttributes[k] + if !ok { + return false + } + if !v.Equal(otherV) { + return false + } + } + return true +} + +// SetNestedBlocks nests `attributes` under another attribute, allowing +// multiple instances of that group of attributes to appear in the +// configuration, while requiring each group of values be unique. Minimum and +// maximum numbers of times the group can appear in the configuration can be +// set using `opts`. +func SetNestedBlocks(attributes map[string]Attribute, opts SetNestedBlocksOptions) NestedBlocks { + return setNestedBlocks{ + nestedAttributes: nestedAttributes(attributes), + min: opts.MinItems, + max: opts.MaxItems, + } +} + +type setNestedBlocks struct { + nestedAttributes + + min, max int +} + +// SetNestedBlocksOptions captures additional, optional parameters for +// SetNestedBlocks. +type SetNestedBlocksOptions struct { + MinItems int + MaxItems int +} + +func (s setNestedBlocks) GetNestingMode() NestingMode { + return NestingModeSet +} + +func (s setNestedBlocks) GetMinItems() int64 { + return int64(s.min) +} + +func (s setNestedBlocks) GetMaxItems() int64 { + return int64(s.max) +} + +// AttributeType returns an attr.Type corresponding to the nested attributes. +func (s setNestedBlocks) AttributeType() attr.Type { + return types.SetType{ + ElemType: s.nestedAttributes.AttributeType(), + } +} + +func (s setNestedBlocks) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + _, ok := step.(tftypes.ElementKeyValue) + if !ok { + return nil, fmt.Errorf("can't use %T on sets", step) + } + return s.nestedAttributes, nil +} + +func (s setNestedBlocks) Equal(o NestedBlocks) bool { + other, ok := o.(setNestedBlocks) + if !ok { + return false + } + if s.min != other.min { + return false + } + if s.max != other.max { + return false + } + if len(other.nestedAttributes) != len(s.nestedAttributes) { + return false + } + for k, v := range s.nestedAttributes { + otherV, ok := other.nestedAttributes[k] + if !ok { + return false + } + if !v.Equal(otherV) { + return false + } + } + return true +} diff --git a/tfsdk/plan_test.go b/tfsdk/plan_test.go index bb51df089..5e38100f5 100644 --- a/tfsdk/plan_test.go +++ b/tfsdk/plan_test.go @@ -381,6 +381,100 @@ func TestPlanGetAttribute(t *testing.T) { path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-ListNestedBlocks-null-WithElementKeyInt-WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyInt-WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-Map-null-WithElementKeyString": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ @@ -799,6 +893,112 @@ func TestPlanGetAttribute(t *testing.T) { })).WithAttributeName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-SetNestedBlocks-null-WithElementKeyValue-WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + })).WithAttributeName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyValue-WithAttributeName": { + plan: Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + })).WithAttributeName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-SingleNestedAttributes-null-WithAttributeName": { plan: Plan{ Raw: tftypes.NewValue(tftypes.Object{ diff --git a/tfsdk/schema.go b/tfsdk/schema.go index 7894fc630..57d631e95 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -64,6 +64,9 @@ func (s Schema) AttributeType() attr.Type { if attr.Attributes != nil { attrTypes[name] = attr.Attributes.AttributeType() } + if attr.Blocks != nil { + attrTypes[name] = attr.Blocks.AttributeType() + } } return types.ObjectType{AttrTypes: attrTypes} } @@ -96,6 +99,10 @@ func (s Schema) AttributeTypeAtPath(path *tftypes.AttributePath) (attr.Type, err return a.Type, nil } + if a.definesBlocks() { + return a.Blocks.AttributeType(), nil + } + return a.Attributes.AttributeType(), nil } @@ -109,6 +116,9 @@ func (s Schema) TerraformType(ctx context.Context) tftypes.Type { if attr.Attributes != nil { attrTypes[name] = attr.Attributes.AttributeType().TerraformType(ctx) } + if attr.Blocks != nil { + attrTypes[name] = attr.Blocks.AttributeType().TerraformType(ctx) + } } return tftypes.Object{AttributeTypes: attrTypes} } @@ -140,20 +150,32 @@ func (s Schema) AttributeAtPath(path *tftypes.AttributePath) (Attribute, error) // tfprotov6Schema returns the *tfprotov6.Schema equivalent of a Schema. At least // one attribute must be set in the schema, or an error will be returned. func (s Schema) tfprotov6Schema(ctx context.Context) (*tfprotov6.Schema, error) { + if len(s.Attributes) < 1 { + return nil, errors.New("must have at least one attribute in the schema") + } + result := &tfprotov6.Schema{ Version: s.Version, } var attrs []*tfprotov6.SchemaAttribute + var blocks []*tfprotov6.SchemaNestedBlock for name, attr := range s.Attributes { - a, err := attr.tfprotov6SchemaAttribute(ctx, name, tftypes.NewAttributePath().WithAttributeName(name)) + proto6Raw, err := attr.tfprotov6(ctx, name, tftypes.NewAttributePath().WithAttributeName(name)) if err != nil { return nil, err } - attrs = append(attrs, a) + switch proto6 := proto6Raw.(type) { + case *tfprotov6.SchemaAttribute: + attrs = append(attrs, proto6) + case *tfprotov6.SchemaNestedBlock: + blocks = append(blocks, proto6) + default: + return nil, fmt.Errorf("unknown tfprotov6 type %T for Attribute", proto6) + } } sort.Slice(attrs, func(i, j int) bool { @@ -168,14 +190,23 @@ func (s Schema) tfprotov6Schema(ctx context.Context) (*tfprotov6.Schema, error) return attrs[i].Name < attrs[j].Name }) - if len(attrs) < 1 { - return nil, errors.New("must have at least one attribute in the schema") - } + sort.Slice(blocks, func(i, j int) bool { + if blocks[i] == nil { + return true + } + + if blocks[j] == nil { + return false + } + + return blocks[i].TypeName < blocks[j].TypeName + }) result.Block = &tfprotov6.SchemaBlock{ // core doesn't do anything with version, as far as I can tell, // so let's not set it. Attributes: attrs, + BlockTypes: blocks, Deprecated: s.DeprecationMessage != "", } @@ -254,7 +285,7 @@ func modifyAttributesPlans(ctx context.Context, attrs map[string]Attribute, path } resp.Diagnostics = nestedAttrResp.Diagnostics - if nestedAttr.Attributes != nil { + if nestedAttr.definesAttributes() { nm := nestedAttr.Attributes.GetNestingMode() switch nm { case NestingModeList: @@ -350,5 +381,69 @@ func modifyAttributesPlans(ctx context.Context, attrs map[string]Attribute, path continue } } + + if nestedAttr.definesBlocks() { + nm := nestedAttr.Blocks.GetNestingMode() + switch nm { + case NestingModeList: + l, ok := attrPlan.(types.List) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", attrPlan, nm, attrPath) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + continue + } + + for idx := range l.Elems { + modifyAttributesPlans(ctx, nestedAttr.Blocks.GetAttributes(), attrPath.WithElementKeyInt(idx), req, resp) + } + case NestingModeSet: + s, ok := attrPlan.(types.Set) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", attrPlan, nm, attrPath) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + for _, value := range s.Elems { + tfValueRaw, err := value.ToTerraformValue(ctx) + + if err != nil { + err := fmt.Errorf("error running ToTerraformValue on element value: %v", value) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modification cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(), + ) + + return + } + + tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw) + + modifyAttributesPlans(ctx, nestedAttr.Blocks.GetAttributes(), attrPath.WithElementKeyValue(tfValue), req, resp) + } + default: + err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, attrPath) + resp.Diagnostics.AddAttributeError( + attrPath, + "Attribute Plan Modification Error", + "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(), + ) + + continue + } + } } } diff --git a/tfsdk/schema_test.go b/tfsdk/schema_test.go index ea0154a7a..ee6329561 100644 --- a/tfsdk/schema_test.go +++ b/tfsdk/schema_test.go @@ -210,6 +210,133 @@ func TestSchemaAttributeAtPath(t *testing.T) { expected: Attribute{}, expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to ListNestedAttributes", }, + "WithAttributeName-ListNestedBlocks-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: "AttributeName(\"sub_test\") still remains in the path: can't apply tftypes.AttributeName to ListNestedBlocks", + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyInt-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't apply tftypes.ElementKeyString to ListNestedBlocks", + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: "ElementKeyValue(tftypes.String<\"sub_test\">) still remains in the path: can't apply tftypes.ElementKeyValue to ListNestedBlocks", + }, "WithAttributeName-MapNestedAttributes-WithAttributeName": { schema: Schema{ Attributes: map[string]Attribute{ @@ -474,6 +601,133 @@ func TestSchemaAttributeAtPath(t *testing.T) { Required: true, }, }, + "WithAttributeName-SetNestedBlocks-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithAttributeName("sub_test"), + expected: Attribute{}, + expectedErr: "AttributeName(\"sub_test\") still remains in the path: can't use tftypes.AttributeName on sets", + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyInt": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0), + expected: Attribute{}, + expectedErr: "ElementKeyInt(0) still remains in the path: can't use tftypes.ElementKeyInt on sets", + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyString": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyString("sub_test"), + expected: Attribute{}, + expectedErr: "ElementKeyString(\"sub_test\") still remains in the path: can't use tftypes.ElementKeyString on sets", + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyValue": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "sub_test")), + expected: Attribute{}, + expectedErr: ErrPathInsideAtomicAttribute.Error(), + }, + "WithAttributeName-SetNestedBlocks-WithElementKeyValue-WithAttributeName": { + schema: Schema{ + Attributes: map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "test": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "other": { + Type: types.BoolType, + Optional: true, + }, + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyValue(tftypes.NewValue(tftypes.String, "element")).WithAttributeName("sub_test"), + expected: Attribute{ + Type: types.StringType, + Required: true, + }, + }, "WithAttributeName-SingleNestedAttributes-WithAttributeName": { schema: Schema{ Attributes: map[string]Attribute{ @@ -821,6 +1075,27 @@ func TestSchemaAttributeType(t *testing.T) { }, }), }, + "list_nested_blocks": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, }, } @@ -844,6 +1119,16 @@ func TestSchemaAttributeType(t *testing.T) { "delete_with_instance": types.BoolType, }, }, + "list_nested_blocks": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string": types.StringType, + "number": types.NumberType, + "bool": types.BoolType, + "list": types.ListType{ElemType: types.StringType}, + }, + }, + }, }, } @@ -1192,6 +1477,120 @@ func TestSchemaTfprotov6Schema(t *testing.T) { }, }, }, + "nested-blocks": { + input: Schema{ + Version: 3, + Attributes: map[string]Attribute{ + "list": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, + "set": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "string": { + Type: types.StringType, + Required: true, + }, + "number": { + Type: types.NumberType, + Optional: true, + }, + "bool": { + Type: types.BoolType, + Computed: true, + }, + "list": { + Type: types.ListType{ElemType: types.StringType}, + Computed: true, + Optional: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + expected: &tfprotov6.Schema{ + Version: 3, + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "bool", + Type: tftypes.Bool, + }, + { + Computed: true, + Name: "list", + Optional: true, + Type: tftypes.List{ElementType: tftypes.String}, + }, + { + Name: "number", + Optional: true, + Type: tftypes.Number, + }, + { + Name: "string", + Required: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "list", + }, + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Computed: true, + Name: "bool", + Type: tftypes.Bool, + }, + { + Computed: true, + Name: "list", + Optional: true, + Type: tftypes.List{ElementType: tftypes.String}, + }, + { + Name: "number", + Optional: true, + Type: tftypes.Number, + }, + { + Name: "string", + Required: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "set", + }, + }, + }, + }, + }, "markdown-description": { input: Schema{ Version: 1, diff --git a/tfsdk/serve_provider_test.go b/tfsdk/serve_provider_test.go index df75a0fd0..1c884119d 100644 --- a/tfsdk/serve_provider_test.go +++ b/tfsdk/serve_provider_test.go @@ -230,6 +230,19 @@ func (t *testServeProvider) GetSchema(_ context.Context) (Schema, diag.Diagnosti }, ListNestedAttributesOptions{}), Optional: true, }, + "list-nested-blocks": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "foo": { + Type: types.StringType, + Optional: true, + Computed: true, + }, + "bar": { + Type: types.NumberType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, "map-nested-attributes": { Attributes: MapNestedAttributes(map[string]Attribute{ "foo": { @@ -258,6 +271,19 @@ func (t *testServeProvider) GetSchema(_ context.Context) (Schema, diag.Diagnosti }, SetNestedAttributesOptions{}), Optional: true, }, + "set-nested-blocks": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "foo": { + Type: types.StringType, + Optional: true, + Computed: true, + }, + "bar": { + Type: types.NumberType, + Required: true, + }, + }, SetNestedBlocksOptions{}), + }, }, }, nil } @@ -493,6 +519,46 @@ var testServeProviderProviderSchema = &tfprotov6.Schema{ }, // TODO: add tuples when we support them }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bar", + Type: tftypes.Number, + Required: true, + }, + { + Name: "foo", + Type: tftypes.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "list-nested-blocks", + }, + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bar", + Type: tftypes.Number, + Required: true, + }, + { + Name: "foo", + Type: tftypes.String, + Optional: true, + Computed: true, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + TypeName: "set-nested-blocks", + }, + }, }, } @@ -539,6 +605,10 @@ var testServeProviderProviderType = tftypes.Object{ "foo": tftypes.String, "bar": tftypes.Number, }}}, + "list-nested-blocks": tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, "map-nested-attributes": tftypes.Map{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, "bar": tftypes.Number, @@ -547,6 +617,10 @@ var testServeProviderProviderType = tftypes.Object{ "foo": tftypes.String, "bar": tftypes.Number, }}}, + "set-nested-blocks": tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, }, } diff --git a/tfsdk/serve_resource_two_test.go b/tfsdk/serve_resource_two_test.go index a7152c2e1..9df2cc4e5 100644 --- a/tfsdk/serve_resource_two_test.go +++ b/tfsdk/serve_resource_two_test.go @@ -38,6 +38,22 @@ func (rt testServeResourceTypeTwo) GetSchema(_ context.Context) (Schema, diag.Di }, }, ListNestedAttributesOptions{}), }, + "list_nested_blocks": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "required_bool": { + Required: true, + Type: types.BoolType, + }, + "required_number": { + Required: true, + Type: types.NumberType, + }, + "required_string": { + Required: true, + Type: types.StringType, + }, + }, ListNestedBlocksOptions{}), + }, }, }, nil } @@ -91,6 +107,31 @@ var testServeResourceTypeTwoSchema = &tfprotov6.Schema{ Type: tftypes.String, }, }, + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "required_bool", + Required: true, + Type: tftypes.Bool, + }, + { + Name: "required_number", + Required: true, + Type: tftypes.Number, + }, + { + Name: "required_string", + Required: true, + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "list_nested_blocks", + }, + }, }, } @@ -104,6 +145,13 @@ var testServeResourceTypeTwoType = tftypes.Object{ "size_gb": tftypes.Number, }}, }, + "list_nested_blocks": tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }}, + }, }, } diff --git a/tfsdk/serve_test.go b/tfsdk/serve_test.go index 0b5034e66..c4cabb362 100644 --- a/tfsdk/serve_test.go +++ b/tfsdk/serve_test.go @@ -491,6 +491,25 @@ func TestServerValidateProviderConfig(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 14554216), }), }), + "list-nested-blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), "map": tftypes.NewValue(tftypes.Map{ElementType: tftypes.Number}, map[string]tftypes.Value{ "foo": tftypes.NewValue(tftypes.Number, 123), "bar": tftypes.NewValue(tftypes.Number, 456), @@ -534,6 +553,25 @@ func TestServerValidateProviderConfig(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 14554216), }), }), + "set-nested-blocks": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), }), provider: &testServeProvider{}, providerType: testServeProviderProviderType, @@ -931,6 +969,25 @@ func TestServerConfigureProvider(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 14554216), }), }), + "list-nested-blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), "set-nested-attributes": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "foo": tftypes.String, "bar": tftypes.Number, @@ -950,6 +1007,25 @@ func TestServerConfigureProvider(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 14554216), }), }), + "set-nested-blocks": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "let's do the math"), + "bar": tftypes.NewValue(tftypes.Number, 18973), + }), + tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}, map[string]tftypes.Value{ + "foo": tftypes.NewValue(tftypes.String, "this is why we can't have nice things"), + "bar": tftypes.NewValue(tftypes.Number, 14554216), + }), + }), }), }, "config-unknown-value": { @@ -1014,6 +1090,10 @@ func TestServerConfigureProvider(t *testing.T) { "bar": tftypes.NewValue(tftypes.Number, 456), "baz": tftypes.NewValue(tftypes.Number, 789), }), + "list-nested-blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, tftypes.UnknownValue), "map-nested-attributes": tftypes.NewValue(tftypes.Map{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ "bar": tftypes.Number, "foo": tftypes.String, @@ -1037,6 +1117,10 @@ func TestServerConfigureProvider(t *testing.T) { "foo": tftypes.String, "bar": tftypes.Number, }}}, tftypes.UnknownValue), + "set-nested-blocks": tftypes.NewValue(tftypes.Set{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "foo": tftypes.String, + "bar": tftypes.Number, + }}}, tftypes.UnknownValue), }), }, } @@ -1433,6 +1517,13 @@ func TestServerReadResource(t *testing.T) { }, }, }, tftypes.UnknownValue), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, tftypes.UnknownValue), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -1461,6 +1552,25 @@ func TestServerReadResource(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }) }, @@ -1487,6 +1597,25 @@ func TestServerReadResource(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), }, "two_diags": { @@ -1501,6 +1630,13 @@ func TestServerReadResource(t *testing.T) { }, }, }, tftypes.UnknownValue), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, tftypes.UnknownValue), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -1529,6 +1665,25 @@ func TestServerReadResource(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }) resp.Diagnostics.AddAttributeWarning( tftypes.NewAttributePath().WithAttributeName("disks").WithElementKeyInt(0), @@ -1564,6 +1719,25 @@ func TestServerReadResource(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), expectedDiags: []*tfprotov6.Diagnostic{ @@ -1814,6 +1988,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, nil), config: tftypes.NewValue(testServeResourceTypeTwoType, nil), @@ -2147,6 +2340,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2165,6 +2377,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2183,6 +2414,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -2212,6 +2462,36 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }), modifyPlanFunc: func(ctx context.Context, req ModifyResourcePlanRequest, resp *ModifyResourcePlanResponse) { resp.Plan.Raw = tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -2240,6 +2520,36 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }) }, }, @@ -2261,6 +2571,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "1234567"), @@ -2279,6 +2608,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "1234567"), @@ -2297,6 +2645,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -2317,6 +2684,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), modifyPlanFunc: func(ctx context.Context, req ModifyResourcePlanRequest, resp *ModifyResourcePlanResponse) { resp.RequiresReplace = []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("id")} @@ -2341,6 +2727,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2359,6 +2764,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2377,6 +2801,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -2397,6 +2840,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), modifyPlanFunc: func(ctx context.Context, req ModifyResourcePlanRequest, resp *ModifyResourcePlanResponse) { resp.RequiresReplace = []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("id")} @@ -2429,6 +2891,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2447,6 +2928,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), @@ -2465,6 +2965,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), resource: "test_two", resourceType: testServeResourceTypeTwoType, @@ -2485,6 +3004,25 @@ func TestServerPlanResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), modifyPlanFunc: func(ctx context.Context, req ModifyResourcePlanRequest, resp *ModifyResourcePlanResponse) { resp.RequiresReplace = []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("id")} @@ -3693,6 +4231,13 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.Bool, }, }}, tftypes.UnknownValue), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, tftypes.UnknownValue), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -3703,6 +4248,13 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.Bool, }, }}, nil), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, nil), }), resource: "test_two", action: "create", @@ -3729,6 +4281,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "stringvalue"), + }), + }), }) }, expectedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -3752,6 +4323,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "stringvalue"), + }), + }), }), }, "two_update": { @@ -3776,6 +4366,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), plannedState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -3809,6 +4418,36 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, false), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -3842,6 +4481,36 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, false), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }), resource: "test_two", action: "update", @@ -3879,6 +4548,36 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, false), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }) }, expectedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -3913,6 +4612,36 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, false), + "required_number": tftypes.NewValue(tftypes.Number, 456), + "required_string": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }), }), }, "two_delete": { @@ -3948,6 +4677,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), resource: "test_two", action: "delete", @@ -4077,6 +4825,13 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.Bool, }, }}, tftypes.UnknownValue), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, tftypes.UnknownValue), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -4087,6 +4842,13 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.Bool, }, }}, nil), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, nil), }), providerMeta: tftypes.NewValue(testServeProviderMetaType, map[string]tftypes.Value{ "foo": tftypes.NewValue(tftypes.String, "my provider_meta value"), @@ -4116,6 +4878,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }) }, expectedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -4139,6 +4920,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), }, "two_meta_update": { @@ -4163,6 +4963,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, true), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), plannedState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -4196,6 +5015,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), config: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "test-instance"), @@ -4229,6 +5067,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), providerMeta: tftypes.NewValue(testServeProviderMetaType, map[string]tftypes.Value{ "foo": tftypes.NewValue(tftypes.String, "my provider_meta value"), @@ -4269,6 +5126,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }) }, expectedNewState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -4303,6 +5179,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), }, "two_meta_delete": { @@ -4338,6 +5233,25 @@ func TestServerApplyResourceChange(t *testing.T) { "boot": tftypes.NewValue(tftypes.Bool, false), }), }), + "list_nested_blocks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }}, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "required_bool": tftypes.Bool, + "required_number": tftypes.Number, + "required_string": tftypes.String, + }, + }, map[string]tftypes.Value{ + "required_bool": tftypes.NewValue(tftypes.Bool, true), + "required_number": tftypes.NewValue(tftypes.Number, 123), + "required_string": tftypes.NewValue(tftypes.String, "statevalue"), + }), + }), }), providerMeta: tftypes.NewValue(testServeProviderMetaType, map[string]tftypes.Value{ "foo": tftypes.NewValue(tftypes.String, "my provider_meta value"), diff --git a/tfsdk/state_test.go b/tfsdk/state_test.go index 52bc5c4cf..bb707f7ac 100644 --- a/tfsdk/state_test.go +++ b/tfsdk/state_test.go @@ -1078,6 +1078,100 @@ func TestStateGetAttribute(t *testing.T) { path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), expected: types.String{Value: "value"}, }, + "WithAttributeName-ListNestedBlocks-null-WithElementKeyInt-WithAttributeName": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, nil), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Null: true}, + }, + "WithAttributeName-ListNestedBlocks-WithElementKeyInt-WithAttributeName": { + state: State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, + "other": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "sub_test": tftypes.String, + }, + }, map[string]tftypes.Value{ + "sub_test": tftypes.NewValue(tftypes.String, "value"), + }), + }), + "other": tftypes.NewValue(tftypes.Bool, nil), + }), + Schema: Schema{ + Attributes: map[string]Attribute{ + "test": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "sub_test": { + Type: types.StringType, + Required: true, + }, + }, ListNestedBlocksOptions{}), + }, + "other": { + Type: types.BoolType, + Optional: true, + }, + }, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test").WithElementKeyInt(0).WithAttributeName("sub_test"), + expected: types.String{Value: "value"}, + }, "WithAttributeName-Map-null-WithElementKeyString": { state: State{ Raw: tftypes.NewValue(tftypes.Object{ @@ -2284,7 +2378,7 @@ func TestStateSet(t *testing.T) { }), }), }, - "nested-list": { + "nested-list-attr": { state: State{ Raw: tftypes.Value{}, Schema: Schema{ @@ -2367,6 +2461,87 @@ func TestStateSet(t *testing.T) { }), }), }, + "nested-list-block": { + state: State{ + Raw: tftypes.Value{}, + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Blocks: ListNestedBlocks(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, ListNestedBlocksOptions{}), + }, + }, + }, + }, + val: struct { + Disks []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + } `tfsdk:"disks"` + }{ + Disks: []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + { + ID: "disk0", + DeleteWithInstance: true, + }, + { + ID: "disk1", + DeleteWithInstance: false, + }, + }, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + }), + }, "nested-single": { state: State{ Raw: tftypes.Value{}, @@ -2499,7 +2674,7 @@ func TestStateSet(t *testing.T) { }), }), }, - "nested-set": { + "nested-set-attr": { state: State{ Raw: tftypes.Value{}, Schema: Schema{ @@ -2582,6 +2757,87 @@ func TestStateSet(t *testing.T) { }), }), }, + "nested-set-block": { + state: State{ + Raw: tftypes.Value{}, + Schema: Schema{ + Attributes: map[string]Attribute{ + "disks": { + Blocks: SetNestedBlocks(map[string]Attribute{ + "id": { + Type: types.StringType, + Required: true, + }, + "delete_with_instance": { + Type: types.BoolType, + Optional: true, + }, + }, SetNestedBlocksOptions{}), + }, + }, + }, + }, + val: struct { + Disks []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + } `tfsdk:"disks"` + }{ + Disks: []struct { + ID string `tfsdk:"id"` + DeleteWithInstance bool `tfsdk:"delete_with_instance"` + }{ + { + ID: "disk0", + DeleteWithInstance: true, + }, + { + ID: "disk1", + DeleteWithInstance: false, + }, + }, + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "disks": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "disks": tftypes.NewValue(tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk0"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, true), + }), + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "delete_with_instance": tftypes.Bool, + }, + }, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "disk1"), + "delete_with_instance": tftypes.NewValue(tftypes.Bool, false), + }), + }), + }), + }, "AttrTypeWithValidateError": { state: State{ Raw: tftypes.Value{},