mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-15 19:22:46 -06:00
e831182c8d
Since these error messages get printed in Terraform's output and we encourage users to share them as part of bug reports, we should avoid including sensitive information in them to reduce the risk of accidental exposure.
577 lines
14 KiB
Go
577 lines
14 KiB
Go
package objchange
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/apparentlymart/go-dump/dump"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
func TestAssertPlanValid(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Schema *configschema.Block
|
|
Prior cty.Value
|
|
Config cty.Value
|
|
Planned cty.Value
|
|
WantErrs []string
|
|
}{
|
|
"all empty": {
|
|
&configschema.Block{},
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
nil,
|
|
},
|
|
"no computed, all match": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
"no computed, plan matches, no prior": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"a": cty.String,
|
|
"b": cty.List(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
"no computed, invalid change in plan": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"a": cty.String,
|
|
"b": cty.List(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("new c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
[]string{
|
|
`.b[0].c: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`,
|
|
},
|
|
},
|
|
"no computed, invalid change in plan sensitive": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"a": cty.String,
|
|
"b": cty.List(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("new c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
[]string{
|
|
`.b[0].c: sensitive planned value does not match config value`,
|
|
},
|
|
},
|
|
"no computed, diff suppression in plan": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("new c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"), // plan uses value from prior object
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
"no computed, all null": {
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"a": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.String),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.String),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.String),
|
|
"b": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
|
|
// Nested block collections are never null
|
|
"nested list, null in plan": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"b": cty.List(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.ListValEmpty(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
}))),
|
|
}),
|
|
[]string{
|
|
`.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`,
|
|
},
|
|
},
|
|
"nested set, null in plan": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"b": cty.Set(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetValEmpty(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
}))),
|
|
}),
|
|
[]string{
|
|
`.b: attribute representing a set of nested blocks must be empty to indicate no blocks, not null`,
|
|
},
|
|
},
|
|
"nested map, null in plan": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingMap,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"b": cty.Map(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.MapValEmpty(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
})),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
|
|
"c": cty.String,
|
|
}))),
|
|
}),
|
|
[]string{
|
|
`.b: attribute representing a map of nested blocks must be empty to indicate no blocks, not null`,
|
|
},
|
|
},
|
|
|
|
// We don't actually do any validation for nested set blocks, and so
|
|
// the remaining cases here are just intending to ensure we don't
|
|
// inadvertently start generating errors incorrectly in future.
|
|
"nested set, no computed, no changes": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
"nested set, no computed, invalid change in plan": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("new c value"), // matches neither prior nor config
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
"nested set, no computed, diff suppressed": {
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"b": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"c": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("new c value"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"b": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"c": cty.StringVal("c value"), // plan uses value from prior object
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
errs := AssertPlanValid(test.Schema, test.Prior, test.Config, test.Planned)
|
|
|
|
wantErrs := make(map[string]struct{})
|
|
gotErrs := make(map[string]struct{})
|
|
for _, err := range errs {
|
|
gotErrs[tfdiags.FormatError(err)] = struct{}{}
|
|
}
|
|
for _, msg := range test.WantErrs {
|
|
wantErrs[msg] = struct{}{}
|
|
}
|
|
|
|
t.Logf(
|
|
"\nprior: %sconfig: %splanned: %s",
|
|
dump.Value(test.Planned),
|
|
dump.Value(test.Config),
|
|
dump.Value(test.Planned),
|
|
)
|
|
for msg := range wantErrs {
|
|
if _, ok := gotErrs[msg]; !ok {
|
|
t.Errorf("missing expected error: %s", msg)
|
|
}
|
|
}
|
|
for msg := range gotErrs {
|
|
if _, ok := wantErrs[msg]; !ok {
|
|
t.Errorf("unexpected extra error: %s", msg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|