Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

configs/configschema: Introduce the NestingGroup mode for blocks #20949

Merged
merged 2 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions command/format/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,19 +278,20 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *config
// the objects within are computed.

switch blockS.Nesting {
case configschema.NestingSingle:
case configschema.NestingSingle, configschema.NestingGroup:
var action plans.Action
eqV := new.Equals(old)
switch {
case old.IsNull():
action = plans.Create
case new.IsNull():
action = plans.Delete
case !new.IsKnown() || !old.IsKnown():
case !new.IsWhollyKnown() || !old.IsWhollyKnown():
// "old" should actually always be known due to our contract
// that old values must never be unknown, but we'll allow it
// anyway to be robust.
action = plans.Update
case !(new.Equals(old).True()):
case !eqV.IsKnown() || !eqV.True():
action = plans.Update
}

Expand Down
2 changes: 1 addition & 1 deletion command/jsonconfig/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func marshalExpressions(body hcl.Body, schema *configschema.Block) expressions {
}

switch blockS.Nesting {
case configschema.NestingSingle:
case configschema.NestingSingle, configschema.NestingGroup:
ret[typeName] = marshalExpressions(block.Body, &blockS.Block)
case configschema.NestingList, configschema.NestingSet:
if _, exists := ret[typeName]; !exists {
Expand Down
2 changes: 2 additions & 0 deletions command/jsonprovider/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func marshalBlockTypes(nestedBlock *configschema.NestedBlock) *blockType {
switch nestedBlock.Nesting {
case configschema.NestingSingle:
ret.NestingMode = "single"
case configschema.NestingGroup:
ret.NestingMode = "group"
case configschema.NestingList:
ret.NestingMode = "list"
case configschema.NestingSet:
Expand Down
2 changes: 1 addition & 1 deletion config/hcl2shim/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func ConfigValueFromHCL2Block(v cty.Value, schema *configschema.Block) map[strin

switch blockS.Nesting {

case configschema.NestingSingle:
case configschema.NestingSingle, configschema.NestingGroup:
ret[name] = ConfigValueFromHCL2Block(bv, &blockS.Block)

case configschema.NestingList, configschema.NestingSet:
Expand Down
8 changes: 6 additions & 2 deletions configs/configschema/coerce_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
for typeName, blockS := range b.BlockTypes {
switch blockS.Nesting {

case NestingSingle:
case NestingSingle, NestingGroup:
switch {
case ty.HasAttribute(typeName):
var err error
Expand All @@ -84,7 +84,11 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
return cty.UnknownVal(b.ImpliedType()), err
}
case blockS.MinItems != 1 && blockS.MaxItems != 1:
attrs[typeName] = cty.NullVal(blockS.ImpliedType())
if blockS.Nesting == NestingGroup {
attrs[typeName] = blockS.EmptyValue()
} else {
attrs[typeName] = cty.NullVal(blockS.ImpliedType())
}
default:
// We use the word "attribute" here because we're talking about
// the cty sense of that word rather than the HCL sense.
Expand Down
10 changes: 9 additions & 1 deletion configs/configschema/decoder_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ func (b *Block) DecoderSpec() hcldec.Spec {
childSpec := blockS.Block.DecoderSpec()

switch blockS.Nesting {
case NestingSingle:
case NestingSingle, NestingGroup:
ret[name] = &hcldec.BlockSpec{
TypeName: name,
Nested: childSpec,
Required: blockS.MinItems == 1 && blockS.MaxItems >= 1,
}
if blockS.Nesting == NestingGroup {
ret[name] = &hcldec.DefaultSpec{
Primary: ret[name],
Default: &hcldec.LiteralSpec{
Value: blockS.EmptyValue(),
},
}
}
case NestingList:
// We prefer to use a list where possible, since it makes our
// implied type more complete, but if there are any
Expand Down
59 changes: 59 additions & 0 deletions configs/configschema/empty_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package configschema

import (
"github.com/zclconf/go-cty/cty"
)

// EmptyValue returns the "empty value" for the recieving block, which for
// a block type is a non-null object where all of the attribute values are
// the empty values of the block's attributes and nested block types.
//
// In other words, it returns the value that would be returned if an empty
// block were decoded against the recieving schema, assuming that no required
// attribute or block constraints were honored.
func (b *Block) EmptyValue() cty.Value {
vals := make(map[string]cty.Value)
for name, attrS := range b.Attributes {
vals[name] = attrS.EmptyValue()
}
for name, blockS := range b.BlockTypes {
vals[name] = blockS.EmptyValue()
}
return cty.ObjectVal(vals)
}

// EmptyValue returns the "empty value" for the receiving attribute, which is
// the value that would be returned if there were no definition of the attribute
// at all, ignoring any required constraint.
func (a *Attribute) EmptyValue() cty.Value {
return cty.NullVal(a.Type)
}

// EmptyValue returns the "empty value" for when there are zero nested blocks
// present of the receiving type.
func (b *NestedBlock) EmptyValue() cty.Value {
switch b.Nesting {
case NestingSingle:
return cty.NullVal(b.Block.ImpliedType())
case NestingGroup:
return b.Block.EmptyValue()
case NestingList:
if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() {
return cty.EmptyTupleVal
} else {
return cty.ListValEmpty(ty)
}
case NestingMap:
if ty := b.Block.ImpliedType(); ty.HasDynamicTypes() {
return cty.EmptyObjectVal
} else {
return cty.MapValEmpty(ty)
}
case NestingSet:
return cty.SetValEmpty(b.Block.ImpliedType())
default:
// Should never get here because the above is intended to be exhaustive,
// but we'll be robust and return a result nonetheless.
return cty.NullVal(cty.DynamicPseudoType)
}
}
170 changes: 170 additions & 0 deletions configs/configschema/empty_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package configschema

import (
"fmt"
"testing"

"github.com/apparentlymart/go-dump/dump"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
)

func TestBlockEmptyValue(t *testing.T) {
tests := []struct {
Schema *Block
Want cty.Value
}{
{
&Block{},
cty.EmptyObjectVal,
},
{
&Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
cty.ObjectVal(map[string]cty.Value{
"str": cty.NullVal(cty.String),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"single": {
Nesting: NestingSingle,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"single": cty.NullVal(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"group": {
Nesting: NestingGroup,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"group": cty.ObjectVal(map[string]cty.Value{
"str": cty.NullVal(cty.String),
}),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"list": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"list": cty.ListValEmpty(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"list_dynamic": {
Nesting: NestingList,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.DynamicPseudoType, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"list_dynamic": cty.EmptyTupleVal,
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"map": {
Nesting: NestingMap,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"map": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"map_dynamic": {
Nesting: NestingMap,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.DynamicPseudoType, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"map_dynamic": cty.EmptyObjectVal,
}),
},
{
&Block{
BlockTypes: map[string]*NestedBlock{
"set": {
Nesting: NestingSet,
Block: Block{
Attributes: map[string]*Attribute{
"str": {Type: cty.String, Required: true},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"set": cty.SetValEmpty(cty.Object(map[string]cty.Type{
"str": cty.String,
})),
}),
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) {
got := test.Schema.EmptyValue()
if !test.Want.RawEquals(got) {
t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), dump.Value(got), dump.Value(test.Want))
}
})
}
}
4 changes: 4 additions & 0 deletions configs/configschema/internal_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ func (b *Block) internalValidate(prefix string, err error) error {
case blockS.MinItems < 0 || blockS.MinItems > 1:
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
}
case NestingGroup:
if blockS.MinItems != 0 || blockS.MaxItems != 0 {
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name))
}
case NestingList, NestingSet:
if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 {
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting))
Expand Down
16 changes: 14 additions & 2 deletions configs/configschema/nestingmode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions configs/configschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ const (
// provided directly as an object value.
NestingSingle

// NestingGroup is similar to NestingSingle in that it calls for only a
// single instance of a given block type with no labels, but it additonally
// guarantees that its result will never be null, even if the block is
// absent, and instead the nested attributes and blocks will be treated
// as absent in that case. (Any required attributes or blocks within the
// nested block are not enforced unless the block is explicitly present
// in the configuration, so they are all effectively optional when the
// block is not present.)
//
// This is useful for the situation where a remote API has a feature that
// is always enabled but has a group of settings related to that feature
// that themselves have default values. By using NestingGroup instead of
// NestingSingle in that case, generated plans will show the block as
// present even when not present in configuration, thus allowing any
// default values within to be displayed to the user.
NestingGroup

// NestingList indicates that multiple blocks of the given type are
// permitted, with no labels, and that their corresponding objects should
// be provided in a list.
Expand Down
2 changes: 1 addition & 1 deletion configs/configschema/validate_traversal.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (b *Block) StaticValidateTraversal(traversal hcl.Traversal) tfdiags.Diagnos
}

func (b *NestedBlock) staticValidateTraversal(typeName string, traversal hcl.Traversal) tfdiags.Diagnostics {
if b.Nesting == NestingSingle {
if b.Nesting == NestingSingle || b.Nesting == NestingGroup {
// Single blocks are easy: just pass right through.
return b.Block.StaticValidateTraversal(traversal)
}
Expand Down
Loading