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

Further specify behaviour of terraform show #273

Merged
merged 4 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ FEATURES:
- Add support for `graph` command [GH-257]
- Add support for `taint` command [GH-251]
- Add support for `untaint` command [GH-251]
- Add `ErrStatePlanRead`, returned when Terraform cannot read a given state or plan file [GH-273]

# 0.15.0 (October 05, 2021)

Expand Down
35 changes: 29 additions & 6 deletions tfexec/exit_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ var (

usageRegexp = regexp.MustCompile(`Too many command line arguments|^Usage: .*Options:.*|Error: Invalid -\d+ option`)

noInitErrRegexp = regexp.MustCompile(`Error: Could not satisfy plugin requirements|` +
`Error: Could not load plugin|` + // v0.13
`Please run \"terraform init\"|` + // v1.1.0 early alpha versions (ref 89b05050)
`run:\s+terraform init`) // v1.1.0 (ref df578afd)
noInitErrRegexp = regexp.MustCompile(
// UNINITIALISED PROVIDERS/MODULES
`Error: Could not satisfy plugin requirements|` +
Copy link
Member Author

Choose a reason for hiding this comment

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

gofmt and I are of differing opinions as to the best indentation for this section of code.

`Error: Could not load plugin|` + // v0.13
`Please run \"terraform init\"|` + // v1.1.0 early alpha versions (ref 89b05050)
`run:\s+terraform init|` + // v1.1.0 (ref df578afd)
`Run\s+\"terraform init\"|` + // v1.2.0

// UNINITIALISED BACKENDS
`Error: Initialization required.|` + // v0.13
`Error: Backend initialization required, please run \"terraform init\"`, // v0.15
)

noConfigErrRegexp = regexp.MustCompile(`Error: No configuration files`)

Expand All @@ -33,8 +41,11 @@ var (
tfVersionMismatchConstraintRegexp = regexp.MustCompile(`required_version = "(.+)"|Required version: (.+)\b`)
configInvalidErrRegexp = regexp.MustCompile(`There are some problems with the configuration, described below.`)

stateLockErrRegexp = regexp.MustCompile(`Error acquiring the state lock`)
stateLockInfoRegexp = regexp.MustCompile(`Lock Info:\n\s*ID:\s*([^\n]+)\n\s*Path:\s*([^\n]+)\n\s*Operation:\s*([^\n]+)\n\s*Who:\s*([^\n]+)\n\s*Version:\s*([^\n]+)\n\s*Created:\s*([^\n]+)\n`)
stateLockErrRegexp = regexp.MustCompile(`Error acquiring the state lock`)
stateLockInfoRegexp = regexp.MustCompile(`Lock Info:\n\s*ID:\s*([^\n]+)\n\s*Path:\s*([^\n]+)\n\s*Operation:\s*([^\n]+)\n\s*Who:\s*([^\n]+)\n\s*Version:\s*([^\n]+)\n\s*Created:\s*([^\n]+)\n`)
statePlanReadErrRegexp = regexp.MustCompile(
`Terraform couldn't read the given file as a state or plan file.|` +
`Error: Failed to read the given file as a state or plan file`)
)

func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error {
Expand Down Expand Up @@ -147,6 +158,8 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string
Created: submatches[6],
}
}
case statePlanReadErrRegexp.MatchString(stderr):
return &ErrStatePlanRead{stderr: stderr}
}

return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr)
Expand Down Expand Up @@ -223,6 +236,16 @@ func (e *ErrNoInit) Error() string {
return e.stderr
}

type ErrStatePlanRead struct {
unwrapper

stderr string
}

func (e *ErrStatePlanRead) Error() string {
return e.stderr
}

type ErrNoConfig struct {
unwrapper

Expand Down
3 changes: 2 additions & 1 deletion tfexec/internal/e2etest/errors_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// This file contains tests that only compile/work in Go 1.13 and forward
//go:build go1.13
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth running go fmt ./... on the whole project and update all go build lines? Doesn't have to be in this PR though...

On a related note I guess we forgot to update the Readme.md to say that we now require Go 1.17 (as per #216)

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I'll add both of those to #272 as a general tidy-up, so we can get this one merged and fix all the other CI.

// +build go1.13

package e2etest
Expand Down Expand Up @@ -128,7 +129,7 @@ func TestTFVersionMismatch(t *testing.T) {
}

func TestLockedState(t *testing.T) {
runTest(t, "inmem-backend-locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
runTest(t, "inmem_backend_locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("err during init: %s", err)
Expand Down
4 changes: 2 additions & 2 deletions tfexec/internal/e2etest/force_unlock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const inmemLockID = "2b6a6738-5dd5-50d6-c0ae-f6352977666b"
var forceUnlockDirArgMaxVersion = version.Must(version.NewVersion("0.15.0"))

func TestForceUnlock(t *testing.T) {
runTest(t, "inmem-backend-locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
runTest(t, "inmem_backend_locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init: %v", err)
Expand All @@ -26,7 +26,7 @@ func TestForceUnlock(t *testing.T) {
t.Fatalf("error running ForceUnlock: %v", err)
}
})
runTest(t, "inmem-backend-locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
runTest(t, "inmem_backend_locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.GreaterThanOrEqual(forceUnlockDirArgMaxVersion) {
t.Skip("legacy positional path argument deprecated in favor of global -chdir flag")
}
Expand Down
153 changes: 152 additions & 1 deletion tfexec/internal/e2etest/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,112 @@ func TestShow(t *testing.T) {
})
}

func TestShow_errInitRequired(t *testing.T) {
func TestShow_emptyDir(t *testing.T) {
runTest(t, "empty", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(showMinVersion) {
t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
}

formatVersion := "0.1"
if tfv.Core().GreaterThanOrEqual(v1_0_1) {
formatVersion = "0.2"
}
if tfv.Core().GreaterThanOrEqual(v1_1) {
formatVersion = "1.0"
}

expected := &tfjson.State{
FormatVersion: formatVersion,
}

actual, err := tf.Show(context.Background())
if err != nil {
t.Fatal(err)
}

if diff := diffState(expected, actual); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}

func TestShow_noInitBasic(t *testing.T) {
// Prior to v1.2.0, running show before init always results in ErrNoInit.
// In the basic case, in which the local backend is implicit and there are
// no providers to download, this is unintended behaviour, as
// init is not actually necessary. This is considered a known issue in
// pre-1.2.0 versions.
runTestVersions(t, []string{testutil.Latest012, testutil.Latest013, testutil.Latest014, testutil.Latest015, testutil.Latest_v1, testutil.Latest_v1_1}, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
var noInit *tfexec.ErrNoInit
_, err := tf.Show(context.Background())
if !errors.As(err, &noInit) {
t.Fatalf("expected error ErrNoInit, got %T: %s", err, err)
}
})

// From v1.2.0 onwards, running show before init in the basic case returns
// an empty state with no error.
runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
// HACK KEM: Really I mean tfv.LessThan(version.Must(version.NewVersion("1.2.0"))),
// but I want this test to run for refs/heads/main prior to the release of v1.2.0.
if tfv.LessThan(version.Must(version.NewVersion("1.2.0"))) {

t.Skip("test applies only to v1.2.0 and greater")
}
expected := &tfjson.State{
FormatVersion: "1.0",
}

actual, err := tf.Show(context.Background())
if err != nil {
t.Fatal(err)
}

if diff := diffState(expected, actual); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}

func TestShow_noInitModule(t *testing.T) {
// Prior to v1.2.0, running show before init always results in ErrNoInit.
// In the basic case, in which the local backend is implicit and there are
// no providers to download, this is unintended behaviour, as
// init is not actually necessary. This is considered a known issue in
// pre-1.2.0 versions.
runTestVersions(t, []string{testutil.Latest012, testutil.Latest013, testutil.Latest014, testutil.Latest015, testutil.Latest_v1, testutil.Latest_v1_1}, "registry_module", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
var noInit *tfexec.ErrNoInit
_, err := tf.Show(context.Background())
if !errors.As(err, &noInit) {
t.Fatalf("expected error ErrNoInit, got %T: %s", err, err)
}
})

// From v1.2.0 onwards, running show before init in the basic case returns
// an empty state with no error.
runTest(t, "registry_module", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
// HACK KEM: Really I mean tfv.LessThan(version.Must(version.NewVersion("1.2.0"))),
// but I want this test to run for refs/heads/main prior to the release of v1.2.0.
if tfv.LessThanOrEqual(version.Must(version.NewVersion(testutil.Latest_v1_1))) {
t.Skip("test applies only to v1.2.0 and greater")
}
expected := &tfjson.State{
FormatVersion: "1.0",
}

actual, err := tf.Show(context.Background())
if err != nil {
t.Fatal(err)
}

if diff := diffState(expected, actual); diff != "" {
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
})
}

func TestShow_noInitNonLocalBackend(t *testing.T) {
runTest(t, "inmem_backend", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(showMinVersion) {
t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
}
Expand All @@ -95,6 +199,53 @@ func TestShow_errInitRequired(t *testing.T) {
})
}

func TestShow_noInitLocalBackendNonDefaultState(t *testing.T) {
runTest(t, "local_backend_non_default_state", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(showMinVersion) {
t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
}

var noInit *tfexec.ErrNoInit
_, err := tf.Show(context.Background())
if !errors.As(err, &noInit) {
t.Fatalf("expected error ErrNoInit, got %T: %s", err, err)
}
})
}

func TestShow_noInitEtcdBackend(t *testing.T) {
runTest(t, "etcd_backend", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(showMinVersion) {
t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
}

var noInit *tfexec.ErrNoInit
_, err := tf.Show(context.Background())
if !errors.As(err, &noInit) {
t.Fatalf("expected error ErrNoInit, got %T: %s", err, err)
}
})
}

func TestShow_statefileDoesNotExist(t *testing.T) {
runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
if tfv.LessThan(showMinVersion) {
t.Skip("terraform show was added in Terraform 0.12, so test is not valid")
}

err := tf.Init(context.Background())
if err != nil {
t.Fatalf("error running Init in test directory: %s", err)
}

var statePlanReadErr *tfexec.ErrStatePlanRead
_, err = tf.ShowStateFile(context.Background(), "statefilefoo")
if !errors.As(err, &statePlanReadErr) {
t.Fatalf("expected error ErrStatePlanRead, got %T: %s", err, err)
}
})
}

func TestShow_versionMismatch(t *testing.T) {
runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
// only testing versions without show
Expand Down
Empty file.
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/etcd_backend/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
terraform {
backend "etcd" {
}
}
4 changes: 4 additions & 0 deletions tfexec/internal/e2etest/testdata/inmem_backend/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
terraform {
backend "inmem" {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
backend "local" {
path = "foo.tfstate"
}
}