Merge pull request #22221 from hashicorp/jbardin/min-items
MinItems with dynamic blocks
This commit is contained in:
commit
5878527732
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue