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:
Pat Gavlin 2019-10-30 17:16:55 -07:00 committed by GitHub
parent c5bd24aa30
commit 23a84df254
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 165 additions and 29 deletions

View file

@ -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`.

View file

@ -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() {

View file

@ -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)
}

View file

@ -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,

View file

@ -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

View file

@ -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.

View file

@ -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
}

View file

@ -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),