From 67dbd6d3458cf3c67bba8d0f7770c15d7fd9644f Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 26 Jul 2019 14:43:11 -0700 Subject: [PATCH] don't check MinItems with unknowns in blocks If a block was defined via "dynamic", there will be only one block value until the expansion is known. Since we can't detect dynamic blocks at this point, don't verify MinItems while there are unknown values in the config. The decoder spec can also only check for existence of a block, so limit the check to 0 or 1. --- configs/configschema/coerce_value.go | 10 +++++-- configs/configschema/coerce_value_test.go | 35 ++++++++++++++++++++++- configs/configschema/decoder_spec.go | 14 +++++++-- configs/configschema/decoder_spec_test.go | 27 +++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/configs/configschema/coerce_value.go b/configs/configschema/coerce_value.go index e59f58d8e5..7996c383ab 100644 --- a/configs/configschema/coerce_value.go +++ b/configs/configschema/coerce_value.go @@ -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 { diff --git a/configs/configschema/coerce_value_test.go b/configs/configschema/coerce_value_test.go index 3286751a35..7fd07856d4 100644 --- a/configs/configschema/coerce_value_test.go +++ b/configs/configschema/coerce_value_test.go @@ -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{ diff --git a/configs/configschema/decoder_spec.go b/configs/configschema/decoder_spec.go index d8f41eabc7..e748dd20de 100644 --- a/configs/configschema/decoder_spec.go +++ b/configs/configschema/decoder_spec.go @@ -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: diff --git a/configs/configschema/decoder_spec_test.go b/configs/configschema/decoder_spec_test.go index b779f2c352..d6f6a6a153 100644 --- a/configs/configschema/decoder_spec_test.go +++ b/configs/configschema/decoder_spec_test.go @@ -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{