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

Add detach support which is compatible with hcl2 used for terraform 0.12 #45

Merged
merged 9 commits into from
Sep 27, 2019
7 changes: 5 additions & 2 deletions astro/terraform/terraform_remote_state_disable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
95 changes: 91 additions & 4 deletions astro/terraform/terraform_remote_state_disable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this to explicitly show that this is HCL 1.0

}

module "codecommit" {
Expand All @@ -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" {
Expand All @@ -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 {
btromanova marked this conversation as resolved.
Show resolved Hide resolved
config string
expected string
}{
{
config: `
Copy link
Contributor

Choose a reason for hiding this comment

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

This still looks weird. Is it because of a tabs/spaces mix?

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)
}
}
72 changes: 59 additions & 13 deletions astro/terraform/terraform_remote_state_disable_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}
Expand All @@ -66,24 +69,67 @@ 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")
dansimau marked this conversation as resolved.
Show resolved Hide resolved

buf := &bytes.Buffer{}
printer.Fprint(buf, config)

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
}
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions astro/tests/fixtures/plan-detach/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
terraform {
required_version = ">= 0.8"
}