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_nested_id": testResourceNestedId(),
|
||||
"test_undeleteable": testResourceUndeleteable(),
|
||||
"test_resource_required_min": testResourceRequiredMin(),
|
||||
},
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
if l > blockS.MaxItems && blockS.MaxItems > 0 {
|
||||
|
|
|
@ -331,7 +331,7 @@ func TestCoerceValue(t *testing.T) {
|
|||
"foo": {
|
||||
Block: Block{},
|
||||
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": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
|
|
|
@ -33,6 +33,14 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
|
||||
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 {
|
||||
case NestingSingle, NestingGroup:
|
||||
ret[name] = &hcldec.BlockSpec{
|
||||
|
@ -57,14 +65,14 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
ret[name] = &hcldec.BlockTupleSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: blockS.MinItems,
|
||||
MinItems: minItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
} else {
|
||||
ret[name] = &hcldec.BlockListSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: blockS.MinItems,
|
||||
MinItems: minItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +85,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
|
|||
ret[name] = &hcldec.BlockSetSpec{
|
||||
TypeName: name,
|
||||
Nested: childSpec,
|
||||
MinItems: blockS.MinItems,
|
||||
MinItems: minItems,
|
||||
MaxItems: blockS.MaxItems,
|
||||
}
|
||||
case NestingMap:
|
||||
|
|
|
@ -356,6 +356,33 @@ func TestBlockDecoderSpec(t *testing.T) {
|
|||
}),
|
||||
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": {
|
||||
&Block{},
|
||||
hcltest.MockBody(&hcl.BodyContent{
|
||||
|
|
|
@ -1456,6 +1456,15 @@ func (m schemaMap) validateList(
|
|||
"%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
|
||||
if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems {
|
||||
return nil, []error{fmt.Errorf(
|
||||
|
|
Loading…
Reference in New Issue