From 9d7fdbea2d7756dd9dafa7a5cfa76b728bb3fa56 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 13 Apr 2022 13:34:11 -0400 Subject: [PATCH] Handle -input=false in cloud integration For non-interactive contexts, Terraform is typically executed with the flag -input=false. However for runs that are not set to auto approve, the cloud integration will prompt a user for approval input even with input being set to false. This commit enables the cloud integration to know the value of the input flag and use it to determine whether or not to ask the user for input. If -input is set to false and the run cannot be auto approved, the cloud integration will throw an error stating run confirmation can no longer be handled in the CLI and that they must do so through the browser. --- internal/cloud/backend.go | 4 ++ internal/cloud/backend_apply.go | 4 +- internal/cloud/backend_cli.go | 1 + internal/cloud/backend_common.go | 11 +--- .../cloud/e2e/apply_no_input_flag_test.go | 58 +++++++++++++++++++ internal/cloud/errors.go | 13 +++++ internal/cloud/testing.go | 1 + 7 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 internal/cloud/e2e/apply_no_input_flag_test.go diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index 6f64a9476c..a5ced6c12e 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -89,6 +89,10 @@ type Cloud struct { ignoreVersionConflict bool runningInAutomation bool + + // input stores the value of the -input flag, since it will be used + // to determine whether or not to ask the user for approval of a run. + input bool } var _ backend.Backend = (*Cloud)(nil) diff --git a/internal/cloud/backend_apply.go b/internal/cloud/backend_apply.go index 85c447e602..ff22dd5a90 100644 --- a/internal/cloud/backend_apply.go +++ b/internal/cloud/backend_apply.go @@ -100,7 +100,7 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove - if mustConfirm { + if mustConfirm && b.input { opts := &terraform.InputOpts{Id: "approve"} if op.PlanMode == plans.DestroyMode { @@ -117,6 +117,8 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio if err != nil && err != errRunApproved { return r, err } + } else if mustConfirm && !b.input { + return r, errApplyNeedsUIConfirmation } else { // If we don't need to ask for confirmation, insert a blank // line to separate the ouputs. diff --git a/internal/cloud/backend_cli.go b/internal/cloud/backend_cli.go index cd05496163..62eb834cac 100644 --- a/internal/cloud/backend_cli.go +++ b/internal/cloud/backend_cli.go @@ -16,6 +16,7 @@ func (b *Cloud) CLIInit(opts *backend.CLIOpts) error { b.CLIColor = opts.CLIColor b.ContextOpts = opts.ContextOpts b.runningInAutomation = opts.RunningInAutomation + b.input = opts.Input return nil } diff --git a/internal/cloud/backend_common.go b/internal/cloud/backend_common.go index 05fbadfba1..2f17ff5788 100644 --- a/internal/cloud/backend_common.go +++ b/internal/cloud/backend_common.go @@ -3,7 +3,6 @@ package cloud import ( "bufio" "context" - "errors" "fmt" "io" "math" @@ -17,14 +16,6 @@ import ( "github.com/hashicorp/terraform/internal/terraform" ) -var ( - errApplyDiscarded = errors.New("Apply discarded.") - errDestroyDiscarded = errors.New("Destroy discarded.") - errRunApproved = errors.New("approved using the UI or API") - errRunDiscarded = errors.New("discarded using the UI or API") - errRunOverridden = errors.New("overridden using the UI or API") -) - var ( backoffMin = 1000.0 backoffMax = 3000.0 @@ -388,6 +379,8 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) } + } else if !b.input { + return errPolicyOverrideNeedsUIConfirmation } else { opts := &terraform.InputOpts{ Id: "override", diff --git a/internal/cloud/e2e/apply_no_input_flag_test.go b/internal/cloud/e2e/apply_no_input_flag_test.go new file mode 100644 index 0000000000..1836f730b3 --- /dev/null +++ b/internal/cloud/e2e/apply_no_input_flag_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "testing" +) + +func Test_apply_no_input_flag(t *testing.T) { + t.Parallel() + skipIfMissingEnvVar(t) + + cases := testCases{ + "terraform apply with -input=false": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "new-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-input=false"}, + expectedCmdOutput: `Terraform Cloud has been successfully initialized`, + }, + { + command: []string{"apply", "-input=false"}, + expectedCmdOutput: `Cannot confirm apply due to -input=false. Please handle run confirmation in the UI.`, + expectError: true, + }, + }, + }, + }, + }, + "terraform apply with auto approve and -input=false": { + operations: []operationSets{ + { + prep: func(t *testing.T, orgName, dir string) { + wsName := "cloud-workspace" + tfBlock := terraformConfigCloudBackendName(orgName, wsName) + writeMainTF(t, tfBlock, dir) + }, + commands: []tfCommand{ + { + command: []string{"init", "-input=false"}, + expectedCmdOutput: `Terraform Cloud has been successfully initialized`, + }, + { + command: []string{"apply", "-auto-approve", "-input=false"}, + expectedCmdOutput: `Apply complete!`, + }, + }, + }, + }, + }, + } + + testRunner(t, cases, 1) +} diff --git a/internal/cloud/errors.go b/internal/cloud/errors.go index f387a37538..cf668516f3 100644 --- a/internal/cloud/errors.go +++ b/internal/cloud/errors.go @@ -1,6 +1,7 @@ package cloud import ( + "errors" "fmt" "strings" @@ -8,6 +9,18 @@ import ( "github.com/zclconf/go-cty/cty" ) +// String based errors +var ( + errApplyDiscarded = errors.New("Apply discarded.") + errDestroyDiscarded = errors.New("Destroy discarded.") + errRunApproved = errors.New("approved using the UI or API") + errRunDiscarded = errors.New("discarded using the UI or API") + errRunOverridden = errors.New("overridden using the UI or API") + errApplyNeedsUIConfirmation = errors.New("Cannot confirm apply due to -input=false. Please handle run confirmation in the UI.") + errPolicyOverrideNeedsUIConfirmation = errors.New("Cannot override soft failed policy checks when -input=false. Please open the run in the UI to override.") +) + +// Diagnostic error messages var ( invalidWorkspaceConfigMissingValues = tfdiags.AttributeValue( tfdiags.Error, diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index e5dd538066..4297540fca 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -154,6 +154,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { // Set local to a local test backend. b.local = testLocalBackend(t, b) + b.input = true ctx := context.Background()