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:
parent
3ad720e9dc
commit
da6ac9d6cd
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue