Skip to content

Commit f2c16b7

Browse files
authored
Merge pull request hashicorp#10807 from hashicorp/b-alias-validate
config: smarter provider alias usage validation
2 parents dc052c5 + 0c30cae commit f2c16b7

9 files changed

Lines changed: 184 additions & 16 deletions

File tree

config/config.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -553,15 +553,6 @@ func (c *Config) Validate() error {
553553
// Validate DependsOn
554554
errs = append(errs, c.validateDependsOn(n, r.DependsOn, resources, modules)...)
555555

556-
// Verify provider points to a provider that is configured
557-
if r.Provider != "" {
558-
if _, ok := providerSet[r.Provider]; !ok {
559-
errs = append(errs, fmt.Errorf(
560-
"%s: resource depends on non-configured provider '%s'",
561-
n, r.Provider))
562-
}
563-
}
564-
565556
// Verify provisioners don't contain any splats
566557
for _, p := range r.Provisioners {
567558
// This validation checks that there are now splat variables

config/config_test.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -456,13 +456,6 @@ func TestConfigValidate_providerMultiRefGood(t *testing.T) {
456456
}
457457
}
458458

459-
func TestConfigValidate_providerMultiRefBad(t *testing.T) {
460-
c := testConfig(t, "validate-provider-multi-ref-bad")
461-
if err := c.Validate(); err == nil {
462-
t.Fatal("should not be valid")
463-
}
464-
}
465-
466459
func TestConfigValidate_provConnSplatOther(t *testing.T) {
467460
c := testConfig(t, "validate-prov-conn-splat-other")
468461
if err := c.Validate(); err != nil {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
resource "aws_instance" "foo" {
2+
provider = "aws.foo"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module "child" {
2+
source = "./child"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
resource "aws_instance" "foo" {
2+
provider = "aws.foo"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
provider "aws" { alias = "foo" }
2+
3+
module "child" {
4+
source = "./child"
5+
}

config/module/tree.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ func (t *Tree) Validate() error {
267267
return newErr
268268
}
269269

270+
// If we're the root, we do extra validation. This validation usually
271+
// requires the entire tree (since children don't have parent pointers).
272+
if len(t.path) == 0 {
273+
if err := t.validateProviderAlias(); err != nil {
274+
return err
275+
}
276+
}
277+
270278
// Get the child trees
271279
children := t.Children()
272280

config/module/tree_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package module
22

33
import (
4+
"fmt"
45
"os"
56
"reflect"
67
"strings"
@@ -267,6 +268,49 @@ func TestTreeName(t *testing.T) {
267268
}
268269
}
269270

271+
// This is a table-driven test for tree validation. This is the preferred
272+
// way to test Validate. Non table-driven tests exist historically but
273+
// that style shouldn't be done anymore.
274+
func TestTreeValidate_table(t *testing.T) {
275+
cases := []struct {
276+
Name string
277+
Fixture string
278+
Err string
279+
}{
280+
{
281+
"provider alias in child",
282+
"validate-alias-good",
283+
"",
284+
},
285+
286+
{
287+
"undefined provider alias in child",
288+
"validate-alias-bad",
289+
"alias must be defined",
290+
},
291+
}
292+
293+
for i, tc := range cases {
294+
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
295+
tree := NewTree("", testConfig(t, tc.Fixture))
296+
if err := tree.Load(testStorage(t), GetModeGet); err != nil {
297+
t.Fatalf("err: %s", err)
298+
}
299+
300+
err := tree.Validate()
301+
if (err != nil) != (tc.Err != "") {
302+
t.Fatalf("err: %s", err)
303+
}
304+
if err == nil {
305+
return
306+
}
307+
if !strings.Contains(err.Error(), tc.Err) {
308+
t.Fatalf("err should contain %q: %s", tc.Err, err)
309+
}
310+
})
311+
}
312+
}
313+
270314
func TestTreeValidate_badChild(t *testing.T) {
271315
tree := NewTree("", testConfig(t, "validate-child-bad"))
272316

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package module
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/hashicorp/go-multierror"
8+
"github.com/hashicorp/terraform/dag"
9+
)
10+
11+
// validateProviderAlias validates that all provider alias references are
12+
// defined at some point in the parent tree. This improves UX by catching
13+
// alias typos at the slight cost of requiring a declaration of usage. This
14+
// is usually a good tradeoff since not many aliases are used.
15+
func (t *Tree) validateProviderAlias() error {
16+
// If we're not the root, don't perform this validation. We must be the
17+
// root since we require full tree visibilty.
18+
if len(t.path) != 0 {
19+
return nil
20+
}
21+
22+
// We'll use a graph to keep track of defined aliases at each level.
23+
// As long as a parent defines an alias, it is okay.
24+
var g dag.AcyclicGraph
25+
t.buildProviderAliasGraph(&g, nil)
26+
27+
// Go through the graph and check that the usage is all good.
28+
var err error
29+
for _, v := range g.Vertices() {
30+
pv, ok := v.(*providerAliasVertex)
31+
if !ok {
32+
// This shouldn't happen, just ignore it.
33+
continue
34+
}
35+
36+
// If we're not using any aliases, fast track and just continue
37+
if len(pv.Used) == 0 {
38+
continue
39+
}
40+
41+
// Grab the ancestors since we're going to have to check if our
42+
// parents define any of our aliases.
43+
var parents []*providerAliasVertex
44+
ancestors, _ := g.Ancestors(v)
45+
for _, raw := range ancestors.List() {
46+
if pv, ok := raw.(*providerAliasVertex); ok {
47+
parents = append(parents, pv)
48+
}
49+
}
50+
for k, _ := range pv.Used {
51+
// Check if we define this
52+
if _, ok := pv.Defined[k]; ok {
53+
continue
54+
}
55+
56+
// Check for a parent
57+
found := false
58+
for _, parent := range parents {
59+
_, found = parent.Defined[k]
60+
if found {
61+
break
62+
}
63+
}
64+
if found {
65+
continue
66+
}
67+
68+
// We didn't find the alias, error!
69+
err = multierror.Append(err, fmt.Errorf(
70+
"module %s: provider alias must be defined by the module or a parent: %s",
71+
strings.Join(pv.Path, "."), k))
72+
}
73+
}
74+
75+
return err
76+
}
77+
78+
func (t *Tree) buildProviderAliasGraph(g *dag.AcyclicGraph, parent dag.Vertex) {
79+
// Add all our defined aliases
80+
defined := make(map[string]struct{})
81+
for _, p := range t.config.ProviderConfigs {
82+
defined[p.FullName()] = struct{}{}
83+
}
84+
85+
// Add all our used aliases
86+
used := make(map[string]struct{})
87+
for _, r := range t.config.Resources {
88+
if r.Provider != "" {
89+
used[r.Provider] = struct{}{}
90+
}
91+
}
92+
93+
// Add it to the graph
94+
vertex := &providerAliasVertex{
95+
Path: t.Path(),
96+
Defined: defined,
97+
Used: used,
98+
}
99+
g.Add(vertex)
100+
101+
// Connect to our parent if we have one
102+
if parent != nil {
103+
g.Connect(dag.BasicEdge(vertex, parent))
104+
}
105+
106+
// Build all our children
107+
for _, c := range t.Children() {
108+
c.buildProviderAliasGraph(g, vertex)
109+
}
110+
}
111+
112+
// providerAliasVertex is the vertex for the graph that keeps track of
113+
// defined provider aliases.
114+
type providerAliasVertex struct {
115+
Path []string
116+
Defined map[string]struct{}
117+
Used map[string]struct{}
118+
}

0 commit comments

Comments
 (0)