diff --git a/plans/objchange/objchange.go b/plans/objchange/objchange.go index ab16f68a4b..cf08dda64e 100644 --- a/plans/objchange/objchange.go +++ b/plans/objchange/objchange.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform/configs/configschema" ) -// ProposedNewObject constructs a proposed new object value by combining the +// ProposedNew constructs a proposed new object value by combining the // computed attribute values from "prior" with the configured attribute values // from "config". // @@ -24,7 +24,7 @@ import ( // heuristic based on matching non-computed attribute values and so it may // produce strange results with more "extreme" cases, such as a nested set // block where _all_ attributes are computed. -func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value { +func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { // If the config and prior are both null, return early here before // populating the prior block. The prevents non-null blocks from appearing // the proposed state value. @@ -39,10 +39,10 @@ func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty. // below by giving us one non-null level of object to pull values from. prior = AllAttributesNull(schema) } - return proposedNewObject(schema, prior, config) + return proposedNew(schema, prior, config) } -// PlannedDataResourceObject is similar to ProposedNewObject but tailored for +// PlannedDataResourceObject is similar to proposedNewBlock but tailored for // planning data resources in particular. Specifically, it replaces the values // of any Computed attributes not set in the configuration with an unknown // value, which serves as a placeholder for a value to be filled in by the @@ -51,33 +51,32 @@ func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty. // Data resources are different because the planning of them is handled // entirely within Terraform Core and not subject to customization by the // provider. This function is, in effect, producing an equivalent result to -// passing the ProposedNewObject result into a provider's PlanResourceChange +// passing the proposedNewBlock result into a provider's PlanResourceChange // function, assuming a fixed implementation of PlanResourceChange that just // fills in unknown values as needed. func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value { - // Our trick here is to run the ProposedNewObject logic with an + // Our trick here is to run the proposedNewBlock logic with an // entirely-unknown prior value. Because of cty's unknown short-circuit // behavior, any operation on prior returns another unknown, and so // unknown values propagate into all of the parts of the resulting value // that would normally be filled in by preserving the prior state. prior := cty.UnknownVal(schema.ImpliedType()) - return proposedNewObject(schema, prior, config) + return proposedNew(schema, prior, config) } -func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value { +func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { if config.IsNull() || !config.IsKnown() { // This is a weird situation, but we'll allow it anyway to free // callers from needing to specifically check for these cases. return prior } if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) { - panic("ProposedNewObject only supports object-typed values") + panic("ProposedNew only supports object-typed values") } // From this point onwards, we can assume that both values are non-null // object types, and that the config value itself is known (though it // may contain nested values that are unknown.) - newAttrs := map[string]cty.Value{} for name, attr := range schema.Attributes { priorV := prior.GetAttr(name) @@ -118,167 +117,171 @@ func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty. for name, blockType := range schema.BlockTypes { priorV := prior.GetAttr(name) configV := config.GetAttr(name) - var newV cty.Value - switch blockType.Nesting { - - case configschema.NestingSingle, configschema.NestingGroup: - newV = ProposedNewObject(&blockType.Block, priorV, configV) - - case configschema.NestingList: - // Nested blocks are correlated by index. - configVLen := 0 - if configV.IsKnown() && !configV.IsNull() { - configVLen = configV.LengthInt() - } - if configVLen > 0 { - newVals := make([]cty.Value, 0, configVLen) - for it := configV.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals = append(newVals, configEV) - continue - } - priorEV := priorV.Index(idx) - - newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) - newVals = append(newVals, newEV) - } - // Despite the name, a NestingList might also be a tuple, if - // its nested schema contains dynamically-typed attributes. - if configV.Type().IsTupleType() { - newV = cty.TupleVal(newVals) - } else { - newV = cty.ListVal(newVals) - } - } else { - // Despite the name, a NestingList might also be a tuple, if - // its nested schema contains dynamically-typed attributes. - if configV.Type().IsTupleType() { - newV = cty.EmptyTupleVal - } else { - newV = cty.ListValEmpty(blockType.ImpliedType()) - } - } - - case configschema.NestingMap: - // Despite the name, a NestingMap may produce either a map or - // object value, depending on whether the nested schema contains - // dynamically-typed attributes. - if configV.Type().IsObjectType() { - // Nested blocks are correlated by key. - configVLen := 0 - if configV.IsKnown() && !configV.IsNull() { - configVLen = configV.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - atys := configV.Type().AttributeTypes() - for name := range atys { - configEV := configV.GetAttr(name) - if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[name] = configEV - continue - } - priorEV := priorV.GetAttr(name) - - newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) - newVals[name] = newEV - } - // Although we call the nesting mode "map", we actually use - // object values so that elements might have different types - // in case of dynamically-typed attributes. - newV = cty.ObjectVal(newVals) - } else { - newV = cty.EmptyObjectVal - } - } else { - configVLen := 0 - if configV.IsKnown() && !configV.IsNull() { - configVLen = configV.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - for it := configV.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - k := idx.AsString() - if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[k] = configEV - continue - } - priorEV := priorV.Index(idx) - - newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) - newVals[k] = newEV - } - newV = cty.MapVal(newVals) - } else { - newV = cty.MapValEmpty(blockType.ImpliedType()) - } - } - - case configschema.NestingSet: - if !configV.Type().IsSetType() { - panic("configschema.NestingSet value is not a set as expected") - } - - // Nested blocks are correlated by comparing the element values - // after eliminating all of the computed attributes. In practice, - // this means that any config change produces an entirely new - // nested object, and we only propagate prior computed values - // if the non-computed attribute values are identical. - var cmpVals [][2]cty.Value - if priorV.IsKnown() && !priorV.IsNull() { - cmpVals = setElementCompareValues(&blockType.Block, priorV, false) - } - configVLen := 0 - if configV.IsKnown() && !configV.IsNull() { - configVLen = configV.LengthInt() - } - if configVLen > 0 { - used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value - newVals := make([]cty.Value, 0, configVLen) - for it := configV.ElementIterator(); it.Next(); { - _, configEV := it.Element() - var priorEV cty.Value - for i, cmp := range cmpVals { - if used[i] { - continue - } - if cmp[1].RawEquals(configEV) { - priorEV = cmp[0] - used[i] = true // we can't use this value on a future iteration - break - } - } - if priorEV == cty.NilVal { - priorEV = cty.NullVal(blockType.ImpliedType()) - } - - newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) - newVals = append(newVals, newEV) - } - newV = cty.SetVal(newVals) - } else { - newV = cty.SetValEmpty(blockType.Block.ImpliedType()) - } - - default: - // Should never happen, since the above cases are comprehensive. - panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) - } - - newAttrs[name] = newV + newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV) } return cty.ObjectVal(newAttrs) } +func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value { + var newV cty.Value + + switch schema.Nesting { + + case configschema.NestingSingle, configschema.NestingGroup: + newV = ProposedNew(&schema.Block, prior, config) + + case configschema.NestingList: + // Nested blocks are correlated by index. + configVLen := 0 + if config.IsKnown() && !config.IsNull() { + configVLen = config.LengthInt() + } + if configVLen > 0 { + newVals := make([]cty.Value, 0, configVLen) + for it := config.ElementIterator(); it.Next(); { + idx, configEV := it.Element() + if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { + // If there is no corresponding prior element then + // we just take the config value as-is. + newVals = append(newVals, configEV) + continue + } + priorEV := prior.Index(idx) + + newEV := ProposedNew(&schema.Block, priorEV, configEV) + newVals = append(newVals, newEV) + } + // Despite the name, a NestingList might also be a tuple, if + // its nested schema contains dynamically-typed attributes. + if config.Type().IsTupleType() { + newV = cty.TupleVal(newVals) + } else { + newV = cty.ListVal(newVals) + } + } else { + // Despite the name, a NestingList might also be a tuple, if + // its nested schema contains dynamically-typed attributes. + if config.Type().IsTupleType() { + newV = cty.EmptyTupleVal + } else { + newV = cty.ListValEmpty(schema.ImpliedType()) + } + } + + case configschema.NestingMap: + // Despite the name, a NestingMap may produce either a map or + // object value, depending on whether the nested schema contains + // dynamically-typed attributes. + if config.Type().IsObjectType() { + // Nested blocks are correlated by key. + configVLen := 0 + if config.IsKnown() && !config.IsNull() { + configVLen = config.LengthInt() + } + if configVLen > 0 { + newVals := make(map[string]cty.Value, configVLen) + atys := config.Type().AttributeTypes() + for name := range atys { + configEV := config.GetAttr(name) + if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { + // If there is no corresponding prior element then + // we just take the config value as-is. + newVals[name] = configEV + continue + } + priorEV := prior.GetAttr(name) + + newEV := ProposedNew(&schema.Block, priorEV, configEV) + newVals[name] = newEV + } + // Although we call the nesting mode "map", we actually use + // object values so that elements might have different types + // in case of dynamically-typed attributes. + newV = cty.ObjectVal(newVals) + } else { + newV = cty.EmptyObjectVal + } + } else { + configVLen := 0 + if config.IsKnown() && !config.IsNull() { + configVLen = config.LengthInt() + } + if configVLen > 0 { + newVals := make(map[string]cty.Value, configVLen) + for it := config.ElementIterator(); it.Next(); { + idx, configEV := it.Element() + k := idx.AsString() + if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { + // If there is no corresponding prior element then + // we just take the config value as-is. + newVals[k] = configEV + continue + } + priorEV := prior.Index(idx) + + newEV := ProposedNew(&schema.Block, priorEV, configEV) + newVals[k] = newEV + } + newV = cty.MapVal(newVals) + } else { + newV = cty.MapValEmpty(schema.ImpliedType()) + } + } + + case configschema.NestingSet: + if !config.Type().IsSetType() { + panic("configschema.NestingSet value is not a set as expected") + } + + // Nested blocks are correlated by comparing the element values + // after eliminating all of the computed attributes. In practice, + // this means that any config change produces an entirely new + // nested object, and we only propagate prior computed values + // if the non-computed attribute values are identical. + var cmpVals [][2]cty.Value + if prior.IsKnown() && !prior.IsNull() { + cmpVals = setElementCompareValues(&schema.Block, prior, false) + } + configVLen := 0 + if config.IsKnown() && !config.IsNull() { + configVLen = config.LengthInt() + } + if configVLen > 0 { + used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value + newVals := make([]cty.Value, 0, configVLen) + for it := config.ElementIterator(); it.Next(); { + _, configEV := it.Element() + var priorEV cty.Value + for i, cmp := range cmpVals { + if used[i] { + continue + } + if cmp[1].RawEquals(configEV) { + priorEV = cmp[0] + used[i] = true // we can't use this value on a future iteration + break + } + } + if priorEV == cty.NilVal { + priorEV = cty.NullVal(schema.ImpliedType()) + } + + newEV := ProposedNew(&schema.Block, priorEV, configEV) + newVals = append(newVals, newEV) + } + newV = cty.SetVal(newVals) + } else { + newV = cty.SetValEmpty(schema.Block.ImpliedType()) + } + + default: + // Should never happen, since the above cases are comprehensive. + panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting)) + } + return newV +} + // setElementCompareValues takes a known, non-null value of a cty.Set type and // returns a table -- constructed of two-element arrays -- that maps original // set element values to corresponding values that have all of the computed @@ -290,7 +293,7 @@ func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty. // value and the one-indexed element is the corresponding "compare value". // // This is intended to help correlate prior elements with configured elements -// in ProposedNewObject. The result is a heuristic rather than an exact science, +// in proposedNewBlock. The result is a heuristic rather than an exact science, // since e.g. two separate elements may reduce to the same value through this // process. The caller must therefore be ready to deal with duplicates. func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value { diff --git a/plans/objchange/objchange_test.go b/plans/objchange/objchange_test.go index 065e4add88..9a523609d3 100644 --- a/plans/objchange/objchange_test.go +++ b/plans/objchange/objchange_test.go @@ -33,6 +33,18 @@ func TestProposedNewObject(t *testing.T) { Type: cty.String, Computed: true, }, + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Computed: true, + }, }, BlockTypes: map[string]*configschema.NestedBlock{ "baz": { @@ -57,6 +69,9 @@ func TestProposedNewObject(t *testing.T) { cty.NullVal(cty.DynamicPseudoType), cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("hello"), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + })), "bar": cty.NullVal(cty.String), "baz": cty.ObjectVal(map[string]cty.Value{ "boz": cty.StringVal("world"), @@ -76,6 +91,9 @@ func TestProposedNewObject(t *testing.T) { // usually changes them to "unknown" during PlanResourceChange, // to indicate that the value will be decided during apply. "bar": cty.NullVal(cty.String), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + })), "baz": cty.ObjectVal(map[string]cty.Value{ "boz": cty.StringVal("world"), @@ -90,6 +108,18 @@ func TestProposedNewObject(t *testing.T) { Type: cty.String, Optional: true, }, + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Computed: true, + }, }, BlockTypes: map[string]*configschema.NestedBlock{ "baz": { @@ -109,14 +139,20 @@ func TestProposedNewObject(t *testing.T) { cty.NullVal(cty.DynamicPseudoType), cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + })), "baz": cty.NullVal(cty.Object(map[string]cty.Type{ "boz": cty.String, })), }), - // The baz block does not exist in the config, and therefore - // shouldn't be planned. + // The bloop attribue and baz block does not exist in the config, + // and therefore shouldn't be planned. cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + })), "baz": cty.NullVal(cty.Object(map[string]cty.Type{ "boz": cty.String, })), @@ -141,6 +177,21 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Computed: true, + Optional: true, + }, + }, }, cty.NullVal(cty.DynamicPseudoType), cty.ObjectVal(map[string]cty.Value{ @@ -149,6 +200,11 @@ func TestProposedNewObject(t *testing.T) { "boz": cty.StringVal("world"), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "baz": cty.SetVal([]cty.Value{ @@ -156,6 +212,11 @@ func TestProposedNewObject(t *testing.T) { "boz": cty.StringVal("world"), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + }), + }), }), }, "prior attributes": { @@ -179,6 +240,18 @@ func TestProposedNewObject(t *testing.T) { Optional: true, Computed: true, }, + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, }, }, cty.ObjectVal(map[string]cty.Value{ @@ -186,18 +259,27 @@ func TestProposedNewObject(t *testing.T) { "bar": cty.StringVal("petit dejeuner"), "baz": cty.StringVal("grande dejeuner"), "boz": cty.StringVal("a la monde"), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("hello"), "bar": cty.NullVal(cty.String), "baz": cty.NullVal(cty.String), "boz": cty.StringVal("world"), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bleep"), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("hello"), "bar": cty.StringVal("petit dejeuner"), "baz": cty.StringVal("grande dejeuner"), "boz": cty.StringVal("world"), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bleep"), + }), }), }, "prior nested single": { @@ -221,24 +303,54 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("beep"), "baz": cty.StringVal("boop"), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("bap"), "baz": cty.NullVal(cty.String), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.StringVal("beep"), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("bap"), "baz": cty.StringVal("boop"), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.StringVal("beep"), + }), }), }, "prior nested list": { @@ -262,6 +374,20 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ @@ -270,6 +396,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.StringVal("boop"), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("baz"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ @@ -282,6 +416,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("baz"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ListVal([]cty.Value{ @@ -294,6 +436,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("baz"), + }), + }), }), }, "prior nested list with dynamic": { @@ -317,6 +467,24 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.DynamicPseudoType, + Required: true, + }, + "blub": { + Type: cty.DynamicPseudoType, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.TupleVal([]cty.Value{ @@ -325,6 +493,16 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.StringVal("boop"), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + "blub": cty.StringVal("glub"), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("baz"), + "blub": cty.NullVal(cty.String), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.TupleVal([]cty.Value{ @@ -337,6 +515,12 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + "blub": cty.NullVal(cty.String), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.TupleVal([]cty.Value{ @@ -349,6 +533,12 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("bar"), + "blub": cty.NullVal(cty.String), + }), + }), }), }, "prior nested map": { @@ -372,6 +562,20 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ @@ -384,6 +588,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.StringVal("boot"), }), }), + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ @@ -396,6 +608,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.MapVal(map[string]cty.Value{ @@ -408,6 +628,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + }), + }), }), }, "prior nested map with dynamic": { @@ -431,6 +659,20 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.DynamicPseudoType, + Required: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ @@ -443,6 +685,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.ListVal([]cty.Value{cty.StringVal("boot")}), }), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.NumberIntVal(13), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ @@ -455,6 +705,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.List(cty.String)), }), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blep"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.NumberIntVal(13), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ @@ -467,6 +725,14 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.List(cty.String)), }), }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blep"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.NumberIntVal(13), + }), + }), }), }, "prior nested set": { @@ -492,6 +758,24 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.ObjectVal(map[string]cty.Value{ "foo": cty.SetVal([]cty.Value{ @@ -504,6 +788,16 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.StringVal("boot"), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glubglub"), + "bleep": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glubglub"), + "bleep": cty.StringVal("beep"), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.SetVal([]cty.Value{ @@ -516,6 +810,16 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glubglub"), + "bleep": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "foo": cty.SetVal([]cty.Value{ @@ -528,6 +832,16 @@ func TestProposedNewObject(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glubglub"), + "bleep": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), }), }, "sets differing only by unknown": { @@ -546,6 +860,20 @@ func TestProposedNewObject(t *testing.T) { }, }, }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, + }, }, cty.NullVal(cty.DynamicPseudoType), cty.ObjectVal(map[string]cty.Value{ @@ -557,6 +885,14 @@ func TestProposedNewObject(t *testing.T) { "optional": cty.UnknownVal(cty.String), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.UnknownVal(cty.String), + }), + }), }), cty.ObjectVal(map[string]cty.Value{ "multi": cty.SetVal([]cty.Value{ @@ -570,6 +906,14 @@ func TestProposedNewObject(t *testing.T) { "optional": cty.UnknownVal(cty.String), }), }), + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.UnknownVal(cty.String), + }), + }), }), }, "nested list in set": { @@ -858,7 +1202,7 @@ func TestProposedNewObject(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - got := ProposedNewObject(test.Schema, test.Prior, test.Config) + got := ProposedNew(test.Schema, test.Prior, test.Config) if !got.RawEquals(test.Want) { t.Errorf("wrong result\ngot: %swant: %s", dump.Value(got), dump.Value(test.Want)) } diff --git a/plans/objchange/plan_valid.go b/plans/objchange/plan_valid.go index 69acb8979a..6a91428f83 100644 --- a/plans/objchange/plan_valid.go +++ b/plans/objchange/plan_valid.go @@ -53,18 +53,10 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat impTy := schema.ImpliedType() - for name, attrS := range schema.Attributes { - plannedV := plannedState.GetAttr(name) - configV := config.GetAttr(name) - priorV := cty.NullVal(attrS.Type) - if !priorState.IsNull() { - priorV = priorState.GetAttr(name) - } + // verify attributes + moreErrs := assertPlannedAttrsValid(schema.Attributes, priorState, config, plannedState, path) + errs = append(errs, moreErrs...) - path := append(path, cty.GetAttrStep{Name: name}) - moreErrs := assertPlannedValueValid(attrS, priorV, configV, plannedV, path) - errs = append(errs, moreErrs...) - } for name, blockS := range schema.BlockTypes { path := append(path, cty.GetAttrStep{Name: name}) plannedV := plannedState.GetAttr(name) @@ -229,13 +221,34 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat return errs } +func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error { + var errs []error + for name, attrS := range schema { + moreErrs := assertPlannedAttrValid(name, attrS, priorState, config, plannedState, path) + errs = append(errs, moreErrs...) + } + return errs +} + +func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error { + plannedV := plannedState.GetAttr(name) + configV := config.GetAttr(name) + priorV := cty.NullVal(attrS.Type) + if !priorState.IsNull() { + priorV = priorState.GetAttr(name) + } + path = append(path, cty.GetAttrStep{Name: name}) + + return assertPlannedValueValid(attrS, priorV, configV, plannedV, path) +} + func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error { var errs []error if plannedV.RawEquals(configV) { // This is the easy path: provider didn't change anything at all. return errs } - if plannedV.RawEquals(priorV) && !priorV.IsNull() { + if plannedV.RawEquals(priorV) && !priorV.IsNull() && !configV.IsNull() { // Also pretty easy: there is a prior value and the provider has // returned it unchanged. This indicates that configV and plannedV // are functionally equivalent and so the provider wishes to disregard @@ -248,6 +261,11 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla return errs } + // If this attribute has a NestedType, validate the nested object + if attrS.NestedType != nil { + return assertPlannedObjectValid(attrS.NestedType, priorV, configV, plannedV, path) + } + // If none of the above conditions match, the provider has made an invalid // change to this attribute. if priorV.IsNull() { @@ -265,3 +283,151 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla } return errs } + +func assertPlannedObjectValid(schema *configschema.Object, prior, config, planned cty.Value, path cty.Path) []error { + var errs []error + + if planned.IsNull() && !config.IsNull() { + errs = append(errs, path.NewErrorf("planned for absense but config wants existence")) + return errs + } + if config.IsNull() && !planned.IsNull() { + errs = append(errs, path.NewErrorf("planned for existence but config wants absense")) + return errs + } + if planned.IsNull() { + // No further checks possible if the planned value is null + return errs + } + + switch schema.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + moreErrs := assertPlannedAttrsValid(schema.Attributes, prior, config, planned, path) + errs = append(errs, moreErrs...) + + case configschema.NestingList: + // A NestingList might either be a list or a tuple, depending on + // whether there are dynamically-typed attributes inside. However, + // both support a similar-enough API that we can treat them the + // same for our purposes here. + + plannedL := planned.LengthInt() + configL := config.LengthInt() + if plannedL != configL { + errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) + return errs + } + for it := planned.ElementIterator(); it.Next(); { + idx, plannedEV := it.Element() + path := append(path, cty.IndexStep{Key: idx}) + if !plannedEV.IsKnown() { + errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) + continue + } + if !config.HasIndex(idx).True() { + continue // should never happen since we checked the lengths above + } + configEV := config.Index(idx) + priorEV := cty.NullVal(schema.ImpliedType()) + if !prior.IsNull() && prior.HasIndex(idx).True() { + priorEV = prior.Index(idx) + } + + moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path) + errs = append(errs, moreErrs...) + } + + case configschema.NestingMap: + // A NestingMap might either be a map or an object, depending on + // whether there are dynamically-typed attributes inside, but + // that's decided statically and so all values will have the same + // kind. + if planned.Type().IsObjectType() { + plannedAtys := planned.Type().AttributeTypes() + configAtys := config.Type().AttributeTypes() + for k := range plannedAtys { + if _, ok := configAtys[k]; !ok { + errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) + continue + } + path := append(path, cty.GetAttrStep{Name: k}) + + plannedEV := planned.GetAttr(k) + if !plannedEV.IsKnown() { + errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) + continue + } + configEV := config.GetAttr(k) + priorEV := cty.NullVal(schema.ImpliedType()) + if !prior.IsNull() && prior.Type().HasAttribute(k) { + priorEV = prior.GetAttr(k) + } + moreErrs := assertPlannedAttrsValid(schema.Attributes, priorEV, configEV, plannedEV, path) + errs = append(errs, moreErrs...) + } + for k := range configAtys { + if _, ok := plannedAtys[k]; !ok { + errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", k)) + continue + } + } + } else { + plannedL := planned.LengthInt() + configL := config.LengthInt() + if plannedL != configL { + errs = append(errs, path.NewErrorf("block count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) + return errs + } + for it := planned.ElementIterator(); it.Next(); { + idx, plannedEV := it.Element() + path := append(path, cty.IndexStep{Key: idx}) + if !plannedEV.IsKnown() { + errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) + continue + } + k := idx.AsString() + if !config.HasIndex(idx).True() { + errs = append(errs, path.NewErrorf("block key %q from plan is not present in config", k)) + continue + } + configEV := config.Index(idx) + priorEV := cty.NullVal(schema.ImpliedType()) + if !prior.IsNull() && prior.HasIndex(idx).True() { + priorEV = prior.Index(idx) + } + moreErrs := assertPlannedObjectValid(schema, priorEV, configEV, plannedEV, path) + errs = append(errs, moreErrs...) + } + for it := config.ElementIterator(); it.Next(); { + idx, _ := it.Element() + if !planned.HasIndex(idx).True() { + errs = append(errs, path.NewErrorf("block key %q from config is not present in plan", idx.AsString())) + continue + } + } + } + + case configschema.NestingSet: + // Because set elements have no identifier with which to correlate + // them, we can't robustly validate the plan for a nested block + // backed by a set, and so unfortunately we need to just trust the + // provider to do the right thing. :( + // + // (In principle we could correlate elements by matching the + // subset of attributes explicitly set in config, except for the + // special diff suppression rule which allows for there to be a + // planned value that is constructed by mixing part of a prior + // value with part of a config value, creating an entirely new + // element that is not present in either prior nor config.) + for it := planned.ElementIterator(); it.Next(); { + idx, plannedEV := it.Element() + path := append(path, cty.IndexStep{Key: idx}) + if !plannedEV.IsKnown() { + errs = append(errs, path.NewErrorf("element representing nested block must not be unknown itself; set nested attribute values to unknown instead")) + continue + } + } + } + + return errs +} diff --git a/plans/objchange/plan_valid_test.go b/plans/objchange/plan_valid_test.go index cc41d694a3..1abc156773 100644 --- a/plans/objchange/plan_valid_test.go +++ b/plans/objchange/plan_valid_test.go @@ -579,6 +579,545 @@ func TestAssertPlanValid(t *testing.T) { }), nil, }, + + // Attributes with NestedTypes + "NestedType attr, no computed, all match": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), + }), + }), + }), + nil, + }, + "NestedType attr, no computed, plan matches, no prior": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + })), + })), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("c value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("c value"), + }), + }), + }), + nil, + }, + "NestedType, no computed, invalid change in plan": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + })), + })), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("c value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("new c value"), + }), + }), + }), + []string{ + `.a[0].b: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`, + }, + }, + "NestedType attr, no computed, invalid change in plan sensitive": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + Sensitive: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "a": cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + })), + })), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("new b value"), + }), + }), + }), + []string{ + `.a[0].b: sensitive planned value does not match config value`, + }, + }, + "NestedType attr, no computed, diff suppression in plan": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("new b value"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "b": cty.StringVal("b value"), // plan uses value from prior object + }), + }), + }), + nil, + }, + "NestedType attr, no computed, all null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.DynamicPseudoType), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.DynamicPseudoType), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.DynamicPseudoType), + }), + nil, + }, + "NestedType attr, no computed, all zero value": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "b": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + }))), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + }))), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "b": cty.String, + }))), + }), + nil, + }, + "NestedType NestingSet attribute to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "blop": cty.String, + }))), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "blop": cty.String, + }))), + }), + nil, + }, + "NestedType deep nested optional set attribute to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bleep": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blome": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blome": cty.StringVal("ok"), + }), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Set( + cty.Object(map[string]cty.Type{ + "blome": cty.String, + }), + )), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.List( + cty.Object(map[string]cty.Type{ + "blome": cty.String, + }), + )), + }), + }), + }), + nil, + }, + "NestedType deep nested set": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bleep": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blome": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blome": cty.StringVal("ok"), + }), + }), + }), + }), + }), + // Note: bloop is null in the config + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Set( + cty.Object(map[string]cty.Type{ + "blome": cty.String, + }), + )), + }), + }), + }), + // provider sends back the prior value, not matching the config + cty.ObjectVal(map[string]cty.Value{ + "bleep": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blome": cty.StringVal("ok"), + }), + }), + }), + }), + }), + nil, // we cannot validate individual set elements, and trust the provider's response + }, + "NestedType nested computed list attribute": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Optional: true, + }, + }, + }, + Computed: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "blop": cty.String, + }))), + }), + + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + nil, + }, + "NestedType nested list attribute to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "blop": cty.String, + }))), + }), + + // provider returned the old value + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + []string{".bloop: planned for existence but config wants absense"}, + }, + "NestedType nested set attribute to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "blop": cty.String, + }))), + }), + // provider returned the old value + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("ok"), + }), + }), + }), + []string{".bloop: planned for existence but config wants absense"}, + }, } for name, test := range tests { diff --git a/terraform/node_resource_abstract_instance.go b/terraform/node_resource_abstract_instance.go index c73f600500..5bc1a44681 100644 --- a/terraform/node_resource_abstract_instance.go +++ b/terraform/node_resource_abstract_instance.go @@ -649,7 +649,7 @@ func (n *NodeAbstractResourceInstance) plan( return plan, state, diags } - proposedNewVal := objchange.ProposedNewObject(schema, unmarkedPriorVal, configValIgnored) + proposedNewVal := objchange.ProposedNew(schema, unmarkedPriorVal, configValIgnored) // Call pre-diff hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { @@ -861,7 +861,7 @@ func (n *NodeAbstractResourceInstance) plan( } // create a new proposed value from the null state and the config - proposedNewVal = objchange.ProposedNewObject(schema, nullPriorVal, unmarkedConfigVal) + proposedNewVal = objchange.ProposedNew(schema, nullPriorVal, unmarkedConfigVal) resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ TypeName: n.Addr.Resource.Resource.Type, @@ -1423,7 +1423,7 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, currentSt // While we don't propose planned changes for data sources, we can // generate a proposed value for comparison to ensure the data source // is returning a result following the rules of the provider contract. - proposedVal := objchange.ProposedNewObject(schema, unmarkedPriorVal, unmarkedConfigVal) + proposedVal := objchange.ProposedNew(schema, unmarkedPriorVal, unmarkedConfigVal) if errs := objchange.AssertObjectCompatible(schema, proposedVal, newVal); len(errs) > 0 { // Resources have the LegacyTypeSystem field to signal when they are // using an SDK which may not produce precise values. While data