Skip to content

Commit

Permalink
Add more test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
yorinasub17 committed Jul 31, 2019
1 parent 3601cfe commit a107c89
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 12 deletions.
42 changes: 34 additions & 8 deletions config/locals.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ import (
// MaxIter is the maximum number of depth we support in recursively evaluating locals.
const MaxIter = 1000

// A consistent detail message for all "not a valid identifier" diagnostics. This is exactly the same as that returned
// by terraform.
const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes."
// Detailed error messages in diagnostics returned by parsing locals
const (
// A consistent detail message for all "not a valid identifier" diagnostics. This is exactly the same as that returned
// by terraform.
badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes."

// A consistent error message for multiple locals block in terragrunt config (which is currently not supported)
multipleLocalsBlockDetail = "Terragrunt currently does not support multiple locals blocks in a single config. Consolidate to a single locals block."
)

// Local represents a single local name binding. This holds the unevaluated expression, extracted from the parsed file
// (but before decoding) so that we can look for references to other locals before evaluating.
Expand Down Expand Up @@ -76,7 +82,13 @@ func evaluateLocalsBlock(
}

var err error
locals, evaluatedLocals, evaluated, err = attemptEvaluateLocals(terragruntOptions, filename, locals, evaluatedLocals)
locals, evaluatedLocals, evaluated, err = attemptEvaluateLocals(
terragruntOptions,
filename,
locals,
evaluatedLocals,
diagsWriter,
)
if err != nil {
terragruntOptions.Logger.Printf("Encountered error while evaluating locals.")
return nil, err
Expand Down Expand Up @@ -105,6 +117,7 @@ func attemptEvaluateLocals(
filename string,
locals []*Local,
evaluatedLocals map[string]cty.Value,
diagsWriter hcl.DiagnosticWriter,
) (unevaluatedLocals []*Local, newEvaluatedLocals map[string]cty.Value, evaluated bool, 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
Expand Down Expand Up @@ -139,7 +152,7 @@ func attemptEvaluateLocals(
if canEvaluate(terragruntOptions, local.Expr, evaluatedLocals) {
evaluatedVal, diags := local.Expr.Value(evalCtx)
if diags.HasErrors() {
// TODO Write out diagnostic errors
diagsWriter.WriteDiagnostics(diags)
return nil, evaluatedLocals, false, errors.WithStackTrace(diags)
}
newEvaluatedLocals[local.Name] = evaluatedVal
Expand Down Expand Up @@ -239,13 +252,26 @@ func getLocalsBlock(hclFile *hcl.File) (*hcl.Block, hcl.Diagnostics) {
}
// We use PartialContent here, because we are only interested in parsing out the locals block.
parsedLocals, _, diags := hclFile.Body.PartialContent(localsSchema)
extractedLocalsBlocks := []*hcl.Block{}
for _, block := range parsedLocals.Blocks {
if block.Type == "locals" {
return block, diags
extractedLocalsBlocks = append(extractedLocalsBlocks, block)
}
}
// No locals block parsed
return nil, diags
// We currently only support parsing a single locals block
if len(extractedLocalsBlocks) == 1 {
return extractedLocalsBlocks[0], diags
} else if len(extractedLocalsBlocks) > 1 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Multiple locals block",
Detail: multipleLocalsBlockDetail,
})
return nil, diags
} else {
// No locals block parsed
return nil, diags
}
}

// decodeLocalsBlock loads the block into name expression pairs to assist with evaluation of the locals prior to
Expand Down
109 changes: 109 additions & 0 deletions config/locals_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package config

import (
"fmt"
"testing"

"github.com/hashicorp/hcl2/hclparse"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty/gocty"

"github.com/gruntwork-io/terragrunt/errors"
)

func TestEvaluateLocalsBlock(t *testing.T) {
Expand Down Expand Up @@ -54,6 +57,79 @@ func TestEvaluateLocalsBlock(t *testing.T) {
assert.Equal(t, actualBar, "us-east-1")
}

func TestEvaluateLocalsBlockMultiDeepReference(t *testing.T) {
t.Parallel()

terragruntOptions := mockOptionsForTest(t)
mockFilename := "terragrunt.hcl"

parser := hclparse.NewParser()
file, err := parseHcl(parser, LocalsTestMultiDeepReferenceConfig, mockFilename)
require.NoError(t, err)

evaluatedLocals, err := evaluateLocalsBlock(terragruntOptions, parser, file, mockFilename)
require.NoError(t, err)

expected := "a"

var actualA string
require.NoError(t, gocty.FromCtyValue(evaluatedLocals["a"], &actualA))
assert.Equal(t, actualA, expected)

testCases := []string{
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
}
for _, testCase := range testCases {
expected = fmt.Sprintf("%s/%s", expected, testCase)

var actual string
require.NoError(t, gocty.FromCtyValue(evaluatedLocals[testCase], &actual))
assert.Equal(t, actual, expected)
}
}

func TestEvaluateLocalsBlockImpossibleWillFail(t *testing.T) {
t.Parallel()

terragruntOptions := mockOptionsForTest(t)
mockFilename := "terragrunt.hcl"

parser := hclparse.NewParser()
file, err := parseHcl(parser, LocalsTestImpossibleConfig, mockFilename)
require.NoError(t, err)

_, err = evaluateLocalsBlock(terragruntOptions, parser, file, mockFilename)
require.Error(t, err)

switch errors.Unwrap(err).(type) {
case CouldNotEvaluateAllLocalsError:
default:
t.Fatalf("Did not get expected error: %s", err)
}
}

func TestEvaluateLocalsBlockMultipleLocalsBlocksWillFail(t *testing.T) {
t.Parallel()

terragruntOptions := mockOptionsForTest(t)
mockFilename := "terragrunt.hcl"

parser := hclparse.NewParser()
file, err := parseHcl(parser, MultipleLocalsBlockConfig, mockFilename)
require.NoError(t, err)

_, err = evaluateLocalsBlock(terragruntOptions, parser, file, mockFilename)
require.Error(t, err)
}

type Foo struct {
Region string `cty:"region"`
Foo string `cty:"foo"`
Expand Down Expand Up @@ -81,3 +157,36 @@ locals {
z = local.x + local.y
}
`

const LocalsTestMultiDeepReferenceConfig = `
# 10 chains deep
locals {
a = "a"
b = "${local.a}/b"
c = "${local.b}/c"
d = "${local.c}/d"
e = "${local.d}/e"
f = "${local.e}/f"
g = "${local.f}/g"
h = "${local.g}/h"
i = "${local.h}/i"
j = "${local.i}/j"
}
`

const LocalsTestImpossibleConfig = `
locals {
a = local.b
b = local.a
}
`

const MultipleLocalsBlockConfig = `
locals {
a = "a"
}
locals {
b = "b"
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
inputs = {
a = "foo"
b = local.a
}
3 changes: 3 additions & 0 deletions test/fixture-locals-errors/undefined-local/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
inputs = {
data = local.file_contents
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions test/fixture-locals/local-in-include/qa/my-app/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
backend "s3" {}
}

# Create an arbitrary local resource
data "template_file" "test" {
template = "Hello, I am a template."
}
7 changes: 7 additions & 0 deletions test/fixture-locals/local-in-include/qa/my-app/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
locals {
parent_path = find_in_parent_folders()
}

include {
path = local.parent_path
}
10 changes: 10 additions & 0 deletions test/fixture-locals/local-in-include/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Configure Terragrunt to automatically store tfstate files in an S3 bucket
remote_state {
backend = "s3"
config = {
encrypt = true
bucket = "__FILL_IN_BUCKET_NAME__"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-west-2"
}
}
41 changes: 37 additions & 4 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ const (
TEST_FIXTURE_AUTO_RETRY_EXHAUST = "fixture-auto-retry/exhaust"
TEST_FIXTURE_AUTO_RETRY_APPLY_ALL_RETRIES = "fixture-auto-retry/apply-all"
TEST_FIXTURE_INPUTS = "fixture-inputs"
TEST_FIXTURE_LOCALS = "fixture-locals"
TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL = "fixture-locals-errors/undefined-local"
TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL_BUT_INPUT = "fixture-locals-errors/undefined-local-but-input"
TEST_FIXTURE_LOCALS_CANONICAL = "fixture-locals/canonical"
TEST_FIXTURE_LOCALS_IN_INCLUDE = "fixture-locals/local-in-include"
TEST_FIXTURE_LOCALS_IN_INCLUDE_CHILD_REL_PATH = "qa/my-app"
TERRAFORM_BINARY = "terraform"
TERRAFORM_FOLDER = ".terraform"
TERRAFORM_STATE = "terraform.tfstate"
Expand Down Expand Up @@ -1107,14 +1111,14 @@ func TestInputsPassedThroughCorrectly(t *testing.T) {
func TestLocalsParsing(t *testing.T) {
t.Parallel()

cleanupTerraformFolder(t, TEST_FIXTURE_LOCALS)
cleanupTerraformFolder(t, TEST_FIXTURE_LOCALS_CANONICAL)

runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS))
runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS_CANONICAL))

stdout := bytes.Buffer{}
stderr := bytes.Buffer{}

err := runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS), &stdout, &stderr)
err := runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS_CANONICAL), &stdout, &stderr)
require.NoError(t, err)

