Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locals #802

Merged
merged 10 commits into from
Aug 1, 2019
2 changes: 2 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,7 @@ at all.
This section contains detailed documentation for the following aspects of Terragrunt:

1. [Inputs](#inputs)
1. [Locals](#locals)
1. [AWS credentials](#aws-credentials)
1. [AWS IAM policies](#aws-iam-policies)
1. [Built-in Functions](#built-in-functions)
Expand All @@ -1228,7 +1229,7 @@ You can set values for your module's input parameters by specifying an `inputs`
inputs = {
instance_type = "t2.micro"
instance_count = 10

tags = {
Name = "example-app"
}
Expand All @@ -1253,6 +1254,79 @@ Note that Terragrunt will respect any `TF_VAR_xxx` variables you've manually set
anything in `inputs` will NOT be override anything you've already set in your environment.


### Locals

You can use locals to bind a name to an expression, so you can reuse that expression without having to repeat it multiple times (keeping your Terragrunt configuration DRY).
config. For example, suppose that you need to use the AWS region in multiple inputs. You can bind the name `aws_region`
using locals:

```
locals {
aws_region = "us-east-1"
}

inputs = {
aws_region = local.aws_region
s3_endpoint = "com.amazonaws.${local.aws_region}.s3"
}
```

You can use any valid terragrunt expression in the `locals` configuration. The `locals` block also supports referencing other `locals`:

```
locals {
x = 2
y = 40
answer = local.x + local.y
}
```

##### Including globally defined locals
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for adding 👍


Currently you can only reference `locals` defined in the same config file. `terragrunt` does not automatically include
`locals` defined in the parent config of an `include` block into the current context. If you wish to reuse variables
globally, consider using `yaml` or `json` files that are included and merged using the `terraform` built in functions
available to `terragrunt`.

For example, suppose you had the following directory tree:

```
.
├── terragrunt.hcl
├── mysql
│   └── terragrunt.hcl
└── vpc
└── terragrunt.hcl
```

Instead of adding the `locals` block to the parent `terragrunt.hcl` file, you can define a file `common_vars.yaml`
that contains the global variables you wish to pull in:

```
.
├── terragrunt.hcl
├── common_vars.yaml
├── mysql
│   └── terragrunt.hcl
└── vpc
└── terragrunt.hcl
```

You can then include them into the `locals` block of the child terragrunt config using `yamldecode` and `file`:

```
# child terragrunt.hcl
locals {
common_vars = yamldecode(file("${get_terragrunt_dir()}/${find_in_parent_folders("common_vars.yaml")}")),
region = "us-east-1"
}
```

This configuration will load in the `common_vars.yaml` file and bind it to the attribute `common_vars` so that it is available
in the current context. Note that because `locals` is a block, there currently is a way to merge the map into the top
level.


### AWS credentials

Terragrunt uses the official [AWS SDK for Go](https://aws.amazon.com/sdk-for-go/), which
Expand Down
14 changes: 1 addition & 13 deletions cli/hclfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"io/ioutil"
"os"

"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/hashicorp/hcl2/hclwrite"
"github.com/mattn/go-zglob"
"golang.org/x/crypto/ssh/terminal"

"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/util"
Expand Down Expand Up @@ -65,21 +63,11 @@ func formatTgHCL(terragruntOptions *options.TerragruntOptions, tgHclFile string)
return ioutil.WriteFile(tgHclFile, newContents, info.Mode())
}

// getDiagnosticsWriter returns a hcl2 parsing diagnostics emitter for the current terminal.
func getDiagnosticsWriter(parser *hclparse.Parser) hcl.DiagnosticWriter {
termColor := terminal.IsTerminal(int(os.Stderr.Fd()))
termWidth, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err != nil {
termWidth = 80
}
return hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(termWidth), termColor)
}

// checkErrors takes in the contents of a terragrunt.hcl file and looks for syntax errors.
func checkErrors(contents []byte, tgHclFile string) error {
parser := hclparse.NewParser()
_, diags := parser.ParseHCL(contents, tgHclFile)
diagWriter := getDiagnosticsWriter(parser)
diagWriter := util.GetDiagnosticsWriter(parser)
diagWriter.WriteDiagnostics(diags)
if diags.HasErrors() {
return diags
Expand Down
64 changes: 50 additions & 14 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package config

import (
"fmt"
"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/zclconf/go-cty/cty"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/hashicorp/hcl2/gohcl"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hclparse"
"github.com/zclconf/go-cty/cty"

"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/remote"
Expand All @@ -28,6 +29,7 @@ type TerragruntConfig struct {
Skip bool
IamRole string
Inputs map[string]interface{}
Locals map[string]interface{}
}

func (conf *TerragruntConfig) String() string {
Expand All @@ -45,6 +47,15 @@ type terragruntConfigFile struct {
PreventDestroy *bool `hcl:"prevent_destroy,attr"`
Skip *bool `hcl:"skip,attr"`
IamRole *string `hcl:"iam_role,attr"`

// This should be ignored
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be skipped here because hcl can't decode it correctly when it self references locals. It is not necessary to decode it into this struct here because it is evaluated in a separate loop.

However, we don't want to completely omit the existence of it because we want a syntax error if we have blocks that terragrunt doesn't expect (e.g typos). Hence it is added here as an attribute but the struct won't attempt to parse anything in it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps expand the comment a bit to explain this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locals *terragruntLocal `hcl:"locals,block"`
}

// We use a struct designed to not parse the block, as locals are parsed and decoded using a special routine that allows
// references to the other locals in the same block.
type terragruntLocal struct {
Remain hcl.Body `hcl:",remain"`
}

type terragruntInclude struct {
Expand Down Expand Up @@ -240,7 +251,8 @@ func ParseConfigFile(filename string, terragruntOptions *options.TerragruntOptio
// Parse the Terragrunt config contained in the given string and merge it with the given include config (if any)
func ParseConfigString(configString string, terragruntOptions *options.TerragruntOptions, includeFromChild *IncludeConfig, filename string) (*TerragruntConfig, error) {
// Parse the HCL string into an AST body that can be decoded multiple times later without having to re-parse
file, err := parseHcl(configString, filename)
parser := hclparse.NewParser()
file, err := parseHcl(parser, configString, filename)
if err != nil {
return nil, err
}
Expand All @@ -251,6 +263,13 @@ func ParseConfigString(configString string, terragruntOptions *options.Terragrun
return nil, err
}

// Evaluate all the expressions in the locals block separately and generate the variables list to use in the
// evaluation context.
locals, err := evaluateLocalsBlock(terragruntOptions, parser, file, filename)
if err != nil {
return nil, err
}

var includeForDecode *IncludeConfig = nil
if terragruntInclude.Include != nil && includeFromChild != nil {
return nil, errors.WithStackTrace(TooManyLevelsOfInheritance{
Expand All @@ -266,7 +285,7 @@ func ParseConfigString(configString string, terragruntOptions *options.Terragrun

// Decode the rest of the config, passing in this config's `include` block or the child's `include` block, whichever
// is appropriate
terragruntConfigFile, err := decodeAsTerragruntConfigFile(file, filename, terragruntOptions, includeForDecode)
terragruntConfigFile, err := decodeAsTerragruntConfigFile(file, filename, terragruntOptions, includeForDecode, locals)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -299,24 +318,37 @@ func ParseConfigString(configString string, terragruntOptions *options.Terragrun
// an include block)
func decodeAsTerragruntInclude(file *hcl.File, filename string, terragruntOptions *options.TerragruntOptions) (*terragruntInclude, error) {
terragruntInclude := terragruntInclude{}
err := decodeHcl(file, filename, &terragruntInclude, terragruntOptions, nil)
err := decodeHcl(file, filename, &terragruntInclude, terragruntOptions, nil, nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An include can't have its own locals?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point! Updated the evaluation order so it can be there.

if err != nil {
return nil, err
}
return &terragruntInclude, nil
}

func decodeAsTerragruntConfigFile(file *hcl.File, filename string, terragruntOptions *options.TerragruntOptions, include *IncludeConfig) (*terragruntConfigFile, error) {
func decodeAsTerragruntConfigFile(
file *hcl.File,
filename string,
terragruntOptions *options.TerragruntOptions,
include *IncludeConfig,
locals map[string]cty.Value,
) (*terragruntConfigFile, error) {
terragruntConfig := terragruntConfigFile{}
err := decodeHcl(file, filename, &terragruntConfig, terragruntOptions, include)
err := decodeHcl(file, filename, &terragruntConfig, terragruntOptions, include, locals)
if err != nil {
return nil, err
}
return &terragruntConfig, nil
}

// decodeHcl uses the HCL2 parser to decode the parsed HCL into the struct specified by out.
func decodeHcl(file *hcl.File, filename string, out interface{}, terragruntOptions *options.TerragruntOptions, include *IncludeConfig) (err error) {
func decodeHcl(
file *hcl.File,
filename string,
out interface{},
terragruntOptions *options.TerragruntOptions,
include *IncludeConfig,
locals map[string]cty.Value,
) (err error) {
// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from
// those panics here and convert them to normal errors
defer func() {
Expand All @@ -325,7 +357,13 @@ func decodeHcl(file *hcl.File, filename string, out interface{}, terragruntOptio
}
}()

evalContext := CreateTerragruntEvalContext(filename, terragruntOptions, include)
// Convert locals to a cty object for use in the evaluation context. Otherwise, we can't bind the whole map under
// the name `local`.
localsAsCty, err := convertLocalsMapToCtyVal(locals)
if err != nil {
return err
}
evalContext := CreateTerragruntEvalContext(filename, terragruntOptions, include, localsAsCty)

decodeDiagnostics := gohcl.DecodeBody(file.Body, evalContext, out)
if decodeDiagnostics != nil && decodeDiagnostics.HasErrors() {
Expand All @@ -336,7 +374,7 @@ func decodeHcl(file *hcl.File, filename string, out interface{}, terragruntOptio
}

// parseHcl uses the HCL2 parser to parse the given string into an HCL file body.
func parseHcl(hcl string, filename string) (file *hcl.File, err error) {
func parseHcl(parser *hclparse.Parser, hcl string, filename string) (file *hcl.File, err error) {
// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from
// those panics here and convert them to normal errors
defer func() {
Expand All @@ -345,8 +383,6 @@ func parseHcl(hcl string, filename string) (file *hcl.File, err error) {
}
}()

parser := hclparse.NewParser()

file, parseDiagnostics := parser.ParseHCL([]byte(hcl), filename)
if parseDiagnostics != nil && parseDiagnostics.HasErrors() {
return nil, parseDiagnostics
Expand Down
23 changes: 17 additions & 6 deletions config/config_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ package config

import (
"fmt"
"github.com/gruntwork-io/terragrunt/shell"
"github.com/hashicorp/hcl2/hcl"
tflang "github.com/hashicorp/terraform/lang"
"github.com/zclconf/go-cty/cty/function"
"path/filepath"

"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/hcl2/hcl"
tflang "github.com/hashicorp/terraform/lang"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"

"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/shell"
"github.com/gruntwork-io/terragrunt/util"
)

Expand Down Expand Up @@ -63,7 +65,12 @@ type EnvVar struct {

// Create an EvalContext for the HCL2 parser. We can define functions and variables in this context that the HCL2 parser
// will make available to the Terragrunt configuration during parsing.
func CreateTerragruntEvalContext(filename string, terragruntOptions *options.TerragruntOptions, include *IncludeConfig) *hcl.EvalContext {
func CreateTerragruntEvalContext(
filename string,
terragruntOptions *options.TerragruntOptions,
include *IncludeConfig,
locals *cty.Value,
) *hcl.EvalContext {
tfscope := tflang.Scope{
BaseDir: filepath.Dir(filename),
}
Expand Down Expand Up @@ -91,9 +98,13 @@ func CreateTerragruntEvalContext(filename string, terragruntOptions *options.Ter
functions[k] = v
}

return &hcl.EvalContext{
ctx := &hcl.EvalContext{
Functions: functions,
}
if locals != nil {
ctx.Variables = map[string]cty.Value{"local": *locals}
}
return ctx
}

// Return the directory where the Terragrunt configuration file lives
Expand Down
2 changes: 2 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ include {
opts := options.TerragruntOptions{
TerragruntConfigPath: "../test/fixture-parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/" + DefaultTerragruntConfigPath,
NonInteractive: true,
Logger: util.CreateLogger(""),
}

terragruntConfig, err := ParseConfigString(config, &opts, nil, opts.TerragruntConfigPath)
Expand Down Expand Up @@ -852,6 +853,7 @@ terraform {
TerragruntConfigPath: "../test/fixture-parent-folders/terragrunt-in-root/child/" + DefaultTerragruntConfigPath,
NonInteractive: true,
MaxFoldersToCheck: 5,
Logger: util.CreateLogger(""),
}

terragruntConfig, err := ParseConfigString(config, &opts, nil, DefaultTerragruntConfigPath)
Expand Down
Loading