Run apply -refresh-state instead of refresh

When a user runs `terraform refresh` we give them an error message that
tells them to run `terraform apply -refresh-state`. We could just run
that command for them, though. That is what this PR does.
This commit is contained in:
Barrett Clark 2021-10-08 14:36:37 -05:00 committed by Chris Arcand
parent bf02b5cb53
commit edbc84420c
5 changed files with 106 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-svchost/disco"
"github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs/configschema" "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/remote"
"github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/terraform"
@ -643,9 +644,16 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend.
case backend.OperationTypeApply: case backend.OperationTypeApply:
f = b.opApply f = b.opApply
case backend.OperationTypeRefresh: case backend.OperationTypeRefresh:
return nil, fmt.Errorf( // The `terraform refresh` command has been deprecated in favor of `terraform apply -refresh-state`.
"\n\nThe \"refresh\" operation is not supported when using Terraform Cloud. " + // Rather than respond with an error telling the user to run the other command we can just run
"Use \"terraform apply -refresh-only\" instead.") // 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: default:
return nil, fmt.Errorf( return nil, fmt.Errorf(
"\n\nTerraform Cloud does not support the %q operation.", op.Type) "\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] [reset][red]The remote operation was not cancelled.[reset]
` `
const refreshToApplyRefresh = `[bold][yellow]Proceeding with 'terraform apply -refresh-only -auto-approve'.[reset]`
var ( var (
workspaceConfigurationHelp = fmt.Sprintf( workspaceConfigurationHelp = fmt.Sprintf(
`The 'workspaces' block configures how Terraform CLI maps its workspaces for this single `The 'workspaces' block configures how Terraform CLI maps its workspaces for this single

View File

@ -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) { func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) {
if b.CLI != nil { if b.CLI != nil {
header := planDefaultHeader header := planDefaultHeader
if op.Type == backend.OperationTypeApply { if op.Type == backend.OperationTypeApply || op.Type == backend.OperationTypeRefresh {
header = applyDefaultHeader header = applyDefaultHeader
} }
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n")) b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n"))

View File

@ -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())
}
}

View File

@ -0,0 +1,6 @@
resource "random_pet" "always_new" {
keepers = {
uuid = uuid() # Force a new name each time
}
length = 3
}

View File

@ -17,10 +17,17 @@ type RefreshCommand struct {
} }
func (c *RefreshCommand) Run(rawArgs []string) int { func (c *RefreshCommand) Run(rawArgs []string) int {
var diags tfdiags.Diagnostics
// Parse and apply global view arguments // Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs) common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common) 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 // Parse and validate flags
args, diags := arguments.ParseRefresh(rawArgs) args, diags := arguments.ParseRefresh(rawArgs)