diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index ea25b2a5..9fc152dd 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -46,6 +46,7 @@ var ( 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`) + lockIdInvalidErrRegexp = regexp.MustCompile(`Failed to unlock state: `) ) func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error { @@ -160,6 +161,8 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string } case statePlanReadErrRegexp.MatchString(stderr): return &ErrStatePlanRead{stderr: stderr} + case lockIdInvalidErrRegexp.MatchString(stderr): + return &ErrLockIdInvalid{stderr: stderr} } return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr) @@ -256,6 +259,16 @@ func (e *ErrNoConfig) Error() string { return e.stderr } +type ErrLockIdInvalid struct { + unwrapper + + stderr string +} + +func (e *ErrLockIdInvalid) Error() string { + return e.stderr +} + // ErrCLIUsage is returned when the combination of flags or arguments is incorrect. // // CLI indicates usage errors in three different ways: either diff --git a/tfexec/force_unlock.go b/tfexec/force_unlock.go index e501baf5..de95f547 100644 --- a/tfexec/force_unlock.go +++ b/tfexec/force_unlock.go @@ -2,6 +2,7 @@ package tfexec import ( "context" + "fmt" "os/exec" ) @@ -21,7 +22,10 @@ func (opt *DirOption) configureForceUnlock(conf *forceUnlockConfig) { // ForceUnlock represents the `terraform force-unlock` command func (tf *Terraform) ForceUnlock(ctx context.Context, lockID string, opts ...ForceUnlockOption) error { - unlockCmd := tf.forceUnlockCmd(ctx, lockID, opts...) + unlockCmd, err := tf.forceUnlockCmd(ctx, lockID, opts...) + if err != nil { + return err + } if err := tf.runTerraformCmd(ctx, unlockCmd); err != nil { return err @@ -30,7 +34,7 @@ func (tf *Terraform) ForceUnlock(ctx context.Context, lockID string, opts ...For return nil } -func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) *exec.Cmd { +func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) (*exec.Cmd, error) { c := defaultForceUnlockOptions for _, o := range opts { @@ -43,8 +47,12 @@ func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ... // optional positional arguments if c.dir != "" { + err := tf.compatible(ctx, nil, tf0_15_0) + if err != nil { + return nil, fmt.Errorf("[DIR] option was removed in Terraform v0.15.0") + } args = append(args, c.dir) } - return tf.buildTerraformCmd(ctx, nil, args...) + return tf.buildTerraformCmd(ctx, nil, args...), nil } diff --git a/tfexec/force_unlock_test.go b/tfexec/force_unlock_test.go new file mode 100644 index 00000000..b817979b --- /dev/null +++ b/tfexec/force_unlock_test.go @@ -0,0 +1,63 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestForceUnlockCmd(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1_1)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + forceUnlockCmd, err := tf.forceUnlockCmd(context.Background(), "12345") + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "force-unlock", + "-no-color", + "-force", + "12345", + }, nil, forceUnlockCmd) + }) +} + +// The optional final positional [DIR] argument is available +// until v0.15.0. +func TestForceUnlockCmd_pre015(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest014)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("override all defaults", func(t *testing.T) { + forceUnlockCmd, err := tf.forceUnlockCmd(context.Background(), "12345", Dir("mydir")) + if err != nil { + t.Fatal(err) + } + + assertCmd(t, []string{ + "force-unlock", + "-no-color", + "-force", + "12345", + "mydir", + }, nil, forceUnlockCmd) + }) +} diff --git a/tfexec/internal/e2etest/force_unlock_test.go b/tfexec/internal/e2etest/force_unlock_test.go index 46657275..c5d626ac 100644 --- a/tfexec/internal/e2etest/force_unlock_test.go +++ b/tfexec/internal/e2etest/force_unlock_test.go @@ -2,6 +2,7 @@ package e2etest import ( "context" + "errors" "testing" "github.com/hashicorp/go-version" @@ -40,4 +41,19 @@ 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) { + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init: %v", err) + } + + err = tf.ForceUnlock(context.Background(), "badlockid") + if err == nil { + t.Fatalf("expected error when running ForceUnlock with invalid lock id") + } + var foErr *tfexec.ErrLockIdInvalid + if !errors.As(err, &foErr) { + t.Fatalf("expected ErrLockIdInvalid, %T returned: %s", err, err) + } + }) }