Skip to content

Commit

Permalink
command+core: Minimal initial implementation of -replace=... option
Browse files Browse the repository at this point in the history
This only includes the internal mechanisms to make it work, and not any
of the necessary UI changes to "terraform plan" and "terraform apply" to
make their output make sense in this mode.

The force-replace options are ultimately handled inside the
NodeAbstractResourceInstance.plan method, at the same place we handle the
similar situation of the provider indicating that replacement is needed,
and so the rest of the changes here are just to propagate the settings
through all of the layers in order to reach that point.

Since the plan renderer has no awareness of this situation right now, it
renders force-replaced instances in the same way as once where the
provider indicates replacement is needed, annotated with "must be
replaced". In a later commit we'll add a special case to show these ones
as "will be replaced as requested", but otherwise this is a complete
implementation of the necessary functionality.
  • Loading branch information
apparentlymart committed Apr 22, 2021
1 parent 0bd3417 commit af00be6
Show file tree
Hide file tree
Showing 17 changed files with 548 additions and 209 deletions.
11 changes: 6 additions & 5 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,12 @@ type Operation struct {

// The options below are more self-explanatory and affect the runtime
// behavior of the operation.
PlanMode plans.Mode
AutoApprove bool
Parallelism int
Targets []addrs.Targetable
Variables map[string]UnparsedVariableValue
PlanMode plans.Mode
AutoApprove bool
Parallelism int
Targets []addrs.Targetable
ForceReplace []addrs.AbsResourceInstance
Variables map[string]UnparsedVariableValue

// Some operations use root module variables only opportunistically or
// don't need them at all. If this flag is set, the backend must treat
Expand Down
2 changes: 2 additions & 0 deletions backend/local/backend_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.
// Copy set options from the operation
opts.PlanMode = op.PlanMode
opts.Targets = op.Targets
opts.ForceReplace = op.ForceReplace
opts.UIInput = op.UIIn
opts.Hooks = op.Hooks

Expand Down Expand Up @@ -264,6 +265,7 @@ func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextO
opts.Variables = variables
opts.Changes = plan.Changes
opts.Targets = plan.TargetAddrs
opts.ForceReplace = plan.ForceReplaceAddrs
opts.ProviderSHA256s = plan.ProviderSHA256s

tfCtx, ctxDiags := terraform.NewContext(&opts)
Expand Down
1 change: 1 addition & 0 deletions command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ func (c *ApplyCommand) OperationRequest(
opReq.PlanFile = planFile
opReq.PlanRefresh = args.Refresh
opReq.Targets = args.Targets
opReq.ForceReplace = args.ForceReplace
opReq.Type = backend.OperationTypeApply
opReq.View = view.Operation()

Expand Down
59 changes: 59 additions & 0 deletions command/arguments/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,65 @@ func TestParseApply_targets(t *testing.T) {
}
}

func TestParseApply_replace(t *testing.T) {
foobarbaz, _ := addrs.ParseAbsResourceInstanceStr("foo_bar.baz")
foobarbeep, _ := addrs.ParseAbsResourceInstanceStr("foo_bar.beep")
testCases := map[string]struct {
args []string
want []addrs.AbsResourceInstance
wantErr string
}{
"no addresses by default": {
args: nil,
want: nil,
},
"one address": {
args: []string{"-replace=foo_bar.baz"},
want: []addrs.AbsResourceInstance{foobarbaz},
},
"two addresses": {
args: []string{"-replace=foo_bar.baz", "-replace", "foo_bar.beep"},
want: []addrs.AbsResourceInstance{foobarbaz, foobarbeep},
},
"non-resource-instance address": {
args: []string{"-replace=module.boop"},
want: nil,
wantErr: "A resource instance address is required here.",
},
"data resource address": {
args: []string{"-replace=data.foo.bar"},
want: nil,
wantErr: "Only managed resources can be used",
},
"invalid traversal": {
args: []string{"-replace=foo."},
want: nil,
wantErr: "Dot must be followed by attribute name",
},
"invalid address": {
args: []string{"-replace=data[0].foo"},
want: nil,
wantErr: "A data source name is required",
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args)
if len(diags) > 0 {
if tc.wantErr == "" {
t.Fatalf("unexpected diags: %v", diags)
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.ForceReplace, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
}
})
}
}

func TestParseApply_vars(t *testing.T) {
testCases := map[string]struct {
args []string
Expand Down
53 changes: 50 additions & 3 deletions command/arguments/extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,25 @@ type Operation struct {
// their dependencies.
Targets []addrs.Targetable

// ForceReplace addresses cause Terraform to force a particular set of
// resource instances to generate "replace" actions in any plan where they
// would normally have generated "no-op" or "update" actions.
//
// This is currently limited to specific instances because typical uses
// of replace are associated with only specific remote objects that the
// user has somehow learned to be malfunctioning, in which case it
// would be unusual and potentially dangerous to replace everything under
// a module all at once. We could potentially loosen this later if we
// learn a use-case for broader matching.
ForceReplace []addrs.AbsResourceInstance

// These private fields are used only temporarily during decoding. Use
// method Parse to populate the exported fields from these, validating
// the raw values in the process.
targetsRaw []string
destroyRaw bool
refreshOnlyRaw bool
targetsRaw []string
forceReplaceRaw []string
destroyRaw bool
refreshOnlyRaw bool
}

// Parse must be called on Operation after initial flag parse. This processes
Expand Down Expand Up @@ -103,6 +116,39 @@ func (o *Operation) Parse() tfdiags.Diagnostics {
o.Targets = append(o.Targets, target.Subject)
}

for _, raw := range o.forceReplaceRaw {
traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(raw), "", hcl.Pos{Line: 1, Column: 1})
if syntaxDiags.HasErrors() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Invalid force-replace address %q", raw),
syntaxDiags[0].Detail,
))
continue
}

addr, addrDiags := addrs.ParseAbsResourceInstance(traversal)
if addrDiags.HasErrors() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Invalid force-replace address %q", raw),
addrDiags[0].Description().Detail,
))
continue
}

if addr.Resource.Resource.Mode != addrs.ManagedResourceMode {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Invalid force-replace address %q", raw),
"Only managed resources can be used with the -replace=... option.",
))
continue
}

o.ForceReplace = append(o.ForceReplace, addr)
}

// If you add a new possible value for o.PlanMode here, consider also
// adding a specialized error message for it in ParseApplyDestroy.
switch {
Expand Down Expand Up @@ -178,6 +224,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only")
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace")
}

// Gather all -var and -var-file arguments into one heterogenous structure
Expand Down
1 change: 1 addition & 0 deletions command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func (c *PlanCommand) OperationRequest(
opReq.PlanRefresh = args.Refresh
opReq.PlanOutPath = planOutPath
opReq.Targets = args.Targets
opReq.ForceReplace = args.ForceReplace
opReq.Type = backend.OperationTypePlan
opReq.View = view.Operation()

Expand Down
Loading

0 comments on commit af00be6

Please sign in to comment.