From 2ddf8b4b8c9659fb62262037e41d0293306234e9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 4 Feb 2018 11:05:23 -0800 Subject: [PATCH] cmd/hcldec: allow spec file to define variables and functions The spec file can now additionally define default variables and functions for the eval context used to evaluate the input file. --- cmd/hcldec/main.go | 32 +++++++++++++++++--- cmd/hcldec/spec-format.md | 64 +++++++++++++++++++++++++++++++++++++-- cmd/hcldec/spec.go | 55 +++++++++++++++++++++++++++++++-- 3 files changed, 140 insertions(+), 11 deletions(-) diff --git a/cmd/hcldec/main.go b/cmd/hcldec/main.go index 9ace52d..6bb3bf7 100644 --- a/cmd/hcldec/main.go +++ b/cmd/hcldec/main.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/hcl2/hclparse" flag "github.com/spf13/pflag" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" ctyjson "github.com/zclconf/go-cty/cty/json" "golang.org/x/crypto/ssh/terminal" ) @@ -69,18 +70,26 @@ func realmain(args []string) error { var diags hcl.Diagnostics - spec, specDiags := loadSpecFile(*specFile) + specContent, specDiags := loadSpecFile(*specFile) diags = append(diags, specDiags...) if specDiags.HasErrors() { diagWr.WriteDiagnostics(diags) os.Exit(2) } - var ctx *hcl.EvalContext + spec := specContent.RootSpec + + ctx := &hcl.EvalContext{ + Variables: map[string]cty.Value{}, + Functions: map[string]function.Function{}, + } + for name, val := range specContent.Variables { + ctx.Variables[name] = val + } + for name, f := range specContent.Functions { + ctx.Functions[name] = f + } if len(*vars) != 0 { - ctx = &hcl.EvalContext{ - Variables: map[string]cty.Value{}, - } for i, varsSpec := range *vars { var vals map[string]cty.Value var valsDiags hcl.Diagnostics @@ -98,6 +107,19 @@ func realmain(args []string) error { } } + // If we have empty context elements then we'll nil them out so that + // we'll produce e.g. "variables are not allowed" errors instead of + // "variable not found" errors. + if len(ctx.Variables) == 0 { + ctx.Variables = nil + } + if len(ctx.Functions) == 0 { + ctx.Functions = nil + } + if ctx.Variables == nil && ctx.Functions == nil { + ctx = nil + } + var bodies []hcl.Body if len(args) == 0 { diff --git a/cmd/hcldec/spec-format.md b/cmd/hcldec/spec-format.md index 69b21b8..d80beda 100644 --- a/cmd/hcldec/spec-format.md +++ b/cmd/hcldec/spec-format.md @@ -275,7 +275,7 @@ literal { `literal` spec blocks accept the following argument: * `value` (required) - The value to return. This attribute may be an expression - that uses [functions](#functions). + that uses [functions](#spec-definition-functions). `literal` is a leaf spec type, so no nested spec blocks are permitted. @@ -331,9 +331,62 @@ transform { spec. The variable `nested` is defined when evaluating this expression, with the result value of the nested spec. -The `result` expression may use [functions](#functions). +The `result` expression may use [functions](#spec-definition-functions). -## Functions +## Predefined Variables + +`hcldec` accepts values for variables to expose into the input file's +expression scope as CLI options, and this is the most common way to pass +values since it allows them to be dynamically populated by the calling +application. + +However, it's also possible to pre-define variables with constant values +within a spec file, using the top-level `variables` block type: + +```hcl +variables { + name = "Stephen" +} +``` + +Variables of the same name defined via the `hcldec` command line with override +predefined variables of the same name, so this mechanism can also be used to +provide defaults for variables that are overridden only in certain contexts. + +## Custom Functions + +The spec can make arbitrary HCL functions available in the input file's +expression scope, and thus allow simple computation within the input file, +in addition to HCL's built-in operators. + +Custom functions are defined in the spec file with the top-level `function` +block type: + +``` +function "add_one" { + params = ["n"] + result = n + 1 +} +``` + +Functions behave in a similar way to the `transform` spec type in that the +given `result` attribute expression is evaluated with additional variables +defined with the same names as the defined `params`. + +The [spec definition functions](#spec-definition-functions) can be used within +custom function expressions, allowing them to be optionally exposed into the +input file: + +``` +function "upper" { + params = ["str"] + result = upper(str) +} +``` + +Custom functions defined in the spec cannot be called from the spec itself. + +## Spec Definition Functions Certain expressions within a specification may use the following functions. The documentation for each spec type above specifies where functions may @@ -355,6 +408,11 @@ be used. * `substr(string, offset, length)` returns the requested substring of the given string. * `upper(string)` returns the given string with all lowercase letters converted to uppercase. +Note that these expressions are valid in the context of the _spec_ file, not +the _input_. Functions can be exposed into the input file using +[Custom Functions](#custom-functions) within the spec, which may in turn +refer to these spec definition functions. + ## Type Expressions Type expressions are used to describe the expected type of an attribute, as diff --git a/cmd/hcldec/spec.go b/cmd/hcldec/spec.go index 8010919..31effd3 100644 --- a/cmd/hcldec/spec.go +++ b/cmd/hcldec/spec.go @@ -3,23 +3,72 @@ package main import ( "fmt" + "github.com/hashicorp/hcl2/ext/userfunc" "github.com/hashicorp/hcl2/gohcl" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcldec" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" ) +type specFileContent struct { + Variables map[string]cty.Value + Functions map[string]function.Function + RootSpec hcldec.Spec +} + var specCtx = &hcl.EvalContext{ Functions: specFuncs, } -func loadSpecFile(filename string) (hcldec.Spec, hcl.Diagnostics) { +func loadSpecFile(filename string) (specFileContent, hcl.Diagnostics) { file, diags := parser.ParseHCLFile(filename) if diags.HasErrors() { - return errSpec, diags + return specFileContent{RootSpec: errSpec}, diags } - return decodeSpecRoot(file.Body) + vars, funcs, specBody, declDiags := decodeSpecDecls(file.Body) + diags = append(diags, declDiags...) + + spec, specDiags := decodeSpecRoot(specBody) + diags = append(diags, specDiags...) + + return specFileContent{ + Variables: vars, + Functions: funcs, + RootSpec: spec, + }, diags +} + +func decodeSpecDecls(body hcl.Body) (map[string]cty.Value, map[string]function.Function, hcl.Body, hcl.Diagnostics) { + funcs, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext { + return specCtx + }) + + content, body, moreDiags := body.PartialContent(&hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "variables", + }, + }, + }) + diags = append(diags, moreDiags...) + + vars := make(map[string]cty.Value) + for _, block := range content.Blocks { + // We only have one block type in our schema, so we can assume all + // blocks are of that type. + attrs, moreDiags := block.Body.JustAttributes() + diags = append(diags, moreDiags...) + + for name, attr := range attrs { + val, moreDiags := attr.Expr.Value(specCtx) + diags = append(diags, moreDiags...) + vars[name] = val + } + } + + return vars, funcs, body, diags } func decodeSpecRoot(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {