Skip to content

Commit

Permalink
[QTI-236] Resolve non-constant step variable index expressions (#44)
Browse files Browse the repository at this point in the history
In order to pass information between modules, we have to convert step variable
expressions into absolute traversals if we don't know the value of them.

HCL only supports converting an expression into an absolute traversal if all
traversals have constant keys. This works fine if you use static values, but in
cases where you want to refer to variables or matrix variants it would break,
e.g.:

```hcl
matrix {
  arch   = ["arm64", "amd64"]
  distro = ["rhel", "ubuntu"]
}

step "infra" {
  // Exports an "ami" output that is complex map
}

step "two" {
  variables {
    input = step.one[matrix.distro][matrix.arch]
  }
}
```

Now we attempt to resolve the non-constant keys in index expressions so
that we can render an absolute traversal.

Signed-off-by: Ryan Cragun <[email protected]>
  • Loading branch information
ryancragun authored May 12, 2022
1 parent 22cfe63 commit 440b798
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 3 deletions.
17 changes: 16 additions & 1 deletion acceptance/scenario_generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,37 @@ func TestAcc_Cmd_Scenario_Generate(t *testing.T) {
dir string
args string
uid string
noRc bool
}{
{
"scenario_generate_pass_0",
"test foo:matrixfoo",
fmt.Sprintf("%x", sha256.Sum256([]byte("test [foo:matrixfoo]"))),
false,
},
{
"scenario_generate_pass_0",
"test foo:matrixbar",
fmt.Sprintf("%x", sha256.Sum256([]byte("test [foo:matrixbar]"))),
false,
},
{
"scenario_generate_pass_backend",
"",
fmt.Sprintf("%x", sha256.Sum256([]byte("test"))),
false,
},
{
"scenario_generate_pass_cloud",
"",
fmt.Sprintf("%x", sha256.Sum256([]byte("test"))),
false,
},
{
"scenario_generate_step_vars",
"step_vars distro:rhel arch:arm",
fmt.Sprintf("%x", sha256.Sum256([]byte("step_vars [arch:arm distro:rhel]"))),
true,
},
} {
t.Run(fmt.Sprintf("%s %s", test.dir, test.args), func(t *testing.T) {
Expand All @@ -62,7 +73,11 @@ func TestAcc_Cmd_Scenario_Generate(t *testing.T) {
require.NoError(t, err)
s.Close()
rc, err := os.Open(filepath.Join(outDir, test.uid, "terraform.rc"))
require.NoError(t, err)
if test.noRc {
require.Error(t, err)
} else {
require.NoError(t, err)
}
rc.Close()
})
}
Expand Down
26 changes: 26 additions & 0 deletions acceptance/scenarios/scenario_generate_step_vars/enos.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module "infra" {
source = "./modules/infra"
}

module "target" {
source = "./modules/target"
}

scenario "step_vars" {
matrix {
distro = ["ubuntu", "rhel"]
arch = ["arm", "amd"]
}

step "infra" {
module = module.infra
}

step "target" {
module = module.target

variables {
ami = step.infra.amis[matrix.distro][matrix.arch]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
output "amis" {
value = {
"ubuntu" = {
"arm" = "ubuntu-arm"
"amd" = "ubuntu-amd"
}
"rhel" = {
"arm" = "rhel-arm"
"amd" = "rhel-amd"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
variable "ami" {
type = string
}
62 changes: 60 additions & 2 deletions internal/flightplan/step_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/customdecode"
"github.com/hashicorp/hcl/v2/hclsyntax"
)

// StepVariableType is a cty capsule type that represents "step" variables.
Expand Down Expand Up @@ -42,6 +43,63 @@ func StepVariableFromVal(v cty.Value) (*StepVariable, hcl.Diagnostics) {
return v.EncapsulatedValue().(*StepVariable), diags
}

// absTraversalForExpr is similar to hcl.AbsTraversalForExpr() in that it returns
// an expression as an absolute value. Where it differs is that our implementation
// will use the passed in EvalContext to resolve values in the expression that
// might otherwise be unknown.
// NOTE: This implemenation currently only support expanding the values of keys
// in index expressions. Enos is intended to support passing configuration between
// modules by reference. If you need to perform complex operations on step
// variables you'll need to perform that in the module that is taking the value
// as an input.
func absTraversalForExpr(expr hcl.Expression, ctx *hcl.EvalContext) (hcl.Traversal, hcl.Diagnostics) {
traversal, diags := hcl.AbsTraversalForExpr(expr)
if !diags.HasErrors() {
// We have a valid absolute traversal
return traversal, diags
}

traversal = hcl.Traversal{}

// If we're here we're dealing with an expression that has neither a known
// value or a static absolute traversal. We'll attempt to unwrap our expresion
// and decode unknown values into static values where possible.
for {
switch t := expr.(type) {
case *hclsyntax.ScopeTraversalExpr:
// We're run into what is likely the root of our traversal. Append
// what we've got and break our loop as there are no more collection
// expressions to unwrap.
return append(t.AsTraversal(), traversal...), nil
case *hclsyntax.IndexExpr:
v, err := t.Key.Value(ctx)
if err != nil {
return traversal, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "unable to resolve index value",
Detail: err.Error(),
Subject: t.StartRange().Ptr(),
Context: t.SrcRange.Ptr(),
}}
}
// Add our known index value to the traversal and set the next
// collection expression for unwrapping
traversal = append(hcl.Traversal{hcl.TraverseIndex{
SrcRange: t.SrcRange,
Key: v,
}}, traversal...)
expr = t.Collection
default:
return traversal, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("expanding expression for %s is not supported", reflect.TypeOf(t).Name()),
Subject: t.StartRange().Ptr(),
Context: t.Range().Ptr(),
}}
}
}
}

func init() {
StepVariableType = cty.CapsuleWithOps("stepvar", reflect.TypeOf(StepVariable{}), &cty.CapsuleOps{
ExtensionData: func(key any) any {
Expand Down Expand Up @@ -88,7 +146,7 @@ func init() {

// We have an unknown value. Let's find out if it's a
// valid traversal to another "step".
traversal, moreDiags := hcl.AbsTraversalForExpr(expr)
traversal, moreDiags := absTraversalForExpr(expr, ctx)
if moreDiags.HasErrors() {
// If it's not an absolute traversal we can't do
// static analysis.
Expand All @@ -104,7 +162,7 @@ func init() {
Subject: traversal.SourceRange().Ptr(),
Context: expr.Range().Ptr(),
Summary: "step variable is unknowable",
Detail: "step variables can only be uknown if the value is a reference to a step module output",
Detail: "step variables can only be unknown if the value is a reference to a step module output",
})
}

Expand Down

0 comments on commit 440b798

Please sign in to comment.