outputs := map[string]TerraformOutput{}
Expand All @@ -1124,6 +1128,35 @@ func TestLocalsParsing(t *testing.T) {
assert.Equal(t, outputs["answer"].Value, float64(42))
}

func TestLocalsInInclude(t *testing.T) {
t.Parallel()

childPath := util.JoinPath(TEST_FIXTURE_LOCALS_IN_INCLUDE, TEST_FIXTURE_LOCALS_IN_INCLUDE_CHILD_REL_PATH)
cleanupTerraformFolder(t, TEST_FIXTURE_LOCALS_IN_INCLUDE)

s3BucketName := fmt.Sprintf("terragrunt-%s-%s", strings.ToLower(t.Name()), strings.ToLower(uniqueId()))
defer deleteS3Bucket(t, TERRAFORM_REMOTE_STATE_S3_REGION, s3BucketName)

tmpTerragruntConfigPath := createTmpTerragruntConfigWithParentAndChild(t, TEST_FIXTURE_LOCALS_IN_INCLUDE, TEST_FIXTURE_LOCALS_IN_INCLUDE_CHILD_REL_PATH, s3BucketName, config.DefaultTerragruntConfigPath, config.DefaultTerragruntConfigPath)
runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s --terragrunt-working-dir %s", tmpTerragruntConfigPath, childPath))
}

func TestUndefinedLocalsReferenceBreaks(t *testing.T) {
t.Parallel()

cleanupTerraformFolder(t, TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL)
err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL), os.Stdout, os.Stderr)
assert.Error(t, err)
}

func TestUndefinedLocalsReferenceToInputsBreaks(t *testing.T) {
t.Parallel()

cleanupTerraformFolder(t, TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL_BUT_INPUT)
err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir %s", TEST_FIXTURE_LOCALS_ERROR_UNDEFINED_LOCAL_BUT_INPUT), os.Stdout, os.Stderr)
assert.Error(t, err)
}

type TerraformOutput struct {
Sensitive bool
Type interface{}
Expand Down

0 comments on commit a107c89

Please sign in to comment.