Merge pull request #22221 from hashicorp/jbardin/min-items

MinItems with dynamic blocks
This commit is contained in:
James Bardin 2019-07-29 09:45:42 -07:00 committed by GitHub
commit 5878527732
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 6 deletions

View File

@ -37,6 +37,7 @@ func Provider() terraform.ResourceProvider {
"test_resource_config_mode": testResourceConfigMode(), "test_resource_config_mode": testResourceConfigMode(),
"test_resource_nested_id": testResourceNestedId(), "test_resource_nested_id": testResourceNestedId(),
"test_undeleteable": testResourceUndeleteable(), "test_undeleteable": testResourceUndeleteable(),
"test_resource_required_min": testResourceRequiredMin(),
}, },
DataSourcesMap: map[string]*schema.Resource{ DataSourcesMap: map[string]*schema.Resource{
"test_data_source": testDataSource(), "test_data_source": testDataSource(),

View File

@ -0,0 +1,68 @@
package test
import (
"github.com/hashicorp/terraform/helper/schema"
)
func testResourceRequiredMin() *schema.Resource {
return &schema.Resource{
Create: testResourceRequiredMinCreate,
Read: testResourceRequiredMinRead,
Update: testResourceRequiredMinUpdate,
Delete: testResourceRequiredMinDelete,
CustomizeDiff: func(d *schema.ResourceDiff, _ interface{}) error {
if d.HasChange("dependent_list") {
d.SetNewComputed("computed_list")
}
return nil
},
Schema: map[string]*schema.Schema{
"min_items": {
Type: schema.TypeList,
Optional: true,
MinItems: 2,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
"required_min_items": {
Type: schema.TypeList,
Required: true,
MinItems: 2,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"val": {
Type: schema.TypeString,
Required: true,
},
},
},
},
},
}
}
func testResourceRequiredMinCreate(d *schema.ResourceData, meta interface{}) error {
d.SetId("testId")
return testResourceRequiredMinRead(d, meta)
}
func testResourceRequiredMinRead(d *schema.ResourceData, meta interface{}) error {
return nil
}
func testResourceRequiredMinUpdate(d *schema.ResourceData, meta interface{}) error {
return testResourceRequiredMinRead(d, meta)
}
func testResourceRequiredMinDelete(d *schema.ResourceData, meta interface{}) error {
d.SetId("")
return nil
}

View File

@ -0,0 +1,66 @@
package test
import (
"regexp"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestResource_dynamicRequiredMinItems(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: testAccCheckResourceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: `
resource "test_resource_required_min" "a" {
}
`,
ExpectError: regexp.MustCompile(`"required_min_items" blocks are required`),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_list" "a" {
dependent_list {
val = "a"
}
}
resource "test_resource_required_min" "b" {
dynamic "required_min_items" {
for_each = test_resource_list.a.computed_list
content {
val = required_min_items.value
}
}
}
`),
ExpectError: regexp.MustCompile(`required_min_items: attribute supports 2 item as a minimum`),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_list" "c" {
dependent_list {
val = "a"
}
dependent_list {
val = "b"
}
}
resource "test_resource_required_min" "b" {
dynamic "required_min_items" {
for_each = test_resource_list.c.computed_list
content {
val = required_min_items.value
}
}
}
`),
},
},
})
}

View File

