mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
11 KiB
394 lines
11 KiB
package views
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/lang/globalref"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/providers"
|
|
"github.com/hashicorp/terraform/internal/terminal"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
// Ensure that the correct view type and in-automation settings propagate to the
|
|
// Operation view.
|
|
func TestPlanHuman_operation(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
defer done(t)
|
|
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)).Operation()
|
|
if hv, ok := v.(*OperationHuman); !ok {
|
|
t.Fatalf("unexpected return type %t", v)
|
|
} else if hv.inAutomation != true {
|
|
t.Fatalf("unexpected inAutomation value on Operation view")
|
|
}
|
|
}
|
|
|
|
// Verify that Hooks includes a UI hook
|
|
func TestPlanHuman_hooks(t *testing.T) {
|
|
streams, done := terminal.StreamsForTesting(t)
|
|
defer done(t)
|
|
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation((true)))
|
|
hooks := v.Hooks()
|
|
|
|
var uiHook *UiHook
|
|
for _, hook := range hooks {
|
|
if ch, ok := hook.(*UiHook); ok {
|
|
uiHook = ch
|
|
}
|
|
}
|
|
if uiHook == nil {
|
|
t.Fatalf("expected Hooks to include a UiHook: %#v", hooks)
|
|
}
|
|
}
|
|
|
|
// Helper functions to build a trivial test plan, to exercise the plan
|
|
// renderer.
|
|
func testPlan(t *testing.T) *plans.Plan {
|
|
t.Helper()
|
|
|
|
plannedVal := cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.UnknownVal(cty.String),
|
|
"foo": cty.StringVal("bar"),
|
|
})
|
|
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
changes := plans.NewChanges()
|
|
addr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_resource",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
|
|
|
|
changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
|
Addr: addr,
|
|
PrevRunAddr: addr,
|
|
ProviderAddr: addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
Before: priorValRaw,
|
|
After: plannedValRaw,
|
|
},
|
|
})
|
|
|
|
return &plans.Plan{
|
|
Changes: changes,
|
|
}
|
|
}
|
|
|
|
func testSchemas() *terraform.Schemas {
|
|
provider := testProvider()
|
|
return &terraform.Schemas{
|
|
Providers: map[addrs.Provider]*terraform.ProviderSchema{
|
|
addrs.NewDefaultProvider("test"): provider.ProviderSchema(),
|
|
},
|
|
}
|
|
}
|
|
|
|
func testProvider() *terraform.MockProvider {
|
|
p := new(terraform.MockProvider)
|
|
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
|
|
return providers.ReadResourceResponse{NewState: req.PriorState}
|
|
}
|
|
|
|
p.GetProviderSchemaResponse = testProviderSchema()
|
|
|
|
return p
|
|
}
|
|
|
|
func testProviderSchema() *providers.GetProviderSchemaResponse {
|
|
return &providers.GetProviderSchemaResponse{
|
|
Provider: providers.Schema{
|
|
Block: &configschema.Block{},
|
|
},
|
|
ResourceTypes: map[string]providers.Schema{
|
|
"test_resource": {
|
|
Block: &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"id": {Type: cty.String, Computed: true},
|
|
"foo": {Type: cty.String, Optional: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestFilterRefreshChange(t *testing.T) {
|
|
tests := map[string]struct {
|
|
paths []cty.Path
|
|
before, after, expected cty.Value
|
|
}{
|
|
"attr was null": {
|
|
// nested attr was null
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("attr_null_before").GetAttr("b"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("old"),
|
|
"b": cty.NullVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
|
|
// we old picked the change in b
|
|
"a": cty.StringVal("old"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
"object was null": {
|
|
// nested object attrs were null
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("obj_null_before").GetAttr("b"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_before": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"a": cty.String,
|
|
"b": cty.String,
|
|
})),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_before": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_before": cty.ObjectVal(map[string]cty.Value{
|
|
// optimally "a" would be null, but we need to take the
|
|
// entire object since it was null before.
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
"object becomes null": {
|
|
// nested object attr becoming null
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("obj_null_after").GetAttr("a"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("old"),
|
|
"b": cty.StringVal("old"),
|
|
}),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_after": cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"a": cty.String,
|
|
"b": cty.String,
|
|
})),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"obj_null_after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.NullVal(cty.String),
|
|
"b": cty.StringVal("old"),
|
|
}),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
"dynamic adding values": {
|
|
// dynamic gaining values
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.DynamicVal,
|
|
}),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
// the entire attr object is taken here because there is
|
|
// nothing to compare within the before value
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
// "other" is picked up here too this time, because we need
|
|
// to take the entire dynamic "attr" value
|
|
"other": cty.ObjectVal(map[string]cty.Value{
|
|
"o": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
"whole object becomes null": {
|
|
// whole object becomes null
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("old"),
|
|
"b": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
after: cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"attr": cty.DynamicPseudoType,
|
|
})),
|
|
// since we have a dynamic type we have to take the entire object
|
|
// because the paths may not apply between versions.
|
|
expected: cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"attr": cty.DynamicPseudoType,
|
|
})),
|
|
},
|
|
"whole object was null": {
|
|
// whole object was null
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
|
|
},
|
|
before: cty.NullVal(cty.Object(map[string]cty.Type{
|
|
"attr": cty.DynamicPseudoType,
|
|
})),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
"restructured dynamic": {
|
|
// dynamic value changing structure significantly
|
|
paths: []cty.Path{
|
|
cty.GetAttrPath("attr").GetAttr("list").IndexInt(1).GetAttr("a"),
|
|
},
|
|
before: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"list": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("old"),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
after: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
// the path does not apply at all to the new object, so we must
|
|
// take all the changes
|
|
expected: cty.ObjectVal(map[string]cty.Value{
|
|
"attr": cty.ObjectVal(map[string]cty.Value{
|
|
"after": cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("new"),
|
|
"b": cty.StringVal("new"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
}
|
|
|
|
for k, tc := range tests {
|
|
t.Run(k, func(t *testing.T) {
|
|
addr, diags := addrs.ParseAbsResourceInstanceStr("test_resource.a")
|
|
if diags != nil {
|
|
t.Fatal(diags.ErrWithWarnings())
|
|
}
|
|
|
|
change := &plans.ResourceInstanceChange{
|
|
Addr: addr,
|
|
Change: plans.Change{
|
|
Before: tc.before,
|
|
After: tc.after,
|
|
Action: plans.Update,
|
|
},
|
|
}
|
|
|
|
var contributing []globalref.ResourceAttr
|
|
for _, p := range tc.paths {
|
|
contributing = append(contributing, globalref.ResourceAttr{
|
|
Resource: addr,
|
|
Attr: p,
|
|
})
|
|
}
|
|
|
|
res := filterRefreshChange(change, contributing)
|
|
if !res.After.RawEquals(tc.expected) {
|
|
t.Errorf("\nexpected: %#v\ngot: %#v\n", tc.expected, res.After)
|
|
}
|
|
})
|
|
}
|
|
}
|