plans/objchange: add handling of NestedTypes inside attributes

- rename ProposedNewObject to ProposedNew:
Now that there is an actual configschema.Object it will be clearer if
the function names match the type the act upon.

- extract attribute-handling logic from assertPlanValid and extend
A new function, assertPlannedAttrsValid, takes the existing
functionality and extends it to validate attributes with NestedTypes.
The NestedType-specific handling is in assertPlannedObjectValid, which
is very similar to the block-handling logic, except that nulls are a
valid plan (an attribute can be null, but not a block).
This commit is contained in:
Kristin Laemmert 2021-02-05 13:41:06 -05:00
parent 3ad720e9dc
commit da6ac9d6cd
5 changed files with 1237 additions and 185 deletions

View File

@ -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 {

View File

@ -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))
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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