Add targeted replaces to update
. (#3418)
Allow the user to specify a set of resources to replace via the `--replace` flag on the CLI. This can be combined with `--target` to replace a specific set of resources without changing any other resources. `--target-replace` is shorthand for `--replace urn --target urn`. Fixes #2643.
This commit is contained in:
parent
c5bd24aa30
commit
23a84df254
|
@ -16,6 +16,12 @@ CHANGELOG
|
|||
- Omit unknowns in resources in stack outputs during preview.
|
||||
[#3427](https://github.com/pulumi/pulumi/pull/3427)
|
||||
|
||||
- `pulumi update` can now be instructed that a set of resources should be replaced by adding a
|
||||
`--replace urn` argument. Multiple resources can be specified using `--replace urn1 --replace urn2`. In order to
|
||||
replace exactly one resource and leave other resources unchanged, invoke `pulumi update --replace urn --target urn`,
|
||||
or `pulumi update --target-replace urn` for short.
|
||||
[#3418](https://github.com/pulumi/pulumi/pull/3418)
|
||||
|
||||
## 1.4.0 (2019-10-24)
|
||||
|
||||
- `FileAsset` in the Python SDK now accepts anything implementing `os.PathLike` in addition to `str`.
|
||||
|
|
36
cmd/up.go
36
cmd/up.go
|
@ -66,7 +66,9 @@ func newUpCmd() *cobra.Command {
|
|||
var suppressOutputs bool
|
||||
var yes bool
|
||||
var secretsProvider string
|
||||
var targets *[]string
|
||||
var targets []string
|
||||
var replaces []string
|
||||
var targetReplaces []string
|
||||
|
||||
// up implementation used when the source of the Pulumi program is in the current working directory.
|
||||
upWorkingDirectory := func(opts backend.UpdateOptions) result.Result {
|
||||
|
@ -100,9 +102,19 @@ func newUpCmd() *cobra.Command {
|
|||
return result.FromError(errors.Wrap(err, "getting stack configuration"))
|
||||
}
|
||||
|
||||
targetUrns := []resource.URN{}
|
||||
for _, t := range *targets {
|
||||
targetUrns = append(targetUrns, resource.URN(t))
|
||||
targetURNs := []resource.URN{}
|
||||
for _, t := range targets {
|
||||
targetURNs = append(targetURNs, resource.URN(t))
|
||||
}
|
||||
|
||||
replaceURNs := []resource.URN{}
|
||||
for _, r := range replaces {
|
||||
replaceURNs = append(replaceURNs, resource.URN(r))
|
||||
}
|
||||
|
||||
for _, tr := range targetReplaces {
|
||||
targetURNs = append(targetURNs, resource.URN(tr))
|
||||
replaceURNs = append(replaceURNs, resource.URN(tr))
|
||||
}
|
||||
|
||||
opts.Engine = engine.UpdateOptions{
|
||||
|
@ -110,8 +122,9 @@ func newUpCmd() *cobra.Command {
|
|||
Parallel: parallel,
|
||||
Debug: debug,
|
||||
Refresh: refresh,
|
||||
ReplaceTargets: replaceURNs,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
UpdateTargets: targetUrns,
|
||||
UpdateTargets: targetURNs,
|
||||
}
|
||||
|
||||
changes, res := s.Update(commandContext(), backend.UpdateOperation{
|
||||
|
@ -375,10 +388,17 @@ func newUpCmd() *cobra.Command {
|
|||
&message, "message", "m", "",
|
||||
"Optional message to associate with the update operation")
|
||||
|
||||
targets = cmd.PersistentFlags().StringArrayP(
|
||||
"target", "t", []string{},
|
||||
cmd.PersistentFlags().StringArrayVarP(
|
||||
&targets, "target", "t", []string{},
|
||||
"Specify a single resource URN to update. Other resources will not be updated."+
|
||||
" Multiple resources can be specified using: --target urn1 --target urn2")
|
||||
" Multiple resources can be specified using --target urn1 --target urn2")
|
||||
cmd.PersistentFlags().StringArrayVar(
|
||||
&replaces, "replace", []string{},
|
||||
"Specify resources to replace. Multiple resources can be specified using --replace run1 --replace urn2")
|
||||
cmd.PersistentFlags().StringArrayVar(
|
||||
&targetReplaces, "target-replace", []string{},
|
||||
"Specify a single resource URN to replace. Other resources will not be updated."+
|
||||
" Shorthand for --target urn --replace urn.")
|
||||
|
||||
// Flags for engine.UpdateOptions.
|
||||
if hasDebugCommands() {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// nolint: goconst
|
||||
package engine
|
||||
|
||||
import (
|
||||
|
@ -4712,3 +4713,81 @@ func TestDependencyChangeDBR(t *testing.T) {
|
|||
}
|
||||
p.Run(t, snap)
|
||||
}
|
||||
|
||||
func TestReplaceSpecificTargets(t *testing.T) {
|
||||
// A
|
||||
// _________|_________
|
||||
// B C D
|
||||
// ___|___ ___|___
|
||||
// E F G H I J
|
||||
// |__|
|
||||
// K L
|
||||
|
||||
p := &TestPlan{}
|
||||
|
||||
urns, old, program := generateComplexTestDependencyGraph(t, p)
|
||||
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{
|
||||
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
|
||||
ignoreChanges []string) (plugin.DiffResult, error) {
|
||||
|
||||
// No resources will change.
|
||||
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
|
||||
},
|
||||
|
||||
CreateF: func(urn resource.URN,
|
||||
news resource.PropertyMap, timeout float64) (resource.ID, resource.PropertyMap, resource.Status, error) {
|
||||
|
||||
return "created-id", news, resource.StatusOK, nil
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
p.Options.host = deploytest.NewPluginHost(nil, nil, program, loaders...)
|
||||
|
||||
getURN := func(name string) resource.URN {
|
||||
return pickURN(t, urns, complexTestDependencyGraphNames, name)
|
||||
}
|
||||
|
||||
p.Options.ReplaceTargets = []resource.URN{
|
||||
getURN("F"),
|
||||
getURN("B"),
|
||||
getURN("G"),
|
||||
}
|
||||
|
||||
p.Steps = []TestStep{{
|
||||
Op: Update,
|
||||
ExpectFailure: false,
|
||||
Validate: func(project workspace.Project, target deploy.Target, j *Journal,
|
||||
evts []Event, res result.Result) result.Result {
|
||||
|
||||
assert.Nil(t, res)
|
||||
assert.True(t, len(j.Entries) > 0)
|
||||
|
||||
replaced := make(map[resource.URN]bool)
|
||||
sames := make(map[resource.URN]bool)
|
||||
for _, entry := range j.Entries {
|
||||
if entry.Step.Op() == deploy.OpReplace {
|
||||
replaced[entry.Step.URN()] = true
|
||||
} else if entry.Step.Op() == deploy.OpSame {
|
||||
sames[entry.Step.URN()] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range p.Options.ReplaceTargets {
|
||||
assert.Contains(t, replaced, target)
|
||||
}
|
||||
|
||||
for _, target := range p.Options.ReplaceTargets {
|
||||
assert.NotContains(t, sames, target)
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}}
|
||||
|
||||
p.Run(t, old)
|
||||
}
|
||||
|
|
|
@ -182,6 +182,7 @@ func (planResult *planResult) Walk(cancelCtx *Context, events deploy.Events, pre
|
|||
Refresh: planResult.Options.Refresh,
|
||||
RefreshOnly: planResult.Options.isRefresh,
|
||||
RefreshTargets: planResult.Options.RefreshTargets,
|
||||
ReplaceTargets: planResult.Options.ReplaceTargets,
|
||||
DestroyTargets: planResult.Options.DestroyTargets,
|
||||
UpdateTargets: planResult.Options.UpdateTargets,
|
||||
TrustDependencies: planResult.Options.trustDependencies,
|
||||
|
|
|
@ -65,6 +65,9 @@ type UpdateOptions struct {
|
|||
// Specific resources to refresh during a refresh operation.
|
||||
RefreshTargets []resource.URN
|
||||
|
||||
// Specific resources to replace during an update operation.
|
||||
ReplaceTargets []resource.URN
|
||||
|
||||
// Specific resources to destroy during a destroy operation.
|
||||
DestroyTargets []resource.URN
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ type Options struct {
|
|||
Refresh bool // whether or not to refresh before executing the plan.
|
||||
RefreshOnly bool // whether or not to exit after refreshing.
|
||||
RefreshTargets []resource.URN // The specific resources to refresh during a refresh op.
|
||||
ReplaceTargets []resource.URN // Specific resources to replace.
|
||||
DestroyTargets []resource.URN // Specific resources to destroy.
|
||||
UpdateTargets []resource.URN // Specific resources to update.
|
||||
TrustDependencies bool // whether or not to trust the resource dependency graph.
|
||||
|
|
|
@ -60,15 +60,15 @@ func createTargetMap(targets []resource.URN) map[resource.URN]bool {
|
|||
// checkTargets validates that all the targets passed in refer to existing resources. Diagnostics
|
||||
// are generated for any target that cannot be found. The target must either have existed in the stack
|
||||
// prior to running the operation, or it must be the urn for a resource that was created.
|
||||
func (pe *planExecutor) checkTargets(targets []resource.URN) result.Result {
|
||||
func (pe *planExecutor) checkTargets(targets []resource.URN, op StepOp) result.Result {
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
olds := pe.plan.olds
|
||||
var news map[resource.URN]bool
|
||||
if pe.stepGen != nil && pe.stepGen.creates != nil {
|
||||
news = pe.stepGen.creates
|
||||
if pe.stepGen != nil {
|
||||
news = pe.stepGen.urns
|
||||
}
|
||||
|
||||
hasUnknownTarget := false
|
||||
|
@ -82,7 +82,7 @@ func (pe *planExecutor) checkTargets(targets []resource.URN) result.Result {
|
|||
if !hasOld && !hasNew {
|
||||
hasUnknownTarget = true
|
||||
|
||||
logging.V(7).Infof("Resource to delete (%v) could not be found in the stack.", target)
|
||||
logging.V(7).Infof("Resource to %v (%v) could not be found in the stack.", op, target)
|
||||
if strings.Contains(string(target), "$") {
|
||||
pe.plan.Diag().Errorf(diag.GetTargetCouldNotBeFoundError(), target)
|
||||
} else {
|
||||
|
@ -149,13 +149,17 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
// during `update` that we don't know about because it might be the urn for a resource they
|
||||
// want to create.
|
||||
updateTargetsOpt := createTargetMap(opts.UpdateTargets)
|
||||
replaceTargetsOpt := createTargetMap(opts.ReplaceTargets)
|
||||
destroyTargetsOpt := createTargetMap(opts.DestroyTargets)
|
||||
if res := pe.checkTargets(opts.DestroyTargets); res != nil {
|
||||
if res := pe.checkTargets(opts.ReplaceTargets, OpReplace); res != nil {
|
||||
return res
|
||||
}
|
||||
if res := pe.checkTargets(opts.DestroyTargets, OpDelete); res != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
if updateTargetsOpt != nil && destroyTargetsOpt != nil {
|
||||
contract.Failf("Should not be possible to have both .DestroyTargets and .UpdateTargets")
|
||||
if (updateTargetsOpt != nil || replaceTargetsOpt != nil) && destroyTargetsOpt != nil {
|
||||
contract.Failf("Should not be possible to have both .DestroyTargets and .UpdateTargets or .ReplaceTargets")
|
||||
}
|
||||
|
||||
// Begin iterating the source.
|
||||
|
@ -165,7 +169,7 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
}
|
||||
|
||||
// Set up a step generator for this plan.
|
||||
pe.stepGen = newStepGenerator(pe.plan, opts)
|
||||
pe.stepGen = newStepGenerator(pe.plan, opts, updateTargetsOpt, replaceTargetsOpt)
|
||||
|
||||
// Retire any pending deletes that are currently present in this plan.
|
||||
if res := pe.retirePendingDeletes(callerCtx, opts, preview); res != nil {
|
||||
|
@ -230,7 +234,7 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
return false, pe.performDeletes(ctx, updateTargetsOpt, destroyTargetsOpt)
|
||||
}
|
||||
|
||||
if res := pe.handleSingleEvent(updateTargetsOpt, event.Event); res != nil {
|
||||
if res := pe.handleSingleEvent(event.Event); res != nil {
|
||||
if resErr := res.Error(); resErr != nil {
|
||||
logging.V(4).Infof("planExecutor.Execute(...): error handling event: %v", resErr)
|
||||
pe.reportError(pe.plan.generateEventURN(event.Event), resErr)
|
||||
|
@ -255,7 +259,7 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
// valid. We have to do this *after* performing the steps as the target list may have referred
|
||||
// to a resource that was created in one of hte steps.
|
||||
if res == nil {
|
||||
res = pe.checkTargets(opts.UpdateTargets)
|
||||
res = pe.checkTargets(opts.UpdateTargets, OpUpdate)
|
||||
}
|
||||
|
||||
if res != nil && res.IsBail() {
|
||||
|
@ -366,7 +370,7 @@ func (pe *planExecutor) performDeletes(
|
|||
|
||||
// handleSingleEvent handles a single source event. For all incoming events, it produces a chain that needs
|
||||
// to be executed and schedules the chain for execution.
|
||||
func (pe *planExecutor) handleSingleEvent(updateTargetsOpt map[resource.URN]bool, event SourceEvent) result.Result {
|
||||
func (pe *planExecutor) handleSingleEvent(event SourceEvent) result.Result {
|
||||
contract.Require(event != nil, "event != nil")
|
||||
|
||||
var steps []Step
|
||||
|
@ -374,7 +378,7 @@ func (pe *planExecutor) handleSingleEvent(updateTargetsOpt map[resource.URN]bool
|
|||
switch e := event.(type) {
|
||||
case RegisterResourceEvent:
|
||||
logging.V(4).Infof("planExecutor.handleSingleEvent(...): received RegisterResourceEvent")
|
||||
steps, res = pe.stepGen.GenerateSteps(updateTargetsOpt, e)
|
||||
steps, res = pe.stepGen.GenerateSteps(e)
|
||||
case ReadResourceEvent:
|
||||
logging.V(4).Infof("planExecutor.handleSingleEvent(...): received ReadResourceEvent")
|
||||
steps, res = pe.stepGen.GenerateReadSteps(e)
|
||||
|
@ -444,7 +448,7 @@ func (pe *planExecutor) refresh(callerCtx context.Context, opts Options, preview
|
|||
|
||||
// Make sure if there were any targets specified, that they all refer to existing resources.
|
||||
targetMapOpt := createTargetMap(opts.RefreshTargets)
|
||||
if res := pe.checkTargets(opts.RefreshTargets); res != nil {
|
||||
if res := pe.checkTargets(opts.RefreshTargets, OpRefresh); res != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,9 @@ type stepGenerator struct {
|
|||
plan *Plan // the plan to which this step generator belongs
|
||||
opts Options // options for this step generator
|
||||
|
||||
updateTargetsOpt map[resource.URN]bool // the set of resources to update; resources not in this set will be same'd
|
||||
replaceTargetsOpt map[resource.URN]bool // the set of resoures to replace
|
||||
|
||||
// signals that one or more PolicyViolationEvents have been reported to the user, and the plan
|
||||
// should terminate in error. This primarily allows `preview` to aggregate many policy violation
|
||||
// events and report them all at once.
|
||||
|
@ -62,6 +65,14 @@ type stepGenerator struct {
|
|||
aliased map[resource.URN]resource.URN
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) isTargetedForUpdate(urn resource.URN) bool {
|
||||
return sg.updateTargetsOpt == nil || sg.updateTargetsOpt[urn]
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) isTargetedReplace(urn resource.URN) bool {
|
||||
return sg.replaceTargetsOpt != nil && sg.replaceTargetsOpt[urn]
|
||||
}
|
||||
|
||||
// GenerateReadSteps is responsible for producing one or more steps required to service
|
||||
// a ReadResourceEvent coming from the language host.
|
||||
func (sg *stepGenerator) GenerateReadSteps(event ReadResourceEvent) ([]Step, result.Result) {
|
||||
|
@ -125,8 +136,7 @@ func (sg *stepGenerator) GenerateReadSteps(event ReadResourceEvent) ([]Step, res
|
|||
//
|
||||
// If the given resource is a custom resource, the step generator will invoke Diff and Check on the
|
||||
// provider associated with that resource. If those fail, an error is returned.
|
||||
func (sg *stepGenerator) GenerateSteps(
|
||||
updateTargetsOpt map[resource.URN]bool, event RegisterResourceEvent) ([]Step, result.Result) {
|
||||
func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, result.Result) {
|
||||
|
||||
var invalid bool // will be set to true if this object fails validation.
|
||||
|
||||
|
@ -225,8 +235,9 @@ func (sg *stepGenerator) GenerateSteps(
|
|||
|
||||
// If we are re-creating this resource because it was deleted earlier, the old inputs are now
|
||||
// invalid (they got deleted) so don't consider them. Similarly, if the old resource was External,
|
||||
// don't consider those inputs since Pulumi does not own them.
|
||||
if recreating || wasExternal {
|
||||
// don't consider those inputs since Pulumi does not own them. Finally, if the resource has been
|
||||
// targeted for replacement, ignore its old state.
|
||||
if recreating || wasExternal || sg.isTargetedReplace(urn) {
|
||||
inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns)
|
||||
} else {
|
||||
inputs, failures, err = prov.Check(urn, oldInputs, inputs, allowUnknowns)
|
||||
|
@ -353,7 +364,7 @@ func (sg *stepGenerator) GenerateSteps(
|
|||
|
||||
// If the user requested only specific resources to update, and this resource was not in
|
||||
// that set, then do nothin but create a SameStep for it.
|
||||
if updateTargetsOpt != nil && !updateTargetsOpt[urn] {
|
||||
if !sg.isTargetedForUpdate(urn) {
|
||||
logging.V(7).Infof(
|
||||
"Planner decided not to update '%v' due to not being in target group (same) (inputs=%v)", urn, new.Inputs)
|
||||
} else {
|
||||
|
@ -379,7 +390,7 @@ func (sg *stepGenerator) GenerateSteps(
|
|||
return []Step{NewSameStep(sg.plan, event, old, new)}, nil
|
||||
}
|
||||
|
||||
if updateTargetsOpt != nil && !updateTargetsOpt[urn] {
|
||||
if !sg.isTargetedForUpdate(urn) {
|
||||
d := diag.GetResourceIsBeingCreatedButWasNotSpecifiedInTargetList(urn)
|
||||
|
||||
if sg.plan.preview {
|
||||
|
@ -447,7 +458,9 @@ func (sg *stepGenerator) generateStepsFromDiff(
|
|||
|
||||
// If we are going to perform a replacement, we need to recompute the default values. The above logic
|
||||
// had assumed that we were going to carry them over from the old resource, which is no longer true.
|
||||
if prov != nil {
|
||||
//
|
||||
// Note that if we're performing a targeted replace, we already have the correct inputs.
|
||||
if prov != nil && !sg.isTargetedReplace(urn) {
|
||||
var failures []plugin.CheckFailure
|
||||
inputs, failures, err = prov.Check(urn, nil, goal.Properties, allowUnknowns)
|
||||
if err != nil {
|
||||
|
@ -891,6 +904,11 @@ func (sg *stepGenerator) diff(urn resource.URN, old, new *resource.State, oldInp
|
|||
newInputs resource.PropertyMap, prov plugin.Provider, allowUnknowns bool,
|
||||
ignoreChanges []string) (plugin.DiffResult, error) {
|
||||
|
||||
// If this resource is marked for replacement, just return a "replace" diff that blames the id.
|
||||
if sg.isTargetedReplace(urn) {
|
||||
return plugin.DiffResult{Changes: plugin.DiffSome, ReplaceKeys: []resource.PropertyKey{"id"}}, nil
|
||||
}
|
||||
|
||||
// Before diffing the resource, diff the provider field. If the provider field changes, we may or may
|
||||
// not need to replace the resource.
|
||||
providerChanged, err := sg.providerChanged(urn, old, new)
|
||||
|
@ -1136,10 +1154,14 @@ func (sg *stepGenerator) calculateDependentReplacements(root *resource.State) ([
|
|||
}
|
||||
|
||||
// newStepGenerator creates a new step generator that operates on the given plan.
|
||||
func newStepGenerator(plan *Plan, opts Options) *stepGenerator {
|
||||
func newStepGenerator(
|
||||
plan *Plan, opts Options, updateTargetsOpt, replaceTargetsOpt map[resource.URN]bool) *stepGenerator {
|
||||
|
||||
return &stepGenerator{
|
||||
plan: plan,
|
||||
opts: opts,
|
||||
updateTargetsOpt: updateTargetsOpt,
|
||||
replaceTargetsOpt: replaceTargetsOpt,
|
||||
urns: make(map[resource.URN]bool),
|
||||
reads: make(map[resource.URN]bool),
|
||||
creates: make(map[resource.URN]bool),
|
||||
|
|
Loading…
Reference in a new issue