command: New -replace=... planning option
This allows a similar effect to pre-tainting an object but does the action within the context of a normal plan and apply, avoiding the need for an intermediate state where the old object still exists but is marked as tainted. The core functionality for this was already present, so this commit is just the UI-level changes to make that option available for use and to explain how it contributed to the resulting plan in Terraform's output.
This commit is contained in:
parent
7f39f19ec7
commit
1d3e34e35e
|
@ -266,6 +266,7 @@ func (c *ApplyCommand) OperationRequest(
|
||||||
opReq.PlanFile = planFile
|
opReq.PlanFile = planFile
|
||||||
opReq.PlanRefresh = args.Refresh
|
opReq.PlanRefresh = args.Refresh
|
||||||
opReq.Targets = args.Targets
|
opReq.Targets = args.Targets
|
||||||
|
opReq.ForceReplace = args.ForceReplace
|
||||||
opReq.Type = backend.OperationTypeApply
|
opReq.Type = backend.OperationTypeApply
|
||||||
opReq.View = view.Operation()
|
opReq.View = view.Operation()
|
||||||
|
|
||||||
|
|
|
@ -1862,6 +1862,93 @@ func TestApply_targetFlagsDiags(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApply_replace(t *testing.T) {
|
||||||
|
td := tempDir(t)
|
||||||
|
testCopyDir(t, testFixturePath("apply-replace"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(
|
||||||
|
addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "a",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
&states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"id":"hello"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
},
|
||||||
|
addrs.AbsProviderConfig{
|
||||||
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
|
Module: addrs.RootModule,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||||
|
ResourceTypes: map[string]providers.Schema{
|
||||||
|
"test_instance": {
|
||||||
|
Block: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Computed: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||||
|
return providers.PlanResourceChangeResponse{
|
||||||
|
PlannedState: req.ProposedNewState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createCount := 0
|
||||||
|
deleteCount := 0
|
||||||
|
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
||||||
|
if req.PriorState.IsNull() {
|
||||||
|
createCount++
|
||||||
|
}
|
||||||
|
if req.PlannedState.IsNull() {
|
||||||
|
deleteCount++
|
||||||
|
}
|
||||||
|
return providers.ApplyResourceChangeResponse{
|
||||||
|
NewState: req.PlannedState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view, done := testView(t)
|
||||||
|
c := &ApplyCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
View: view,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-auto-approve",
|
||||||
|
"-state", statePath,
|
||||||
|
"-replace", "test_instance.a",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
output := done(t)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := output.Stdout(), "1 added, 0 changed, 1 destroyed"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong change summary\ngot output:\n%s\n\nwant substring: %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := createCount, 1; got != want {
|
||||||
|
t.Errorf("wrong create count %d; want %d", got, want)
|
||||||
|
}
|
||||||
|
if got, want := deleteCount, 1; got != want {
|
||||||
|
t.Errorf("wrong create count %d; want %d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApply_pluginPath(t *testing.T) {
|
func TestApply_pluginPath(t *testing.T) {
|
||||||
// Create a temporary working directory that is empty
|
// Create a temporary working directory that is empty
|
||||||
td := tempDir(t)
|
td := tempDir(t)
|
||||||
|
|
|
@ -211,6 +211,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) {
|
func TestParseApply_vars(t *testing.T) {
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
args []string
|
args []string
|
||||||
|
|
|
@ -63,10 +63,23 @@ type Operation struct {
|
||||||
// their dependencies.
|
// their dependencies.
|
||||||
Targets []addrs.Targetable
|
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
|
// These private fields are used only temporarily during decoding. Use
|
||||||
// method Parse to populate the exported fields from these, validating
|
// method Parse to populate the exported fields from these, validating
|
||||||
// the raw values in the process.
|
// the raw values in the process.
|
||||||
targetsRaw []string
|
targetsRaw []string
|
||||||
|
forceReplaceRaw []string
|
||||||
destroyRaw bool
|
destroyRaw bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,6 +115,39 @@ func (o *Operation) Parse() tfdiags.Diagnostics {
|
||||||
o.Targets = append(o.Targets, target.Subject)
|
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
|
// If you add a new possible value for o.PlanMode here, consider also
|
||||||
// adding a specialized error message for it in ParseApplyDestroy.
|
// adding a specialized error message for it in ParseApplyDestroy.
|
||||||
switch {
|
switch {
|
||||||
|
@ -161,6 +207,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
|
||||||
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
|
f.BoolVar(&operation.Refresh, "refresh", true, "refresh")
|
||||||
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
|
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
|
||||||
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
|
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
|
// Gather all -var and -var-file arguments into one heterogenous structure
|
||||||
|
|
|
@ -60,6 +60,8 @@ func ResourceChange(
|
||||||
switch change.ActionReason {
|
switch change.ActionReason {
|
||||||
case plans.ResourceInstanceReplaceBecauseTainted:
|
case plans.ResourceInstanceReplaceBecauseTainted:
|
||||||
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr)))
|
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr)))
|
||||||
|
case plans.ResourceInstanceReplaceByRequest:
|
||||||
|
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr)))
|
||||||
default:
|
default:
|
||||||
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr)))
|
buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,7 @@ func (c *PlanCommand) OperationRequest(
|
||||||
opReq.PlanRefresh = args.Refresh
|
opReq.PlanRefresh = args.Refresh
|
||||||
opReq.PlanOutPath = planOutPath
|
opReq.PlanOutPath = planOutPath
|
||||||
opReq.Targets = args.Targets
|
opReq.Targets = args.Targets
|
||||||
|
opReq.ForceReplace = args.ForceReplace
|
||||||
opReq.Type = backend.OperationTypePlan
|
opReq.Type = backend.OperationTypePlan
|
||||||
opReq.View = view.Operation()
|
opReq.View = view.Operation()
|
||||||
|
|
||||||
|
@ -204,11 +205,21 @@ Plan Customization Options:
|
||||||
-destroy If set, a plan will be generated to destroy all resources
|
-destroy If set, a plan will be generated to destroy all resources
|
||||||
managed by the given configuration and state.
|
managed by the given configuration and state.
|
||||||
|
|
||||||
-refresh=true Update state prior to checking for differences.
|
-refresh=false Skip checking for changes to remote objects while
|
||||||
|
creating the plan. This can potentially make planning
|
||||||
|
faster, but at the expense of possibly planning against
|
||||||
|
a stale record of the remote system state.
|
||||||
|
|
||||||
-target=resource Resource to target. Operation will be limited to this
|
-replace=resource Force replacement of a particular resource instance using
|
||||||
resource and its dependencies. This flag can be used
|
its resource address. If the plan would've normally
|
||||||
multiple times.
|
produced an update or no-op action for this instance,
|
||||||
|
Terraform will plan to replace it instead.
|
||||||
|
|
||||||
|
-target=resource Limit the planning operation to only the given module,
|
||||||
|
resource, or resource instance and all of its
|
||||||
|
dependencies. You can use this option multiple times to
|
||||||
|
include more than one object. This is for exceptional
|
||||||
|
use only.
|
||||||
|
|
||||||
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
-var 'foo=bar' Set a variable in the Terraform configuration. This
|
||||||
flag can be set multiple times.
|
flag can be set multiple times.
|
||||||
|
|
|
@ -1033,6 +1033,78 @@ func TestPlan_targetFlagsDiags(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlan_replace(t *testing.T) {
|
||||||
|
td := tempDir(t)
|
||||||
|
testCopyDir(t, testFixturePath("plan-replace"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
originalState := states.BuildState(func(s *states.SyncState) {
|
||||||
|
s.SetResourceInstanceCurrent(
|
||||||
|
addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "a",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
&states.ResourceInstanceObjectSrc{
|
||||||
|
AttrsJSON: []byte(`{"id":"hello"}`),
|
||||||
|
Status: states.ObjectReady,
|
||||||
|
},
|
||||||
|
addrs.AbsProviderConfig{
|
||||||
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
|
Module: addrs.RootModule,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
statePath := testStateFile(t, originalState)
|
||||||
|
|
||||||
|
p := testProvider()
|
||||||
|
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||||
|
ResourceTypes: map[string]providers.Schema{
|
||||||
|
"test_instance": {
|
||||||
|
Block: &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"id": {Type: cty.String, Computed: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||||
|
return providers.PlanResourceChangeResponse{
|
||||||
|
PlannedState: req.ProposedNewState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view, done := testView(t)
|
||||||
|
c := &PlanCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(p),
|
||||||
|
View: view,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-state", statePath,
|
||||||
|
"-no-color",
|
||||||
|
"-replace", "test_instance.a",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
output := done(t)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout := output.Stdout()
|
||||||
|
if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// planFixtureSchema returns a schema suitable for processing the
|
// planFixtureSchema returns a schema suitable for processing the
|
||||||
// configuration in testdata/plan . This schema should be
|
// configuration in testdata/plan . This schema should be
|
||||||
// assigned to a mock provider named "test".
|
// assigned to a mock provider named "test".
|
||||||
|
|
|
@ -198,6 +198,78 @@ func TestShow_planWithChanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShow_planWithForceReplaceChange(t *testing.T) {
|
||||||
|
// The main goal of this test is to see that the "replace by request"
|
||||||
|
// resource instance action reason can round-trip through a plan file and
|
||||||
|
// be reflected correctly in the "terraform show" output, the same way
|
||||||
|
// as it would appear in "terraform plan" output.
|
||||||
|
|
||||||
|
_, snap := testModuleWithSnapshot(t, "show")
|
||||||
|
plannedVal := cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.UnknownVal(cty.String),
|
||||||
|
"ami": cty.StringVal("bar"),
|
||||||
|
})
|
||||||
|
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plan := testPlan(t)
|
||||||
|
plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
|
||||||
|
Addr: addrs.Resource{
|
||||||
|
Mode: addrs.ManagedResourceMode,
|
||||||
|
Type: "test_instance",
|
||||||
|
Name: "foo",
|
||||||
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||||
|
ProviderAddr: addrs.AbsProviderConfig{
|
||||||
|
Provider: addrs.NewDefaultProvider("test"),
|
||||||
|
Module: addrs.RootModule,
|
||||||
|
},
|
||||||
|
ChangeSrc: plans.ChangeSrc{
|
||||||
|
Action: plans.CreateThenDelete,
|
||||||
|
Before: priorValRaw,
|
||||||
|
After: plannedValRaw,
|
||||||
|
},
|
||||||
|
ActionReason: plans.ResourceInstanceReplaceByRequest,
|
||||||
|
})
|
||||||
|
planFilePath := testPlanFile(
|
||||||
|
t,
|
||||||
|
snap,
|
||||||
|
states.NewState(),
|
||||||
|
plan,
|
||||||
|
)
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
view, done := testView(t)
|
||||||
|
c := &ShowCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
testingOverrides: metaOverridesForProvider(showFixtureProvider()),
|
||||||
|
Ui: ui,
|
||||||
|
View: view,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
planFilePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := done(t).Stdout()
|
||||||
|
if want := `test_instance.foo will be replaced, as requested`; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
|
||||||
|
}
|
||||||
|
if want := `Plan: 1 to add, 0 to change, 1 to destroy.`; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestShow_plan_json(t *testing.T) {
|
func TestShow_plan_json(t *testing.T) {
|
||||||
planPath := showFixturePlanFile(t, plans.Create)
|
planPath := showFixturePlanFile(t, plans.Create)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
resource "test_instance" "a" {
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
resource "test_instance" "a" {
|
||||||
|
}
|
|
@ -135,6 +135,23 @@ the previous section, are also available with the same meanings on
|
||||||
it would effectively disable the entirety of the planning operation in that
|
it would effectively disable the entirety of the planning operation in that
|
||||||
case.
|
case.
|
||||||
|
|
||||||
|
* `-replace=ADDRESS` - Instructs Terraform to plan to replace the single
|
||||||
|
resource instance with the given address. If the given instance would
|
||||||
|
normally have caused only an "update" action, or no action at all, then
|
||||||
|
Terraform will choose a "replace" action instead.
|
||||||
|
|
||||||
|
You can use this option if you have learned that a particular remote object
|
||||||
|
has become degraded in some way. If you are using immutable infrastructure
|
||||||
|
patterns then you may wish to respond to that by replacing the
|
||||||
|
malfunctioning object with a new object that has the same configuration.
|
||||||
|
|
||||||
|
This option is allowed only in the normal planning mode, so this option
|
||||||
|
is incompatible with the `-destroy` option.
|
||||||
|
|
||||||
|
The `-replace=...` option is available only from Terraform v1.0 onwards.
|
||||||
|
For earlier versions, you can achieve a similar effect (with some caveats)
|
||||||
|
using [`terraform taint`](./taint.html).
|
||||||
|
|
||||||
* `-target=ADDRESS` - Instructs Terraform to focus its planning efforts only
|
* `-target=ADDRESS` - Instructs Terraform to focus its planning efforts only
|
||||||
on resource instances which match the given address and on any objects that
|
on resource instances which match the given address and on any objects that
|
||||||
those instances depend on.
|
those instances depend on.
|
||||||
|
|
|
@ -14,6 +14,30 @@ become degraded or damaged. Terraform represents this by marking the
|
||||||
object as "tainted" in the Terraform state, in which case Terraform will
|
object as "tainted" in the Terraform state, in which case Terraform will
|
||||||
propose to replace it in the next plan you create.
|
propose to replace it in the next plan you create.
|
||||||
|
|
||||||
|
~> *Warning:* This command is deprecated, because there are better alternatives
|
||||||
|
available in Terraform v1.0 and later. See below for more details.
|
||||||
|
|
||||||
|
If your intent is to force replacement of a particular object even though
|
||||||
|
there are no configuration changes that would require it, we recommend instead
|
||||||
|
to use the `-replace` option with [`terraform apply`](./apply.html).
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
terraform apply -replace="aws_instance.example[0]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Creating a plan with the "replace" option is superior to using `terraform taint`
|
||||||
|
because it will allow you to see the full effect of that change before you take
|
||||||
|
any externally-visible action. When you use `terraform taint` to get a similar
|
||||||
|
effect, you risk someone else on your team creating a new plan against your
|
||||||
|
tainted object before you've had a chance to review the consequences of that
|
||||||
|
change yourself.
|
||||||
|
|
||||||
|
The `-replace=...` option to `terraform apply` is only available from
|
||||||
|
Terraform v1.0 onwards, so if you are using an earlier version you will need to
|
||||||
|
use `terraform taint` to force object replacement, while considering the
|
||||||
|
caveats described above.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Usage: `terraform taint [options] address`
|
Usage: `terraform taint [options] address`
|
||||||
|
|
|
@ -28,6 +28,14 @@ you can use `terraform untaint` to remove the taint marker from that object.
|
||||||
This command _will not_ modify any real remote objects, but will modify the
|
This command _will not_ modify any real remote objects, but will modify the
|
||||||
state in order to remove the tainted status.
|
state in order to remove the tainted status.
|
||||||
|
|
||||||
|
If you remove the taint marker from an object but then later discover that it
|
||||||
|
was degraded after all, you can create and apply a plan to replace it without
|
||||||
|
first re-tainting the object, by using a command like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
terraform apply -replace="aws_instance.example[0]"
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Usage: `terraform untaint [options] address`
|
Usage: `terraform untaint [options] address`
|
||||||
|
|
Loading…
Reference in New Issue