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.
460 lines
15 KiB
460 lines
15 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package stackaddrs
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
func TestParseRemovedFrom_Stacks(t *testing.T) {
|
|
tcs := []struct {
|
|
from string
|
|
want StackInstance
|
|
vars map[string]cty.Value
|
|
parseDiags func() tfdiags.Diagnostics
|
|
addrDiags func() tfdiags.Diagnostics
|
|
}{
|
|
{
|
|
from: "stack.stack_name",
|
|
want: mustStackInstance(t, "stack.stack_name"),
|
|
},
|
|
{
|
|
from: "stack.parent.stack.child",
|
|
want: mustStackInstance(t, "stack.parent.stack.child"),
|
|
},
|
|
{
|
|
from: "stack.parent[each.key].stack.child",
|
|
want: mustStackInstance(t, "stack.parent[\"parent\"].stack.child"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.StringVal("parent"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.parent.stack.child[each.key]",
|
|
want: mustStackInstance(t, "stack.parent.stack.child[\"child\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.StringVal("child"),
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range tcs {
|
|
t.Run(tc.from, func(t *testing.T) {
|
|
expr := mustExpr(t, tc.from)
|
|
from, parseDiags := ParseRemovedFrom(expr)
|
|
|
|
var wantParseDiags tfdiags.Diagnostics
|
|
if tc.parseDiags != nil {
|
|
wantParseDiags = tc.parseDiags()
|
|
}
|
|
tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags)
|
|
|
|
if from.Component != nil {
|
|
t.Fatal("from.Component should be empty")
|
|
}
|
|
|
|
configAddress := from.TargetStack()
|
|
instanceAddress, addrDiags := from.TargetStackInstance(&hcl.EvalContext{
|
|
Variables: tc.vars,
|
|
}, RootStackInstance)
|
|
var wantAddrDiags tfdiags.Diagnostics
|
|
if tc.addrDiags != nil {
|
|
wantAddrDiags = tc.addrDiags()
|
|
}
|
|
tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags)
|
|
|
|
wantConfigAddress := tc.want.ConfigAddr()
|
|
if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 {
|
|
t.Errorf("wrong config address; %s", diff)
|
|
}
|
|
if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 {
|
|
t.Errorf("wrong instance address: %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseRemovedFrom_Components(t *testing.T) {
|
|
tcs := []struct {
|
|
from string
|
|
want AbsComponentInstance
|
|
vars map[string]cty.Value
|
|
parseDiags func() tfdiags.Diagnostics
|
|
addrDiags func() tfdiags.Diagnostics
|
|
}{
|
|
{
|
|
from: "component.component_name",
|
|
want: mustAbsComponentInstance(t, "component.component_name"),
|
|
},
|
|
{
|
|
from: "component.component_name[0]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[0]"),
|
|
},
|
|
{
|
|
from: "component.component_name[\"key\"]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"),
|
|
},
|
|
{
|
|
from: "component.component_name[each.key]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.StringVal("key"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.value.attribute]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.ObjectVal(map[string]cty.Value{
|
|
"attribute": cty.StringVal("attribute"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.value[\"key\"]]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.StringVal("key"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.value[\"key\"].attribute]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.ObjectVal(map[string]cty.Value{
|
|
"attribute": cty.StringVal("attribute"),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.value[local.key]]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.StringVal("key"),
|
|
}),
|
|
}),
|
|
"local": cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.StringVal("key"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.value[local.key].attribute]",
|
|
want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.MapVal(map[string]cty.Value{
|
|
"key": cty.ObjectVal(map[string]cty.Value{
|
|
"attribute": cty.StringVal("attribute"),
|
|
}),
|
|
}),
|
|
}),
|
|
"local": cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.StringVal("key"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.stack_name.component.component_name",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name"),
|
|
},
|
|
{
|
|
from: "stack.parent.stack.child.component.component_name",
|
|
want: mustAbsComponentInstance(t, "stack.parent.stack.child.component.component_name"),
|
|
},
|
|
{
|
|
from: "stack.stack_name[\"stack\"].component.component_name",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"),
|
|
},
|
|
{
|
|
from: "stack.stack_name.component.component_name[\"component\"]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"),
|
|
},
|
|
{
|
|
from: "stack.stack_name[\"stack\"].component.component_name[\"component\"]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"),
|
|
},
|
|
{
|
|
from: "stack.stack_name.component.component_name[each.value]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("component"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.stack_name[\"stack\"].component.component_name[each.value]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("component"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.stack_name[each.value].component.component_name",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("stack"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.stack_name[each.value].component.component_name[\"component\"]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.StringVal("stack"),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.stack_name[each.value[\"stack\"]].component.component_name[each.value[\"component\"]]",
|
|
want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.ObjectVal(map[string]cty.Value{
|
|
"stack": cty.StringVal("stack"),
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name",
|
|
want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.ObjectVal(map[string]cty.Value{
|
|
"parent": cty.StringVal("parent"),
|
|
"child": cty.StringVal("child"),
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[\"component\"]",
|
|
want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.ObjectVal(map[string]cty.Value{
|
|
"parent": cty.StringVal("parent"),
|
|
"child": cty.StringVal("child"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[each.value[\"component\"]]",
|
|
want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"),
|
|
vars: map[string]cty.Value{
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"value": cty.ObjectVal(map[string]cty.Value{
|
|
"parent": cty.StringVal("parent"),
|
|
"child": cty.StringVal("child"),
|
|
"component": cty.StringVal("component"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name.attribute_key",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 39, Byte: 38},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[0].attribute_key",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 42, Byte: 41},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[\"key\"].attribute_key",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 46, Byte: 45},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name[each.key].attribute_key",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 49, Byte: 48},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
{
|
|
from: "component.component_name.attribute_key[0]",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 42, Byte: 41},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
{
|
|
from: "component[0].component_name",
|
|
parseDiags: func() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid 'from' attribute",
|
|
Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.",
|
|
Subject: &hcl.Range{
|
|
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
|
|
End: hcl.Pos{Line: 1, Column: 28, Byte: 27},
|
|
},
|
|
})
|
|
return diags
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.from, func(t *testing.T) {
|
|
expr := mustExpr(t, tc.from)
|
|
from, parseDiags := ParseRemovedFrom(expr)
|
|
|
|
var wantParseDiags tfdiags.Diagnostics
|
|
if tc.parseDiags != nil {
|
|
wantParseDiags = tc.parseDiags()
|
|
}
|
|
tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags)
|
|
if len(wantParseDiags) > 0 {
|
|
return // don't do the rest of the test if we expected to fail here
|
|
}
|
|
|
|
if from.Component == nil {
|
|
t.Fatal("from.Component should not be empty")
|
|
}
|
|
|
|
configAddress := from.TargetConfigComponent()
|
|
instanceAddress, addrDiags := from.TargetAbsComponentInstance(&hcl.EvalContext{
|
|
Variables: tc.vars,
|
|
}, RootStackInstance)
|
|
var wantAddrDiags tfdiags.Diagnostics
|
|
if tc.addrDiags != nil {
|
|
wantAddrDiags = tc.addrDiags()
|
|
}
|
|
tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags)
|
|
|
|
wantConfigAddress := ConfigComponent{
|
|
Stack: tc.want.Stack.ConfigAddr(),
|
|
Item: tc.want.Item.Component,
|
|
}
|
|
if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 {
|
|
t.Errorf("wrong config address; %s", diff)
|
|
}
|
|
if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 {
|
|
t.Errorf("wrong instance address: %s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func mustStackInstance(t *testing.T, str string) StackInstance {
|
|
traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(str), "", hcl.InitialPos)
|
|
if len(hclDiags) > 0 {
|
|
t.Fatal(hclDiags.Error())
|
|
}
|
|
inst, rest, diags := parseInStackInstancePrefix(traversal)
|
|
if len(diags) > 0 {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
|
|
if len(rest) > 0 {
|
|
t.Fatal("invalid stack instance, has extra steps")
|
|
}
|
|
return inst
|
|
}
|
|
|
|
func mustAbsComponentInstance(t *testing.T, str string) AbsComponentInstance {
|
|
inst, diags := ParseAbsComponentInstanceStr(str)
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Err())
|
|
}
|
|
return inst
|
|
}
|
|
|
|
func mustExpr(t *testing.T, expr string) hcl.Expression {
|
|
ret, diags := hclsyntax.ParseExpression([]byte(expr), "", hcl.InitialPos)
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Error())
|
|
}
|
|
return ret
|
|
}
|