diff --git a/astro/terraform/terraform_remote_state_disable.go b/astro/terraform/terraform_remote_state_disable.go index 86abfcf..5c3a462 100644 --- a/astro/terraform/terraform_remote_state_disable.go +++ b/astro/terraform/terraform_remote_state_disable.go @@ -120,12 +120,15 @@ func (s *Session) deleteBackendConfig() error { if len(candidates) < 1 { return errors.New("cannot find backend configuration in the Terraform files") } + terraformVersion, err := s.Version() + if err != nil { + return err + } for _, f := range candidates { - if err := deleteTerraformBackendConfigFromFile(f); err != nil { + if err := deleteTerraformBackendConfigFromFile(f, terraformVersion); err != nil { return err } } - return nil } diff --git a/astro/terraform/terraform_remote_state_disable_test.go b/astro/terraform/terraform_remote_state_disable_test.go index 1f43cb1..6065add 100644 --- a/astro/terraform/terraform_remote_state_disable_test.go +++ b/astro/terraform/terraform_remote_state_disable_test.go @@ -22,14 +22,16 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDeleteTerraformBackendConfig(t *testing.T) { +// Tests that backend part can be successfully removed from the config +// written in HCL 1.0 language +func TestDeleteTerraformBackendConfigWithHCL1(t *testing.T) { input := []byte(` terraform { backend "s3" {} } provider "aws" { - region = "us-east-1" + region = "${var.aws_region}" } module "codecommit" { @@ -44,13 +46,13 @@ terraform { ] }`) - updatedConfig, err := deleteTerraformBackendConfig(input) + updatedConfig, err := deleteTerraformBackendConfigWithHCL1(input) assert.NoError(t, err) assert.Equal(t, `terraform {} provider "aws" { - region = "us-east-1" + region = "${var.aws_region}" } module "codecommit" { @@ -66,3 +68,88 @@ module "codecommit" { ] }`, string(updatedConfig)) } + +// Tests that backend part can be successfully removed from the config +// written in HCL 2.0 language +func TestDeleteTerraformBackendConfigWithHCL2Success(t *testing.T) { + tests := []struct { + config string + expected string + }{ + { + config: ` + provider "aws"{ + region = var.aws_region + }`, + expected: ` + provider "aws"{ + region = var.aws_region + }`, + }, + { + config: ` + terraform { + version = "v0.12.6" + backend "local" { + path = "path" + } + key = "value" + }`, + expected: ` + terraform { + version = "v0.12.6" + key = "value" + }`, + }, + { + config: ` + terraform {backend "s3" {}} + + provider "aws" { + region = "us-east-1" + }`, + expected: ` + terraform {} + + provider "aws" { + region = "us-east-1" + }`, + }, + } + for _, tt := range tests { + actual, err := deleteTerraformBackendConfigWithHCL2([]byte(tt.config)) + assert.Equal(t, string(actual), tt.expected) + assert.Nil(t, err) + } +} + +// Tests that trying to delete backend part from configs where +// backend secions contains parenthesis fails. See comment on +// deleteTerraformBackendConfigWithHCL2 for clarification. +func TestDeleteTerraformBackendConfigWithHCL2Failure(t *testing.T) { + tests := []struct { + config string + }{ + { + config: ` + terraform { + backend "local" { + path = "module-{{.environment}}" + } + }`, + }, + { + config: ` + terraform { + backend "concil" { + map = {"key": "val"} + } + }`, + }, + } + + for _, tt := range tests { + _, err := deleteTerraformBackendConfigWithHCL2([]byte(tt.config)) + assert.NotNil(t, err) + } +} diff --git a/astro/terraform/terraform_remote_state_disable_utils.go b/astro/terraform/terraform_remote_state_disable_utils.go index 9b9bc53..295ea08 100644 --- a/astro/terraform/terraform_remote_state_disable_utils.go +++ b/astro/terraform/terraform_remote_state_disable_utils.go @@ -19,11 +19,14 @@ package terraform import ( "bytes" "errors" + "fmt" "io/ioutil" "os" + "regexp" "github.com/uber/astro/astro/logger" + version "github.com/burl/go-version" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/printer" @@ -41,22 +44,22 @@ func astGet(l *ast.ObjectList, key string) ast.Node { return nil } -// astDel deletes the node at key from l. Returns an error if the key does not -// exist. -func astDel(l *ast.ObjectList, key string) error { +// astDelIfExists deletes the node at key from l if it exists. +// Returns true if item was deleted. +func astDelIfExists(l *ast.ObjectList, key string) bool { for i := range l.Items { for j := range l.Items[i].Keys { if l.Items[i].Keys[j].Token.Text == key { l.Items = append(l.Items[:i], l.Items[i+1:]...) - return nil + return true } } } - return errors.New("cannot delete key %v: does not exist") + return false } -func deleteTerraformBackendConfig(in []byte) (updatedConfig []byte, err error) { - config, err := parseTerraformConfig(in) +func deleteTerraformBackendConfigWithHCL1(in []byte) (updatedConfig []byte, err error) { + config, err := parseTerraformConfigWithHCL1(in) if err != nil { return nil, err } @@ -66,9 +69,7 @@ func deleteTerraformBackendConfig(in []byte) (updatedConfig []byte, err error) { return nil, errors.New("could not parse \"terraform\" block in config") } - if err := astDel(terraformConfigBlock.List, "backend"); err != nil { - return nil, err - } + astDelIfExists(terraformConfigBlock.List, "backend") buf := &bytes.Buffer{} printer.Fprint(buf, config) @@ -76,14 +77,59 @@ func deleteTerraformBackendConfig(in []byte) (updatedConfig []byte, err error) { return buf.Bytes(), nil } -func deleteTerraformBackendConfigFromFile(file string) error { +// hcl2 (used by terraform 0.12) doesn't provide interface to walk through the AST or +// to modify block values, see https://github.com/hashicorp/hcl2/issues/23 and +// https://github.com/hashicorp/hcl2/issues/88 +// As a work around we'll perform surgery directly on text, if backend config is simple. +// The method returns an error, if the config is too complicated to be parsed with the regexp. +// This method should be rewritten once hcl2 supports AST traversal and modification. +func deleteTerraformBackendConfigWithHCL2(in []byte) (updatedConfig []byte, err error) { + // Regexp to find if any backend configuration exists + backendDefinitionRe := regexp.MustCompile( + // make sure `\s` matches line breaks + `(?s)` + + // match `{backend ` or ` backend `, but not `some_backend` or ` backend_confg` + `[{\s+]backend\s+` + + // backend name and opening of the configuration, e.g. `"s3" {` + `"[^"]+"\s*{`, + ) + // Regexp to find simple backend configuration, which doesn't contain '{}' inside + backendBlockRe := regexp.MustCompile( + // make sure `\s` matches line breaks + `(?s)` + + // match backend and it's name, e.g. `backend "s3"` or ` backend "s3"`, + // note, that opening brace before `backend` is not included in the regex, + // because it should not be removed. + `(\s*backend\s+"[^"]+"\s*` + + // match backend configuration block, that doesn't have inner braces + `{[^{]*?})`, + ) + if backendDefinitionRe.Match(in) { + indexes := backendBlockRe.FindSubmatchIndex(in) + if indexes == nil { + return nil, fmt.Errorf("unable to delete backend config: unsupported syntax") + } + // Remove found backend submatch from config + return append(in[:indexes[2]], in[indexes[3]:]...), nil + } + return in, nil +} + +func deleteTerraformBackendConfig(in []byte, v *version.Version) (updatedConfig []byte, err error) { + if VersionMatches(v, "<0.12") { + return deleteTerraformBackendConfigWithHCL1(in) + } + return deleteTerraformBackendConfigWithHCL2(in) +} + +func deleteTerraformBackendConfigFromFile(file string, v *version.Version) error { logger.Trace.Printf("terraform: deleting backend config from %v", file) b, err := ioutil.ReadFile(file) if err != nil { return err } - updatedConfig, err := deleteTerraformBackendConfig(b) + updatedConfig, err := deleteTerraformBackendConfig(b, v) if err != nil { return err } @@ -107,7 +153,7 @@ func deleteTerraformBackendConfigFromFile(file string) error { return nil } -func parseTerraformConfig(in []byte) (*ast.ObjectList, error) { +func parseTerraformConfigWithHCL1(in []byte) (*ast.ObjectList, error) { astFile, err := hcl.ParseBytes(in) if err != nil { return nil, err diff --git a/astro/tests/fixtures/plan-detach/versions.tf b/astro/tests/fixtures/plan-detach/versions.tf new file mode 100644 index 0000000..bd26e2c --- /dev/null +++ b/astro/tests/fixtures/plan-detach/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 0.8" +}