@ -113,7 +113,10 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list") return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list")
} }
l := coll.LengthInt() l := coll.LengthInt()
if l < blockS.MinItems {
// Assume that if there are unknowns this could have come from
// a dynamic block, and we can't validate MinItems yet.
if l < blockS.MinItems && coll.IsWhollyKnown() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems)
} }
if l > blockS.MaxItems && blockS.MaxItems > 0 { if l > blockS.MaxItems && blockS.MaxItems > 0 {
@ -161,7 +164,10 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set") return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set")
} }
l := coll.LengthInt() l := coll.LengthInt()
if l < blockS.MinItems {
// Assume that if there are unknowns this could have come from
// a dynamic block, and we can't validate MinItems yet.
if l < blockS.MinItems && coll.IsWhollyKnown() {
return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems)
} }
if l > blockS.MaxItems && blockS.MaxItems > 0 { if l > blockS.MaxItems && blockS.MaxItems > 0 {

View File

@ -331,7 +331,7 @@ func TestCoerceValue(t *testing.T) {
"foo": { "foo": {
Block: Block{}, Block: Block{},
Nesting: NestingList, Nesting: NestingList,
MinItems: 1, MinItems: 2,
}, },
}, },
}, },
@ -345,6 +345,39 @@ func TestCoerceValue(t *testing.T) {
}), }),
"", "",
}, },
"unknowns in nested list": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Block: Block{
Attributes: map[string]*Attribute{
"attr": {
Type: cty.String,
Required: true,
},
},
},
Nesting: NestingList,
MinItems: 2,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.UnknownVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"attr": cty.UnknownVal(cty.String),
}),
}),
}),
"",
},
"unknown nested set": { "unknown nested set": {
&Block{ &Block{
Attributes: map[string]*Attribute{ Attributes: map[string]*Attribute{

View File

@ -33,6 +33,14 @@ func (b *Block) DecoderSpec() hcldec.Spec {
childSpec := blockS.Block.DecoderSpec() childSpec := blockS.Block.DecoderSpec()
// We can only validate 0 or 1 for MinItems, because a dynamic block
// may satisfy any number of min items while only having a single
// block in the config.
minItems := 0
if blockS.MinItems > 1 {
minItems = 1
}
switch blockS.Nesting { switch blockS.Nesting {
case NestingSingle, NestingGroup: case NestingSingle, NestingGroup:
ret[name] = &hcldec.BlockSpec{ ret[name] = &hcldec.BlockSpec{
@ -57,14 +65,14 @@ func (b *Block) DecoderSpec() hcldec.Spec {
ret[name] = &hcldec.BlockTupleSpec{ ret[name] = &hcldec.BlockTupleSpec{
TypeName: name, TypeName: name,
Nested: childSpec, Nested: childSpec,
MinItems: blockS.MinItems, MinItems: minItems,
MaxItems: blockS.MaxItems, MaxItems: blockS.MaxItems,
} }
} else { } else {
ret[name] = &hcldec.BlockListSpec{ ret[name] = &hcldec.BlockListSpec{
TypeName: name, TypeName: name,
Nested: childSpec, Nested: childSpec,
MinItems: blockS.MinItems, MinItems: minItems,
MaxItems: blockS.MaxItems, MaxItems: blockS.MaxItems,
} }
} }
@ -77,7 +85,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
ret[name] = &hcldec.BlockSetSpec{ ret[name] = &hcldec.BlockSetSpec{
TypeName: name, TypeName: name,
Nested: childSpec, Nested: childSpec,
MinItems: blockS.MinItems, MinItems: minItems,
MaxItems: blockS.MaxItems, MaxItems: blockS.MaxItems,
} }
case NestingMap: case NestingMap:

View File

@ -356,6 +356,33 @@ func TestBlockDecoderSpec(t *testing.T) {
}), }),
1, // too many "foo" blocks 1, // too many "foo" blocks
}, },
// dynamic blocks may fulfill MinItems, but there is only one block to
// decode.
"required MinItems": {
&Block{
BlockTypes: map[string]*NestedBlock{
"foo": {
Nesting: NestingList,
Block: Block{},
MinItems: 2,
},
},
},
hcltest.MockBody(&hcl.BodyContent{
Blocks: hcl.Blocks{
&hcl.Block{
Type: "foo",
Body: hcl.EmptyBody(),
},
},
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.EmptyObjectVal,
}),
}),
0,
},
"extraneous attribute": { "extraneous attribute": {
&Block{}, &Block{},
hcltest.MockBody(&hcl.BodyContent{ hcltest.MockBody(&hcl.BodyContent{

View File

@ -1456,6 +1456,15 @@ func (m schemaMap) validateList(
"%s: should be a list", k)} "%s: should be a list", k)}
} }
// We can't validate list length if this came from a dynamic block.
// Since there's no way to determine if something was from a dynamic block
// at this point, we're going to skip validation in the new protocol if
// there are any unknowns. Validate will eventually be called again once
// all values are known.
if isProto5() && !isWhollyKnown(raw) {
return nil, nil
}
// Validate length // Validate length
if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems { if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems {
return nil, []error{fmt.Errorf( return nil, []error{fmt.Errorf(