Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: local variables should not be overridden by remote variables during terraform import #29972

Merged
merged 5 commits into from Mar 15, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 23 additions & 16 deletions internal/backend/remote/backend.go
Expand Up @@ -673,19 +673,14 @@ func (b *Remote) StateMgr(name string) (statemgr.Full, error) {
return &remote.State{Client: client}, nil
}

// Operation implements backend.Enhanced.
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
// Get the remote workspace name.
name := op.Workspace
switch {
case op.Workspace == backend.DefaultStateName:
name = b.workspace
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
name = b.prefix + op.Workspace
}
func isLocalExecutionMode(execMode string) bool {
return execMode == "local"
}

func (b *Remote) fetchWorkspace(ctx context.Context, organization string, name string) (*tfe.Workspace, error) {
remoteWorkspaceName := b.getRemoteWorkspaceName(name)
// Retrieve the workspace for this operation.
w, err := b.client.Workspaces.Read(ctx, b.organization, name)
w, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName)
if err != nil {
switch err {
case context.Canceled:
Expand All @@ -695,17 +690,29 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
"workspace %s not found\n\n"+
"The configured \"remote\" backend returns '404 Not Found' errors for resources\n"+
"that do not exist, as well as for resources that a user doesn't have access\n"+
"to. If the resource does exist, please check the rights for the used token.",
"to. If the resource does exist, please check the rights for the used token",
name,
)
default:
return nil, fmt.Errorf(
"The configured \"remote\" backend encountered an unexpected error:\n\n%s",
err := fmt.Errorf(
"the configured \"remote\" backend encountered an unexpected error:\n\n%s",
Copy link
Member

Choose a reason for hiding this comment

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

I think the one remaining failing test is caused by this change in casing of "The" to "the". Was that necessary for some other reason?

Copy link
Contributor

Choose a reason for hiding this comment

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

oops, I meant to come back to this change earlier but I got caught up on some other task. I'll make this change now!

err,
)
return nil, err
}
}

return w, nil
}

// Operation implements backend.Enhanced.
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
w, err := b.fetchWorkspace(ctx, b.organization, op.Workspace)

if err != nil {
return nil, err
}

// Terraform remote version conflicts are not a concern for operations. We
// are in one of three states:
//
Expand All @@ -718,7 +725,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
b.IgnoreVersionConflict()

// Check if we need to use the local backend to run the operation.
if b.forceLocal || !w.Operations {
if b.forceLocal || isLocalExecutionMode(w.ExecutionMode) {
// Record that we're forced to run operations locally to allow the
// command package UI to operate correctly
b.forceLocal = true
Expand Down Expand Up @@ -902,7 +909,7 @@ func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.D

// If the workspace has remote operations disabled, the remote Terraform
// version is effectively meaningless, so we'll skip version verification.
if !workspace.Operations {
if isLocalExecutionMode(workspace.ExecutionMode) {
return nil
}

Expand Down
16 changes: 8 additions & 8 deletions internal/backend/remote/backend_apply_test.go
Expand Up @@ -1526,36 +1526,36 @@ func TestRemote_applyVersionCheck(t *testing.T) {
localVersion string
remoteVersion string
forceLocal bool
hasOperations bool
executionMode string
wantErr string
}{
"versions can be different for remote apply": {
localVersion: "0.14.0",
remoteVersion: "0.13.5",
hasOperations: true,
executionMode: "remote",
},
"versions can be different for local apply": {
localVersion: "0.14.0",
remoteVersion: "0.13.5",
hasOperations: false,
executionMode: "local",
},
"force local with remote operations and different versions is acceptable": {
localVersion: "0.14.0",
remoteVersion: "0.14.0-acme-provider-bundle",
forceLocal: true,
hasOperations: true,
executionMode: "remote",
},
"no error if versions are identical": {
localVersion: "0.14.0",
remoteVersion: "0.14.0",
forceLocal: true,
hasOperations: true,
executionMode: "remote",
},
"no error if force local but workspace has remote operations disabled": {
localVersion: "0.14.0",
remoteVersion: "0.13.5",
forceLocal: true,
hasOperations: false,
executionMode: "local",
},
}

Expand Down Expand Up @@ -1591,7 +1591,7 @@ func TestRemote_applyVersionCheck(t *testing.T) {
b.organization,
b.workspace,
tfe.WorkspaceUpdateOptions{
Operations: tfe.Bool(tc.hasOperations),
ExecutionMode: tfe.String(tc.executionMode),
TerraformVersion: tfe.String(tc.remoteVersion),
},
)
Expand Down Expand Up @@ -1644,7 +1644,7 @@ func TestRemote_applyVersionCheck(t *testing.T) {
hasRemote := strings.Contains(output, "Running apply in the remote backend")
hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
hasResources := run.State.HasManagedResourceInstanceObjects()
if !tc.forceLocal && tc.hasOperations {
if !tc.forceLocal && !isLocalExecutionMode(tc.executionMode) {
if !hasRemote {
t.Errorf("missing remote backend header in output: %s", output)
}
Expand Down
57 changes: 34 additions & 23 deletions internal/backend/remote/backend_context.go
Expand Up @@ -82,37 +82,48 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu
}
ret.Config = config

// 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(fmt.Errorf("error finding remote workspace: %w", 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(fmt.Errorf("error loading variables: %w", err))
return nil, nil, diags
}

if op.AllowUnsetVariables {
// If we're not going to use the variables in an operation we'll be
// more lax about them, stubbing out any unset ones as unknown.
// This gives us enough information to produce a consistent context,
// but not enough information to run a real operation (plan, apply, etc)
ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, config.Module.Variables)
} else {
if tfeVariables != nil {
if op.Variables == nil {
op.Variables = make(map[string]backend.UnparsedVariableValue)
// 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(fmt.Errorf("error finding remote workspace: %w", err))
return nil, nil, diags
}

w, err := b.fetchWorkspace(context.Background(), b.organization, op.Workspace)
if err != nil {
diags = diags.Append(fmt.Errorf("error loading workspace: %w", err))
return nil, nil, diags
}

if isLocalExecutionMode(w.ExecutionMode) {
log.Printf("[TRACE] skipping retrieving variables from workspace %s/%s (%s), workspace is in Local Execution mode", remoteWorkspaceName, b.organization, remoteWorkspaceID)
} else {
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(fmt.Errorf("error loading variables: %w", err))
return nil, nil, diags
}
for _, v := range tfeVariables.Items {
if v.Category == tfe.CategoryTerraform {
op.Variables[v.Key] = &remoteStoredVariableValue{
definition: v,
if tfeVariables != nil {
if op.Variables == nil {
op.Variables = make(map[string]backend.UnparsedVariableValue)
}
for _, v := range tfeVariables.Items {
if v.Category == tfe.CategoryTerraform {
if _, ok := op.Variables[v.Key]; !ok {
op.Variables[v.Key] = &remoteStoredVariableValue{
definition: v,
}
}
}
}
}
Expand Down