Skip to content

Commit

Permalink
Merge pull request #157 from minamijoyo/tf-1.6
Browse files Browse the repository at this point in the history
Add support for Terraform v1.6
  • Loading branch information
minamijoyo authored Oct 5, 2023
2 parents 5eb2a01 + 72284a7 commit 2c48951
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ jobs:
fail-fast: false
matrix:
terraform:
- 1.6.0
- 1.5.7
- 1.4.7
- 1.3.10
- 0.12.31
env:
TERRAFORM_VERSION: ${{ matrix.terraform }}
Expand Down
3 changes: 2 additions & 1 deletion test-fixtures/backend_s3/config.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ terraform {
bucket = "tfstate-test"
key = "test/terraform.tfstate"

# mock s3 endpoint with localstack
# mock s3/iam endpoint with localstack
endpoint = "http://localstack:4566"
iam_endpoint = "http://localstack:4566"
access_key = "dummy"
secret_key = "dummy"
skip_credentials_validation = true
Expand Down
3 changes: 2 additions & 1 deletion test-fixtures/storage_gcs/config.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ terraform {
bucket = "tfstate-test"
key = "test/terraform.tfstate"

# mock s3 endpoint with localstack
# mock s3/iam endpoint with localstack
endpoint = "http://localstack:4566"
iam_endpoint = "http://localstack:4566"
access_key = "dummy"
secret_key = "dummy"
skip_credentials_validation = true
Expand Down
3 changes: 2 additions & 1 deletion test-fixtures/storage_s3/config.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ terraform {
bucket = "tfstate-test"
key = "test/terraform.tfstate"

# mock s3 endpoint with localstack
# mock s3/iam endpoint with localstack
endpoint = "http://localstack:4566"
iam_endpoint = "http://localstack:4566"
access_key = "dummy"
secret_key = "dummy"
skip_credentials_validation = true
Expand Down
14 changes: 3 additions & 11 deletions tfexec/terraform_providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ package tfexec

import (
"context"
"fmt"
"reflect"
"testing"

"github.com/hashicorp/go-version"
)

var providersStdout = `
Expand Down Expand Up @@ -100,18 +97,13 @@ resource "null_resource" "bar" {}
t.Fatalf("failed to run terraform providers: %s", err)
}

v, err := terraformCLI.Version(context.Background())
if err != nil {
t.Fatalf("unexpected version error: %s", err)
}

constraints, err := version.NewConstraint(fmt.Sprintf(">= %s", MinimumTerraformVersionForStateReplaceProvider))
supportsStateReplaceProvider, _, err := terraformCLI.SupportsStateReplaceProvider(context.Background())
if err != nil {
t.Fatalf("unexpected version constraint error: %s", err)
t.Fatalf("failed to determine if Terraform version supports state replace-provider: %s", err)
}

want := providersStdout
if !constraints.Check(v) {
if !supportsStateReplaceProvider {
want = legacyProvidersStdout
}

Expand Down
7 changes: 6 additions & 1 deletion tfexec/terraform_state_replace_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ func (c *terraformCLI) SupportsStateReplaceProvider(ctx context.Context) (bool,
return false, constraints, err
}

if !constraints.Check(v) {
ver, err := truncatePreReleaseVersion(v)
if err != nil {
return false, constraints, err
}

if !constraints.Check(ver) {
return false, constraints, nil
}

Expand Down
5 changes: 3 additions & 2 deletions tfexec/terraform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,17 @@ terraform {
# bucket = "tfstate-test"
key = "%s/terraform.tfstate"
# mock s3 endpoint with localstack
# mock s3/iam endpoint with localstack
endpoint = "%s"
iam_endpoint = "%s"
access_key = "dummy"
secret_key = "dummy"
skip_credentials_validation = true
skip_metadata_api_check = true
force_path_style = true
}
}
`, t.Name(), endpoint)
`, t.Name(), endpoint, endpoint)
source := `
resource "null_resource" "foo" {}
resource "null_resource" "bar" {}
Expand Down
22 changes: 22 additions & 0 deletions tfexec/terraform_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"regexp"
"strings"

"github.com/hashicorp/go-version"
)
Expand All @@ -29,3 +30,24 @@ func (c *terraformCLI) Version(ctx context.Context) (*version.Version, error) {

return version, nil
}

// truncatePreReleaseVersion is a helper function that removes
// pre-release information.
// The hashicorp/go-version returns false when comparing pre-releases, for
// example 1.6.0-rc1 >= 0.13. This is counter-intuitive for determining the
// presence or absence of a feature, so remove the pre-release information
// before comparing.
func truncatePreReleaseVersion(v *version.Version) (*version.Version, error) {
if v.Prerelease() == "" {
return v, nil
}

vs, _, _ := strings.Cut(v.String(), "-")

ver, err := version.NewVersion(vs)
if err != nil {
return nil, err
}

return ver, nil
}
43 changes: 43 additions & 0 deletions tfexec/terraform_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"
"testing"

"github.com/hashicorp/go-version"
)

