From a52d20e5ed2a7ee29a518ed770e063eaee880cbe Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Sun, 26 Sep 2021 14:27:46 -0700 Subject: [PATCH 1/7] add error wrapping for locked state --- tfexec/exit_errors.go | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index 3fa64349..d1ed0b53 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -1,11 +1,13 @@ package tfexec import ( + "bytes" "context" "fmt" "os/exec" "regexp" "strings" + "text/template" ) // this file contains errors parsed from stderr @@ -30,6 +32,9 @@ var ( tfVersionMismatchErrRegexp = regexp.MustCompile(`Error: The currently running version of Terraform doesn't meet the|Error: Unsupported Terraform Core version`) 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:\nID:\s*(.*)\nPath:\s*(.*)\nOperation:\s*(.*)\nWho:\s*(.*)\nVersion:\s*(.*)\nCreated:\s*(.*)\n`) ) func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error { @@ -128,6 +133,20 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string } case configInvalidErrRegexp.MatchString(stderr): return &ErrConfigInvalid{stderr: stderr} + case stateLockErrRegexp.MatchString(stderr): + submatches := stateLockInfoRegexp.FindStringSubmatch(stderr) + if len(submatches) == 7 { + return &ErrStateLocked{ + unwrapper: unwrapper{exitErr, ctxErr}, + + ID: submatches[1], + Path: submatches[2], + Operation: submatches[3], + Who: submatches[4], + Version: submatches[5], + Created: submatches[6], + } + } } return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr) @@ -257,3 +276,33 @@ func (e *ErrTFVersionMismatch) Error() string { return fmt.Sprintf("terraform %s not supported by configuration%s", version, requirement) } + +// ErrStateLocked is returned when the state lock is already held by another process. +type ErrStateLocked struct { + unwrapper + + ID string + Path string + Operation string + Who string + Version string + Created string +} + +func (e *ErrStateLocked) Error() string { + tmpl := `Lock Info: + ID: {{.ID}} + Path: {{.Path}} + Operation: {{.Operation}} + Who: {{.Who}} + Version: {{.Version}} + Created: {{.Created}} +` + + t := template.Must(template.New("LockInfo").Parse(tmpl)) + var out bytes.Buffer + if err := t.Execute(&out, e); err != nil { + return "error acquiring the state lock" + } + return fmt.Sprintf("error acquiring the state lock: %v", out.String()) +} From 9cae73b6876c4a19387f583a0d2a46237a31799d Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Sun, 26 Sep 2021 15:09:03 -0700 Subject: [PATCH 2/7] use strings.Builder instead of bytes.Buffer --- tfexec/exit_errors.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index d1ed0b53..07d6f280 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -1,7 +1,6 @@ package tfexec import ( - "bytes" "context" "fmt" "os/exec" @@ -300,7 +299,7 @@ func (e *ErrStateLocked) Error() string { ` t := template.Must(template.New("LockInfo").Parse(tmpl)) - var out bytes.Buffer + var out strings.Builder if err := t.Execute(&out, e); err != nil { return "error acquiring the state lock" } From b17c3af18e0466bbba8f1f52a5f8e7adc8f6dc52 Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Mon, 27 Sep 2021 18:34:35 -0700 Subject: [PATCH 3/7] parse date for LockInfo created field --- tfexec/exit_errors.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index 07d6f280..d68dc865 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" "text/template" + "time" ) // this file contains errors parsed from stderr @@ -135,6 +136,10 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string case stateLockErrRegexp.MatchString(stderr): submatches := stateLockInfoRegexp.FindStringSubmatch(stderr) if len(submatches) == 7 { + created, err := time.Parse("2006-01-02 15:04:05.000000000 +0000 UTC", submatches[6]) + if err != nil { + break + } return &ErrStateLocked{ unwrapper: unwrapper{exitErr, ctxErr}, @@ -143,7 +148,7 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string Operation: submatches[3], Who: submatches[4], Version: submatches[5], - Created: submatches[6], + Created: created, } } } @@ -285,7 +290,7 @@ type ErrStateLocked struct { Operation string Who string Version string - Created string + Created time.Time } func (e *ErrStateLocked) Error() string { From 76151bbc20f268ec4ca87c330d913c3ad8359df0 Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Mon, 27 Sep 2021 18:50:31 -0700 Subject: [PATCH 4/7] revert date parsing; fix LockInfo regexp --- tfexec/exit_errors.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index d68dc865..de2568d5 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -7,7 +7,6 @@ import ( "regexp" "strings" "text/template" - "time" ) // this file contains errors parsed from stderr @@ -34,7 +33,7 @@ var ( 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:\nID:\s*(.*)\nPath:\s*(.*)\nOperation:\s*(.*)\nWho:\s*(.*)\nVersion:\s*(.*)\nCreated:\s*(.*)\n`) + stateLockInfoRegexp = regexp.MustCompile(`Lock Info:\n\s*ID:\s*(.*)\n\s*Path:\s*(.*)\n\s*Operation:\s*(.*)\n\s*Who:\s*(.*)\n\s*Version:\s*(.*)\n\s*Created:\s*(.*)\n`) ) func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error { @@ -136,10 +135,6 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string case stateLockErrRegexp.MatchString(stderr): submatches := stateLockInfoRegexp.FindStringSubmatch(stderr) if len(submatches) == 7 { - created, err := time.Parse("2006-01-02 15:04:05.000000000 +0000 UTC", submatches[6]) - if err != nil { - break - } return &ErrStateLocked{ unwrapper: unwrapper{exitErr, ctxErr}, @@ -148,7 +143,7 @@ func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string Operation: submatches[3], Who: submatches[4], Version: submatches[5], - Created: created, + Created: submatches[6], } } } @@ -290,7 +285,7 @@ type ErrStateLocked struct { Operation string Who string Version string - Created time.Time + Created string } func (e *ErrStateLocked) Error() string { From fda0bb818f3184121adfa042adb0ab57b8d1107f Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Mon, 27 Sep 2021 18:54:23 -0700 Subject: [PATCH 5/7] add test for StateLockErr; update build tags --- tfexec/cmdstring.go | 2 +- tfexec/internal/e2etest/errors_test.go | 21 ++++++++++++++++++- .../testdata/inmem-backend-locked/main.tf | 5 +++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tfexec/internal/e2etest/testdata/inmem-backend-locked/main.tf diff --git a/tfexec/cmdstring.go b/tfexec/cmdstring.go index 4f81d114..49d742e8 100644 --- a/tfexec/cmdstring.go +++ b/tfexec/cmdstring.go @@ -1,4 +1,4 @@ -// +build go1.13 +//go:build go1.13 package tfexec diff --git a/tfexec/internal/e2etest/errors_test.go b/tfexec/internal/e2etest/errors_test.go index 39d06cce..a2296c40 100644 --- a/tfexec/internal/e2etest/errors_test.go +++ b/tfexec/internal/e2etest/errors_test.go @@ -1,5 +1,5 @@ // This file contains tests that only compile/work in Go 1.13 and forward -// +build go1.13 +//go:build go1.13 package e2etest @@ -127,6 +127,25 @@ 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) { + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("err during init: %s", err) + } + + err = tf.Apply(context.Background()) + if err == nil { + t.Fatal("expected error, but didn't find one") + } + + var stateLockedErr *tfexec.ErrStateLocked + if !errors.As(err, &stateLockedErr) { + t.Fatalf("expected ErrTFVersionMismatch, got %T, %s", err, err) + } + }) +} + func TestContext_alreadyPastDeadline(t *testing.T) { runTest(t, "", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second)) diff --git a/tfexec/internal/e2etest/testdata/inmem-backend-locked/main.tf b/tfexec/internal/e2etest/testdata/inmem-backend-locked/main.tf new file mode 100644 index 00000000..9fb065d7 --- /dev/null +++ b/tfexec/internal/e2etest/testdata/inmem-backend-locked/main.tf @@ -0,0 +1,5 @@ +terraform { + backend "inmem" { + lock_id = "2b6a6738-5dd5-50d6-c0ae-f6352977666b" + } +} From a2af86d63b8ca3ba7092550699add793ca01e7c2 Mon Sep 17 00:00:00 2001 From: Alec Rabold Date: Mon, 27 Sep 2021 19:07:39 -0700 Subject: [PATCH 6/7] replace build tags --- tfexec/cmdstring.go | 2 +- tfexec/internal/e2etest/errors_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tfexec/cmdstring.go b/tfexec/cmdstring.go index 49d742e8..4f81d114 100644 --- a/tfexec/cmdstring.go +++ b/tfexec/cmdstring.go @@ -1,4 +1,4 @@ -//go:build go1.13 +// +build go1.13 package tfexec diff --git a/tfexec/internal/e2etest/errors_test.go b/tfexec/internal/e2etest/errors_test.go index a2296c40..e281a790 100644 --- a/tfexec/internal/e2etest/errors_test.go +++ b/tfexec/internal/e2etest/errors_test.go @@ -1,5 +1,5 @@ // This file contains tests that only compile/work in Go 1.13 and forward -//go:build go1.13 +// +build go1.13 package e2etest From 402d3a101ba91f8d68ecef51275a7d8bd40d74c8 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 4 Oct 2021 12:02:29 +0100 Subject: [PATCH 7/7] Update tfexec/exit_errors.go --- tfexec/exit_errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go index de2568d5..ebbfc3a9 100644 --- a/tfexec/exit_errors.go +++ b/tfexec/exit_errors.go @@ -33,7 +33,7 @@ var ( 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\s*Path:\s*(.*)\n\s*Operation:\s*(.*)\n\s*Who:\s*(.*)\n\s*Version:\s*(.*)\n\s*Created:\s*(.*)\n`) + 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`) ) func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error {