Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions backend/remote/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"

tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
Expand Down Expand Up @@ -67,14 +68,6 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
))
}

if op.Targets != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Resource targeting is currently not supported",
`The "remote" backend does not support resource targeting at this time.`,
))
}

if b.hasExplicitVariableValues(op) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down Expand Up @@ -102,6 +95,26 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
))
}

if len(op.Targets) != 0 {
// For API versions prior to 2.3, RemoteAPIVersion will return an empty string,
// so if there's an error when parsing the RemoteAPIVersion, it's handled as
// equivalent to an API version < 2.3.
currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion())
desiredAPIVersion, _ := version.NewVersion("2.3")

if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Resource targeting is not supported",
fmt.Sprintf(
`The host %s does not support the -target option for `+
`remote plans.`,
b.hostname,
),
))
}
}

// Return if there are any errors.
if diags.HasErrors() {
return nil, diags.Err()
Expand Down
22 changes: 15 additions & 7 deletions backend/remote/backend_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/backend"
Expand Down Expand Up @@ -278,16 +279,23 @@ func TestRemote_applyWithTarget(t *testing.T) {
}

<-run.Done()
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
if run.Result != backend.OperationSuccess {
t.Fatal("expected apply operation to succeed")
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
if run.PlanEmpty {
t.Fatalf("expected plan to be non-empty")
}

errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
if !strings.Contains(errOutput, "targeting is currently not supported") {
t.Fatalf("expected a targeting error, got: %v", errOutput)
// We should find a run inside the mock client that has the same
// target address we requested above.
runsAPI := b.client.Runs.(*mockRuns)
if got, want := len(runsAPI.runs), 1; got != want {
t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
}
for _, run := range runsAPI.runs {
if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions backend/remote/backend_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Op
b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n"))
}
continue
case "skipped_due_to_targeting": // TEMP: not available in the go-tfe library yet; will update this to be tfe.CostEstimateSkippedDueToTargeting once that's available.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're planning to submit this tfe.CostEstimateSkippedDueToTargeting upstream to go-tfe asynchronously from getting this PR in. Will update this to use the named constant in a subsequent PR once it's included in a go-tfe release.

b.CLI.Output("Not available for this plan, because it was created with the -target option.")
b.CLI.Output("\n------------------------------------------------------------------------")
return nil
case tfe.CostEstimateErrored:
return fmt.Errorf(msgPrefix + " errored.")
case tfe.CostEstimateCanceled:
Expand Down
58 changes: 42 additions & 16 deletions backend/remote/backend_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,17 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
}

// Get the remote workspace name.
workspace := op.Workspace
switch {
case op.Workspace == backend.DefaultStateName:
workspace = b.workspace
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
workspace = b.prefix + op.Workspace
}
remoteWorkspaceName := b.getRemoteWorkspaceName(op.Workspace)

// Get the latest state.
log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", workspace)
log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", remoteWorkspaceName)
stateMgr, err := b.StateMgr(op.Workspace)
if err != nil {
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, diags
}

log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", workspace)
log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", remoteWorkspaceName)
if err := op.StateLocker.Lock(stateMgr, op.Type.String()); err != nil {
diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err))
return nil, nil, diags
Expand All @@ -63,7 +57,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
}
}()

log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", workspace)
log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", remoteWorkspaceName)
if err := stateMgr.RefreshState(); err != nil {
diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
return nil, nil, diags
Expand All @@ -83,7 +77,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
// Load the latest state. If we enter contextFromPlanFile below then the
// state snapshot in the plan file must match this, or else it'll return
// error diagnostics.
log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", workspace)
log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", remoteWorkspaceName)
opts.State = stateMgr.State()

log.Printf("[TRACE] backend/remote: loading configuration for the current working directory")
Expand All @@ -94,11 +88,17 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
}
opts.Config = config

log.Printf("[TRACE] backend/remote: retrieving variables from workspace %q", workspace)
tfeVariables, err := b.client.Variables.List(context.Background(), tfe.VariableListOptions{
Organization: tfe.String(b.organization),
Workspace: tfe.String(workspace),
})
// The underlying API expects us to use the opaque workspace id to request
// variables, so we'll need to look that up using our organization name
// and workspace name.
remoteWorkspaceID, err := b.getRemoteWorkspaceID(context.Background(), op.Workspace)
if err != nil {
diags = diags.Append(errwrap.Wrapf("Error finding remote workspace: {{err}}", err))
return nil, nil, diags
}