func TestTerraformCLIVersion(t *testing.T) {
Expand Down Expand Up @@ -87,3 +89,44 @@ func TestAccTerraformCLIVersion(t *testing.T) {
}
fmt.Printf("got = %s\n", got)
}

func TestTruncatePreReleaseVersion(t *testing.T) {
cases := []struct {
desc string
v string
want string
ok bool
}{
{
desc: "pre-release",
v: "1.6.0-rc1",
want: "1.6.0",
ok: true,
},
{
desc: "not pre-release",
v: "1.6.0",
want: "1.6.0",
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
v, err := version.NewVersion(tc.v)
if err != nil {
t.Fatalf("failed to parse version: %s", err)
}
got, err := truncatePreReleaseVersion(v)
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatalf("expected to return an error, but no error, got = %s", got)
}
if tc.ok && got.String() != tc.want {
t.Errorf("got: %s, want: %s", got, tc.want)
}
})
}
}
5 changes: 3 additions & 2 deletions tfexec/test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,17 @@ terraform {
bucket = "%s"
key = "%s"
# mock s3 endpoint with localstack
# mock s3/iam endpoint with localstack
endpoint = "%s"
iam_endpoint = "%s"
access_key = "%s"
secret_key = "%s"
skip_credentials_validation = true
skip_metadata_api_check = true
force_path_style = true
}
}
`, TestS3Region, TestS3Bucket, key, endpoint, TestS3AccessKey, TestS3SecretKey)
`, TestS3Region, TestS3Bucket, key, endpoint, endpoint, TestS3AccessKey, TestS3SecretKey)
return backendConfig
}

Expand Down
10 changes: 4 additions & 6 deletions tfmigrate/multi_state_migrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1010,9 +1010,8 @@ resource "null_resource" "qux" {}
t.Fatalf("expected migrator plan error")
}

expected := "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
if !containsBucketRequiredError(err) {
t.Fatalf("expected migrator plan error to contain bucket required error: %s", err.Error())
}
}

Expand Down Expand Up @@ -1130,8 +1129,7 @@ resource "null_resource" "qux" {}
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}

expected = "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
if !containsBucketRequiredError(err) {
t.Fatalf("expected migrator plan error to contain bucket required error: %s", err.Error())
}
}
10 changes: 4 additions & 6 deletions tfmigrate/state_migrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,8 @@ resource "null_resource" "bar" {}
t.Fatalf("expected migrator plan error")
}

expected := "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
if !containsBucketRequiredError(err) {
t.Fatalf("expected migrator plan error to contain bucket required error: %s", err.Error())
}
}

Expand Down Expand Up @@ -618,8 +617,7 @@ resource "null_resource" "bar" {}
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
}

expected = "Error: \"bucket\": required field is not set"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected migrator plan error to contain %s, got: %s", expected, err.Error())
if !containsBucketRequiredError(err) {
t.Fatalf("expected migrator plan error to contain bucket required error: %s", err.Error())
}
}
16 changes: 16 additions & 0 deletions tfmigrate/test_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package tfmigrate

import "regexp"

// SwitchBackToRemoteFuncError tests verify error messages, but the
// error message for missing bucket key in the s3 backend differs
// depending on the Terraform version.
// Define a helper function to hide the difference.
const testBucketRequiredErrorLegacyTF = `Error: "bucket": required field is not set`
const testBucketRequiredErrorTF16 = `The attribute "bucket" is required by the backend`

var testBucketRequiredErrorRE = regexp.MustCompile(testBucketRequiredErrorLegacyTF + `|` + testBucketRequiredErrorTF16)

func containsBucketRequiredError(err error) bool {
return testBucketRequiredErrorRE.MatchString(err.Error())
}
50 changes: 50 additions & 0 deletions tfmigrate/test_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package tfmigrate

import (
"errors"
"testing"
)

func TestContainsBucketRequiredError(t *testing.T) {
cases := []struct {
desc string
msg string
want bool
}{
{
desc: "terraform v1.5",
msg: `Error: "bucket": required field is not set`,
want: true,
},
{
desc: "terraform v1.6",
msg: `
Error: Missing Required Value
on main.tf line 4, in terraform:
4: backend "s3" {
The attribute "bucket" is required by the backend.
Refer to the backend documentation for additional information which
attributes are required.
`,
want: true,
},
{
desc: "unknown",
msg: `Error: unknown`,
want: false,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got := containsBucketRequiredError(errors.New(tc.msg))
if got != tc.want {
t.Errorf("got: %t, want: %t", got, tc.want)
}
})
}
}

0 comments on commit 2c48951

Please sign in to comment.