diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index 730d8be83..2e611745e 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" @@ -643,9 +644,16 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. case backend.OperationTypeApply: f = b.opApply case backend.OperationTypeRefresh: - return nil, fmt.Errorf( - "\n\nThe \"refresh\" operation is not supported when using Terraform Cloud. " + - "Use \"terraform apply -refresh-only\" instead.") + // The `terraform refresh` command has been deprecated in favor of `terraform apply -refresh-state`. + // Rather than respond with an error telling the user to run the other command we can just run + // that command instead. We will tell the user what we are doing, and then do it. + if b.CLI != nil { + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(refreshToApplyRefresh) + "\n")) + } + op.PlanMode = plans.RefreshOnlyMode + op.PlanRefresh = true + op.AutoApprove = true + f = b.opApply default: return nil, fmt.Errorf( "\n\nTerraform Cloud does not support the %q operation.", op.Type) @@ -982,6 +990,8 @@ const operationNotCanceled = ` [reset][red]The remote operation was not cancelled.[reset] ` +const refreshToApplyRefresh = `[bold][yellow]Proceeding with 'terraform apply -refresh-only -auto-approve'.[reset]` + var ( workspaceConfigurationHelp = fmt.Sprintf( `The 'workspaces' block configures how Terraform CLI maps its workspaces for this single diff --git a/internal/cloud/backend_plan.go b/internal/cloud/backend_plan.go index 4abb22ee3..1956a9c82 100644 --- a/internal/cloud/backend_plan.go +++ b/internal/cloud/backend_plan.go @@ -87,7 +87,7 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { if b.CLI != nil { header := planDefaultHeader - if op.Type == backend.OperationTypeApply { + if op.Type == backend.OperationTypeApply || op.Type == backend.OperationTypeRefresh { header = applyDefaultHeader } b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n")) diff --git a/internal/cloud/backend_refresh_test.go b/internal/cloud/backend_refresh_test.go new file mode 100644 index 000000000..3abb93577 --- /dev/null +++ b/internal/cloud/backend_refresh_test.go @@ -0,0 +1,79 @@ +package cloud + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/clistate" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/mitchellh/cli" +) + +func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { + t.Helper() + + return testOperationRefreshWithTimeout(t, configDir, 0) +} + +func testOperationRefreshWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { + t.Helper() + + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + stateLockerView := views.NewStateLocker(arguments.ViewHuman, view) + operationView := views.NewOperation(arguments.ViewHuman, false, view) + + return &backend.Operation{ + ConfigDir: configDir, + ConfigLoader: configLoader, + PlanRefresh: true, + StateLocker: clistate.NewLocker(timeout, stateLockerView), + Type: backend.OperationTypeRefresh, + View: operationView, + }, configCleanup, done +} + +func TestCloud_refreshBasicActuallyRunsApplyRefresh(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationRefresh(t, "./testdata/refresh") + defer configCleanup() + defer done(t) + + op.UIOut = b.CLI + b.CLIColor = b.cliColorize() + op.PlanMode = plans.RefreshOnlyMode + op.Workspace = testBackendSingleWorkspaceName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backend.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "Proceeding with 'terraform apply -refresh-only -auto-approve'") { + t.Fatalf("expected TFC header in output: %s", output) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} diff --git a/internal/cloud/testdata/refresh/main.tf b/internal/cloud/testdata/refresh/main.tf new file mode 100644 index 000000000..8d61d5f51 --- /dev/null +++ b/internal/cloud/testdata/refresh/main.tf @@ -0,0 +1,6 @@ +resource "random_pet" "always_new" { + keepers = { + uuid = uuid() # Force a new name each time + } + length = 3 +} diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 1bb5d3933..c7c041eb7 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -17,10 +17,17 @@ type RefreshCommand struct { } func (c *RefreshCommand) Run(rawArgs []string) int { + var diags tfdiags.Diagnostics + // Parse and apply global view arguments common, rawArgs := arguments.ParseView(rawArgs) c.View.Configure(common) + // Propagate -no-color for the remote backend's legacy use of Ui. This + // should be removed when the remote backend is migrated to views. + c.Meta.color = !common.NoColor + c.Meta.Color = c.Meta.color + // Parse and validate flags args, diags := arguments.ParseRefresh(rawArgs)