From a107c8969348f1fe3946eca22bac1091d222744f Mon Sep 17 00:00:00 2001 From: Yoriyasu Yano <430092+yorinasub17@users.noreply.github.com> Date: Wed, 31 Jul 2019 11:24:25 -0700 Subject: [PATCH] Add more test cases --- config/locals.go | 42 +++++-- config/locals_test.go | 109 ++++++++++++++++++ .../undefined-local-but-input/terragrunt.hcl | 4 + .../undefined-local/terragrunt.hcl | 3 + .../{ => canonical}/contents.txt | 0 test/fixture-locals/{ => canonical}/main.tf | 0 .../{ => canonical}/terragrunt.hcl | 0 .../local-in-include/qa/my-app/main.tf | 8 ++ .../local-in-include/qa/my-app/terragrunt.hcl | 7 ++ .../local-in-include/terragrunt.hcl | 10 ++ test/integration_test.go | 41 ++++++- 11 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 test/fixture-locals-errors/undefined-local-but-input/terragrunt.hcl create mode 100644 test/fixture-locals-errors/undefined-local/terragrunt.hcl rename test/fixture-locals/{ => canonical}/contents.txt (100%) rename test/fixture-locals/{ => canonical}/main.tf (100%) rename test/fixture-locals/{ => canonical}/terragrunt.hcl (100%) create mode 100644 test/fixture-locals/local-in-include/qa/my-app/main.tf create mode 100644 test/fixture-locals/local-in-include/qa/my-app/terragrunt.hcl create mode 100644 test/fixture-locals/local-in-include/terragrunt.hcl diff --git a/config/locals.go b/config/locals.go index 8b8abd7b4..9c59b5969 100644 --- a/config/locals.go +++ b/config/locals.go @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/config/locals_test.go b/config/locals_test.go index bc9f81a22..6471bb9f2 100644 --- a/config/locals_test.go +++ b/config/locals_test.go @@ -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) { @@ -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"` @@ -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" +} +` diff --git a/test/fixture-locals-errors/undefined-local-but-input/terragrunt.hcl b/test/fixture-locals-errors/undefined-local-but-input/terragrunt.hcl new file mode 100644 index 000000000..b18399e3b --- /dev/null +++ b/test/fixture-locals-errors/undefined-local-but-input/terragrunt.hcl @@ -0,0 +1,4 @@ +inputs = { + a = "foo" + b = local.a +} diff --git a/test/fixture-locals-errors/undefined-local/terragrunt.hcl b/test/fixture-locals-errors/undefined-local/terragrunt.hcl new file mode 100644 index 000000000..8dcc9564a --- /dev/null +++ b/test/fixture-locals-errors/undefined-local/terragrunt.hcl @@ -0,0 +1,3 @@ +inputs = { + data = local.file_contents +} diff --git a/test/fixture-locals/contents.txt b/test/fixture-locals/canonical/contents.txt similarity index 100% rename from test/fixture-locals/contents.txt rename to test/fixture-locals/canonical/contents.txt diff --git a/test/fixture-locals/main.tf b/test/fixture-locals/canonical/main.tf similarity index 100% rename from test/fixture-locals/main.tf rename to test/fixture-locals/canonical/main.tf diff --git a/test/fixture-locals/terragrunt.hcl b/test/fixture-locals/canonical/terragrunt.hcl similarity index 100% rename from test/fixture-locals/terragrunt.hcl rename to test/fixture-locals/canonical/terragrunt.hcl diff --git a/test/fixture-locals/local-in-include/qa/my-app/main.tf b/test/fixture-locals/local-in-include/qa/my-app/main.tf new file mode 100644 index 000000000..f211b9ec4 --- /dev/null +++ b/test/fixture-locals/local-in-include/qa/my-app/main.tf @@ -0,0 +1,8 @@ +terraform { + backend "s3" {} +} + +# Create an arbitrary local resource +data "template_file" "test" { + template = "Hello, I am a template." +} diff --git a/test/fixture-locals/local-in-include/qa/my-app/terragrunt.hcl b/test/fixture-locals/local-in-include/qa/my-app/terragrunt.hcl new file mode 100644 index 000000000..06ecee297 --- /dev/null +++ b/test/fixture-locals/local-in-include/qa/my-app/terragrunt.hcl @@ -0,0 +1,7 @@ +locals { + parent_path = find_in_parent_folders() +} + +include { + path = local.parent_path +} diff --git a/test/fixture-locals/local-in-include/terragrunt.hcl b/test/fixture-locals/local-in-include/terragrunt.hcl new file mode 100644 index 000000000..d68a1ac37 --- /dev/null +++ b/test/fixture-locals/local-in-include/terragrunt.hcl @@ -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" + } +} diff --git a/test/integration_test.go b/test/integration_test.go index 4b0db5cb6..afc343110 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -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" @@ -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{} @@ -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{}