Skip to content

Commit d544766

Browse files
authored
[QTI-254] Remove auto depends_on from steps (#52)
* [QTI-254] Remove auto depends_on from steps In Terraform, we've found that automatically depending on prior modules can have unintended consequences. When a module `depends_on` another module output that has a collection it can require Terraform to plan in multiple phases which can break modules that output dynamic collections. We had to make a choice: keep auto-`depends_on` as a feature in Enos but allow scenario authors an escape hatch that would disable depending on prior steps, or remove auto-`depends_on` and allow Terraform to handle dependencies as it normally would. The former is simple but would require scenario authors to interpret strange Terraform error messages. The latter could run into problems where a step might have an implicit dependency on a prior step that Terraform can't reason about. In those cases the author would need to specify a `depends_on` for that step. It's a bit more complicated but maps to how Terraform actually works. We chose the latter option. * Remove auto-`depends_on` feature. * Allow scenario authors to configure `depends_on` values for steps. Signed-off-by: Ryan Cragun <[email protected]>
1 parent 5fa8ce6 commit d544766

File tree

8 files changed

+479
-28
lines changed

8 files changed

+479
-28
lines changed

acceptance/scenarios/scenario_generate_step_vars/enos.hcl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ variable "project" {
22
type = string
33
}
44

5+
module "setupize" {
6+
source = "./modules/setupize"
7+
}
8+
59
module "infra" {
610
source = "./modules/infra"
711
az = "us-east-1"
@@ -17,11 +21,19 @@ scenario "step_vars" {
1721
arch = ["arm", "amd"]
1822
}
1923

24+
step "setup" {
25+
module = module.setupize
26+
}
27+
2028
step "infra_default" {
29+
depends_on = ["setup"]
30+
2131
module = module.infra
2232
}
2333

2434
step "infra_west" {
35+
depends_on = [step.setup]
36+
2537
module = module.infra
2638

2739
variables {
@@ -31,6 +43,7 @@ scenario "step_vars" {
3143

3244
step "target" {
3345
module = module.target
46+
depends_on = concat([step.setup], [matrix.distro == "ubuntu" ? step.infra_west : step.infra_default])
3447

3548
variables {
3649
ami = step.infra_default.amis[matrix.distro][matrix.arch]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
output "random" {
2+
value = "notactuallyrandom"
3+
}

internal/flightplan/decoder_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func testRequireEqualFP(t *testing.T, fp, expected *FlightPlan) {
9797
for is := range expected.Scenarios[i].Steps {
9898
require.EqualValues(t, expected.Scenarios[i].Steps[is].Name, fp.Scenarios[i].Steps[is].Name)
9999
require.EqualValues(t, expected.Scenarios[i].Steps[is].Providers, fp.Scenarios[i].Steps[is].Providers)
100+
require.EqualValues(t, expected.Scenarios[i].Steps[is].DependsOn, fp.Scenarios[i].Steps[is].DependsOn)
100101
require.EqualValues(t, expected.Scenarios[i].Steps[is].Module.Name, fp.Scenarios[i].Steps[is].Module.Name)
101102
require.EqualValues(t, expected.Scenarios[i].Steps[is].Module.Source, fp.Scenarios[i].Steps[is].Module.Source)
102103
require.EqualValues(t, expected.Scenarios[i].Steps[is].Module.Version, fp.Scenarios[i].Steps[is].Module.Version)

internal/flightplan/flightplan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ func filterTerraformMetaAttrs(in hcl.Attributes) (hcl.Attributes, hcl.Diagnostic
666666
case "count", "for_each", "depends_on":
667667
diags = diags.Append(&hcl.Diagnostic{
668668
Severity: hcl.DiagError,
669-
Summary: "invalid module attribute",
669+
Summary: "invalid attribute",
670670
Detail: fmt.Sprintf(`Terraform meta-arguments "%s" are not valid`, attr.Name),
671671
Subject: attr.NameRange.Ptr(),
672672
Context: hcl.RangeBetween(attr.NameRange, attr.Range).Ptr(),

internal/flightplan/scenario_step.go

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package flightplan
33
import (
44
"errors"
55
"fmt"
6+
"sort"
67
"strings"
78

89
"github.com/zclconf/go-cty/cty"
@@ -17,6 +18,7 @@ var scenarioStepSchema = &hcl.BodySchema{
1718
Attributes: []hcl.AttributeSchema{
1819
{Name: "module", Required: true},
1920
{Name: "providers", Required: false},
21+
{Name: "depends_on", Required: false},
2022
},
2123
Blocks: []hcl.BlockHeaderSchema{
2224
{Type: blockTypeVariables},
@@ -28,6 +30,7 @@ type ScenarioStep struct {
2830
Name string
2931
Module *Module
3032
Providers map[string]*Provider
33+
DependsOn []string
3134
}
3235

3336
// NewScenarioStep returns a new Scenario step
@@ -57,6 +60,13 @@ func (ss *ScenarioStep) decode(block *hcl.Block, ctx *hcl.EvalContext) hcl.Diagn
5760
// Decode our name
5861
ss.Name = block.Labels[0]
5962

63+
// Decode depends_on
64+
moreDiags = ss.decodeAndValidateDependsOn(content, ctx)
65+
diags = diags.Extend(moreDiags)
66+
if moreDiags.HasErrors() {
67+
return diags
68+
}
69+
6070
// Decode the step module reference
6171
moduleAttr, moreDiags := ss.decodeModuleAttribute(block, content, ctx)
6272
diags = diags.Extend(moreDiags)
@@ -336,6 +346,119 @@ func (ss *ScenarioStep) validateModuleAttributeReference(module *hcl.Attribute,
336346
return moduleVal, diags
337347
}
338348

349+
// decodeAndValidateDependsOn decodess the depends_on attribute and ensures that
350+
// that the values reference known steps.
351+
func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx *hcl.EvalContext) hcl.Diagnostics {
352+
var diags hcl.Diagnostics
353+
354+
depends, ok := content.Attributes["depends_on"]
355+
if !ok {
356+
return diags
357+
}
358+
359+
ss.DependsOn = []string{}
360+
dependsOnSet := map[string]struct{}{}
361+
362+
dependsVal, moreDiags := depends.Expr.Value(ctx)
363+
diags = diags.Extend(moreDiags)
364+
if moreDiags.HasErrors() {
365+
return diags
366+
}
367+
368+
if dependsVal.IsNull() || !dependsVal.IsWhollyKnown() || !dependsVal.CanIterateElements() {
369+
return diags.Append(&hcl.Diagnostic{
370+
Severity: hcl.DiagError,
371+
Summary: "depends value must be a known object",
372+
Subject: depends.Expr.Range().Ptr(),
373+
Context: depends.Range.Ptr(),
374+
})
375+
}
376+
377+
// Get our defined steps from the eval context
378+
definedSteps, err := findEvalContextVariable("step", ctx)
379+
if err != nil {
380+
return diags.Append(&hcl.Diagnostic{
381+
Severity: hcl.DiagError,
382+
Summary: "No prior steps have been defined. You cannot depend_on an undefined step",
383+
Detail: err.Error(),
384+
Subject: depends.Expr.Range().Ptr(),
385+
Context: depends.Range.Ptr(),
386+
})
387+
}
388+
389+
// For each depends_on, make sure a matching step is defined and
390+
// matches
391+
for _, depV := range dependsVal.AsValueSlice() {
392+
if depV.Type().Equals(cty.String) {
393+
depName := depV.AsString()
394+
// We've been given a string value for our dep so it must be
395+
// an address to a step. Make sure it's defined.
396+
_, ok := definedSteps.AsValueMap()[depName]
397+
if !ok {
398+
diags = diags.Append(&hcl.Diagnostic{
399+
Severity: hcl.DiagError,
400+
Summary: "step has not been defined",
401+
Detail: fmt.Sprintf("cannot depend_on step %s as it has not been defined", depName),
402+
Subject: depends.Expr.Range().Ptr(),
403+
Context: depends.Range.Ptr(),
404+
})
405+
continue
406+
}
407+
408+
if _, exists := dependsOnSet[depName]; exists {
409+
diags = diags.Append(&hcl.Diagnostic{
410+
Severity: hcl.DiagError,
411+
Summary: "cannot depend on the same step more than once",
412+
Detail: fmt.Sprintf("cannot depend_on step %s more than once", depName),
413+
Subject: depends.Expr.Range().Ptr(),
414+
Context: depends.Range.Ptr(),
415+
})
416+
continue
417+
}
418+
419+
dependsOnSet[depName] = struct{}{}
420+
continue
421+
}
422+
423+
// We've been given some other value. Make sure it's a refernce to an
424+
// an existing step, which exists in the eval context as a module value.
425+
step := NewModule()
426+
err := step.FromCtyValue(depV)
427+
if err != nil {
428+
diags = diags.Append(&hcl.Diagnostic{
429+
Severity: hcl.DiagError,
430+
Summary: "value of depends_on attribute is not a step",
431+
Detail: err.Error(),
432+
Subject: depends.Expr.Range().Ptr(),
433+
Context: depends.Range.Ptr(),
434+
})
435+
continue
436+
}
437+
438+
if _, exists := dependsOnSet[step.Name]; exists {
439+
diags = diags.Append(&hcl.Diagnostic{
440+
Severity: hcl.DiagError,
441+
Summary: "cannot depend on the same step more than once",
442+
Detail: fmt.Sprintf("cannot depend_on step %s more than once", step.Name),
443+
Subject: depends.Expr.Range().Ptr(),
444+
Context: depends.Range.Ptr(),
445+
})
446+
continue
447+
}
448+
449+
dependsOnSet[step.Name] = struct{}{}
450+
continue
451+
452+
}
453+
454+
for name := range dependsOnSet {
455+
ss.DependsOn = append(ss.DependsOn, name)
456+
}
457+
sort.Strings(ss.DependsOn)
458+
459+
return diags
460+
}
461+
339462
// decodeAndValidateProvidersAttribute decodess the providers attribute
340463
// from the content and validates that each sub-attribute references a defined
341464
// provider.
@@ -588,7 +711,19 @@ func (ss *ScenarioStep) insertIntoCtx(ctx *hcl.EvalContext) hcl.Diagnostics {
588711
steps = stepVal.AsValueMap()
589712
}
590713

591-
steps[ss.Name] = cty.ObjectVal(ss.Module.Attrs)
714+
vals := map[string]cty.Value{
715+
"source": cty.StringVal(ss.Module.Source),
716+
"name": cty.StringVal(ss.Name),
717+
}
718+
if ss.Module.Version != "" {
719+
vals["version"] = cty.StringVal(ss.Module.Version)
720+
}
721+
722+
for k, v := range ss.Module.Attrs {
723+
vals[k] = v
724+
}
725+
726+
steps[ss.Name] = cty.ObjectVal(vals)
592727
if ctx.Variables == nil {
593728
ctx.Variables = map[string]cty.Value{}
594729
}

0 commit comments

Comments
 (0)