log.Printf("[TRACE] backend/remote: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID)
tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, tfe.VariableListOptions{})
if err != nil && err != tfe.ErrResourceNotFound {
diags = diags.Append(errwrap.Wrapf("Error loading variables: {{err}}", err))
return nil, nil, diags
Expand Down Expand Up @@ -142,6 +142,32 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu
return tfCtx, stateMgr, diags
}

func (b *Remote) getRemoteWorkspaceName(localWorkspaceName string) string {
switch {
case localWorkspaceName == backend.DefaultStateName:
// The default workspace name is a special case, for when the backend
// is configured to with to an exact remote workspace rather than with
// a remote workspace _prefix_.
return b.workspace
case b.prefix != "" && !strings.HasPrefix(localWorkspaceName, b.prefix):
return b.prefix + localWorkspaceName
default:
return localWorkspaceName
}
}

func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) {
remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName)

log.Printf("[TRACE] backend/remote: looking up workspace id for %s/%s", b.organization, remoteWorkspaceName)
remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName)
if err != nil {
return "", err
}

return remoteWorkspace.ID, nil
}

func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues {
ret := make(terraform.InputValues, len(decls))

Expand Down
13 changes: 7 additions & 6 deletions backend/remote/backend_context_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package remote

import (
"context"
"testing"

tfe "github.com/hashicorp/go-tfe"
Expand Down Expand Up @@ -176,6 +177,11 @@ func TestRemoteContextWithVars(t *testing.T) {
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
defer configCleanup()

workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}

op := &backend.Operation{
ConfigDir: configDir,
ConfigLoader: configLoader,
Expand All @@ -187,12 +193,7 @@ func TestRemoteContextWithVars(t *testing.T) {
key := "key"
v.Key = &key
}
if v.Workspace == nil {
v.Workspace = &tfe.Workspace{
Name: b.workspace,
}
}
b.client.Variables.Create(nil, *v)
b.client.Variables.Create(nil, workspaceID, *v)

_, _, diags := b.Context(op)

Expand Down
32 changes: 25 additions & 7 deletions backend/remote/backend_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,11 @@ type mockRuns struct {
client *mockClient
runs map[string]*tfe.Run
workspaces map[string][]*tfe.Run

// If modifyNewRun is non-nil, the create method will call it just before
// saving a new run in the runs map, so that a calling test can mimic
// side-effects that a real server might apply in certain situations.
modifyNewRun func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run)
}

func newMockRuns(client *mockClient) *mockRuns {
Expand Down Expand Up @@ -757,6 +762,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
Permissions: &tfe.RunPermissions{},
Plan: p,
Status: tfe.RunPending,
TargetAddrs: options.TargetAddrs,
}

if options.Message != nil {
r.Message = *options.Message
}

if pc != nil {
Expand All @@ -775,6 +785,12 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
w.CurrentRun = r
}

if m.modifyNewRun != nil {
// caller-provided callback may modify the run in-place to mimic
// side-effects that a real server might take in some situations.
m.modifyNewRun(m.client, options, r)
}

m.runs[r.ID] = r
m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)

Expand Down Expand Up @@ -952,19 +968,21 @@ type mockVariables struct {
workspaces map[string]*tfe.VariableList
}

var _ tfe.Variables = (*mockVariables)(nil)

func newMockVariables(client *mockClient) *mockVariables {
return &mockVariables{
client: client,
workspaces: make(map[string]*tfe.VariableList),
}
}

func (m *mockVariables) List(ctx context.Context, options tfe.VariableListOptions) (*tfe.VariableList, error) {
vl := m.workspaces[*options.Workspace]
func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) {
vl := m.workspaces[workspaceID]
return vl, nil
}

func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) {
v := &tfe.Variable{
ID: generateID("var-"),
Key: *options.Key,
Expand All @@ -980,7 +998,7 @@ func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOp
v.Sensitive = *options.Sensitive
}

workspace := options.Workspace.Name
workspace := workspaceID

if m.workspaces[workspace] == nil {
m.workspaces[workspace] = &tfe.VariableList{}
Expand All @@ -992,15 +1010,15 @@ func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOp
return v, nil
}

func (m *mockVariables) Read(ctx context.Context, variableID string) (*tfe.Variable, error) {
func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) {
panic("not implemented")
}

func (m *mockVariables) Update(ctx context.Context, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) {
panic("not implemented")
}

func (m *mockVariables) Delete(ctx context.Context, variableID string) error {
func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error {
panic("not implemented")
}

Expand Down
Loading