From 3979aec0ae7685e8b9ae6af558252b30bc848e90 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Mon, 1 Oct 2018 10:18:59 +0200 Subject: [PATCH] Ask to cancel a pending remote operation Except when a lock-timeout has exceeded or auto-approve is set. --- backend/remote/backend.go | 58 +++++++++++++++++++++------- backend/remote/backend_apply_test.go | 5 ++- backend/remote/backend_mock.go | 1 + backend/remote/backend_plan.go | 7 +++- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 7c2457d73..c5684ad9d 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -440,7 +440,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend } if r != nil && err == context.Canceled { - runningOp.Err = b.cancel(cancelCtx, r) + runningOp.Err = b.cancel(cancelCtx, op, r.ID) } }() @@ -448,21 +448,45 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend return runningOp, nil } -func (b *Remote) cancel(cancelCtx context.Context, r *tfe.Run) error { +func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, runID string) error { // Retrieve the run to get its current status. - r, err := b.client.Runs.Read(cancelCtx, r.ID) + r, err := b.client.Runs.Read(cancelCtx, runID) if err != nil { return generalError("error cancelling run", err) } - // Make sure we cancel the run if possible. if r.Status == tfe.RunPending && r.Actions.IsCancelable { + // Only ask if the remote operation should be canceled + // if the auto approve flag is not set. + if !op.AutoApprove { + v, err := op.UIIn.Input(&terraform.InputOpts{ + Id: "cancel", + Query: "\nDo you want to cancel the pending remote operation?", + Description: "Only 'yes' will be accepted to cancel.", + }) + if err != nil { + return generalError("error asking to cancel", err) + } + if v != "yes" { + if b.CLI != nil { + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationNotCanceled))) + } + return nil + } + } else { + if b.CLI != nil { + // Insert a blank line to separate the ouputs. + b.CLI.Output("") + } + } + + // Try to cancel the remote operation. err = b.client.Runs.Cancel(cancelCtx, r.ID, tfe.RunCancelOptions{}) if err != nil { return generalError("error cancelling run", err) } if b.CLI != nil { - b.CLI.Output(b.Colorize().Color(strings.TrimSpace(cancelPendingOperation))) + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationCanceled))) } } @@ -470,7 +494,7 @@ func (b *Remote) cancel(cancelCtx context.Context, r *tfe.Run) error { } // Colorize returns the Colorize structure that can be used for colorizing -// output. This is gauranteed to always return a non-nil value and so is useful +// output. This is guaranteed to always return a non-nil value and so useful // as a helper to wrap any potentially colored strings. func (b *Remote) Colorize() *colorstring.Colorize { if b.CLIColor != nil { @@ -497,6 +521,15 @@ func generalError(msg string, err error) error { } } +const generalErr = ` +%s: %v + +The configured "remote" backend encountered an unexpected error. Sometimes +this is caused by network connection problems, in which case you could retry +the command. If the issue persists please open a support ticket to get help +resolving the problem. +` + const notFoundErr = ` %s: %v @@ -505,17 +538,12 @@ that do not exist, as well as for resources that a user doesn't have access to. When the resource does exists, please check the rights for the used token. ` -const generalErr = ` -%s: %v - -The "remote" backend encountered an unexpected error while communicating -with remote backend. In some cases this could be caused by a network -connection problem, in which case you could retry the command. If the issue -persists please open a support ticket to get help resolving the problem. +const operationCanceled = ` +[reset][red]The remote operation was successfully cancelled.[reset] ` -const cancelPendingOperation = `[reset][red] -Pending remote operation cancelled.[reset] +const operationNotCanceled = ` +[reset][red]The remote operation was not cancelled.[reset] ` var schemaDescriptions = map[string]string{ diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index 7e1684aef..27b8b901e 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -297,6 +297,7 @@ func TestRemote_applyLockTimeout(t *testing.T) { defer modCleanup() input := testInput(t, map[string]string{ + "cancel": "yes", "approve": "yes", }) @@ -322,8 +323,8 @@ func TestRemote_applyLockTimeout(t *testing.T) { t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds") } - if len(input.answers) != 1 { - t.Fatalf("expected an unused answer, got: %v", input.answers) + if len(input.answers) != 2 { + t.Fatalf("expected unused answers, got: %v", input.answers) } output := b.CLI.(*cli.MockUi).OutputWriter.String() diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index e391ff5f6..cda8ffa9a 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -595,6 +595,7 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t logs, _ := ioutil.ReadFile(m.client.Plans.logs[p.LogReadURL]) if r.IsDestroy || !bytes.Contains(logs, []byte("No changes. Infrastructure is up-to-date.")) { + r.Actions.IsCancelable = true r.Actions.IsConfirmable = true r.HasChanges = true r.Permissions.CanApply = true diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go index 4ed810213..aec4fe9e3 100644 --- a/backend/remote/backend_plan.go +++ b/backend/remote/backend_plan.go @@ -140,10 +140,15 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, return } - if r.Status == tfe.RunPending { + if r.Status == tfe.RunPending && r.Actions.IsCancelable { if b.CLI != nil { b.CLI.Output(b.Colorize().Color(strings.TrimSpace(lockTimeoutErr))) } + + // We abuse the auto aprove flag to indicate that we do not + // want to ask if the remote operation should be canceled. + op.AutoApprove = true + p, err := os.FindProcess(os.Getpid()) if err != nil { log.Printf("[ERROR] error searching process ID: %v", err)