Loosen resource targeting restrictions. (#3426)
- If an untargeted create would not affect the inputs of any targeted resources, do not fail the update. Untargeted creates that are directly dependend on by targeted resources will still cause failures that inform the user to add the untargeted resources to the --target list. - Users may now pass the `--target-dependents` flag to allow targeted destroys to automatically target dependents that must be destroyed in order to destroy an explicitly targeted resource.
This commit is contained in:
parent
8547ede659
commit
1908a18d20
|
@ -18,6 +18,12 @@ CHANGELOG
|
|||
|
||||
- Add support for go1.13.x
|
||||
|
||||
- `pulumi update --target` and `pulumi destroy --target` will both error if they determine a
|
||||
dependent resource needs to be updated, destroyed, or created that was not was specified in the
|
||||
`--target` list. To proceed with an `update/destroy` after this error, either specify all the
|
||||
reported resources as `--target`s, or pass the `--target-dependents` flag to allow necessary
|
||||
changes to unspecified dependent targets.
|
||||
|
||||
## 1.5.2 (2019-11-13)
|
||||
|
||||
- `pulumi policy publish` now determines the Policy Pack name from the Policy Pack, and the
|
||||
|
|
|
@ -47,6 +47,7 @@ func newDestroyCmd() *cobra.Command {
|
|||
var suppressOutputs bool
|
||||
var yes bool
|
||||
var targets *[]string
|
||||
var targetDependents bool
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "destroy",
|
||||
|
@ -118,11 +119,12 @@ func newDestroyCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
opts.Engine = engine.UpdateOptions{
|
||||
Parallel: parallel,
|
||||
Debug: debug,
|
||||
Refresh: refresh,
|
||||
DestroyTargets: targetUrns,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
Parallel: parallel,
|
||||
Debug: debug,
|
||||
Refresh: refresh,
|
||||
DestroyTargets: targetUrns,
|
||||
TargetDependents: targetDependents,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
}
|
||||
|
||||
_, res := s.Destroy(commandContext(), backend.UpdateOperation{
|
||||
|
@ -163,6 +165,9 @@ func newDestroyCmd() *cobra.Command {
|
|||
"target", "t", []string{},
|
||||
"Specify a single resource URN to destroy. All resources necessary to destroy this target will also be destroyed."+
|
||||
" Multiple resources can be specified using: --target urn1 --target urn2")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&targetDependents, "target-dependents", false,
|
||||
"Allows destroying of dependent targets discovered but not specified in --target list")
|
||||
|
||||
// Flags for engine.UpdateOptions.
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
|
|
|
@ -70,6 +70,7 @@ func newUpCmd() *cobra.Command {
|
|||
var targets []string
|
||||
var replaces []string
|
||||
var targetReplaces []string
|
||||
var targetDependents bool
|
||||
|
||||
// up implementation used when the source of the Pulumi program is in the current working directory.
|
||||
upWorkingDirectory := func(opts backend.UpdateOptions) result.Result {
|
||||
|
@ -126,6 +127,7 @@ func newUpCmd() *cobra.Command {
|
|||
ReplaceTargets: replaceURNs,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
UpdateTargets: targetURNs,
|
||||
TargetDependents: targetDependents,
|
||||
}
|
||||
|
||||
changes, res := s.Update(commandContext(), backend.UpdateOperation{
|
||||
|
@ -403,6 +405,9 @@ func newUpCmd() *cobra.Command {
|
|||
&targetReplaces, "target-replace", []string{},
|
||||
"Specify a single resource URN to replace. Other resources will not be updated."+
|
||||
" Shorthand for --target urn --replace urn.")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&targetDependents, "target-dependents", false,
|
||||
"Allows updating of dependent targets discovered but not specified in --target list")
|
||||
|
||||
// Flags for engine.UpdateOptions.
|
||||
if hasDebugCommands() || hasExperimentalCommands() {
|
||||
|
|
|
@ -169,10 +169,21 @@ type sameSnapshotMutation struct {
|
|||
// mustWrite returns true if any semantically meaningful difference exists between the old and new states of a same
|
||||
// step that forces us to write the checkpoint. If no such difference exists, the checkpoint write that corresponds to
|
||||
// this step can be elided.
|
||||
func (ssm *sameSnapshotMutation) mustWrite(old, new *resource.State) bool {
|
||||
func (ssm *sameSnapshotMutation) mustWrite(step *deploy.SameStep) bool {
|
||||
old := step.Old()
|
||||
new := step.New()
|
||||
|
||||
contract.Assert(old.Delete == new.Delete)
|
||||
contract.Assert(old.External == new.External)
|
||||
|
||||
if step.IsSkippedCreate() {
|
||||
// In the case of a 'resource create' in a program that wasn't specified by the user in the
|
||||
// --target list, we *never* want to write this to the checkpoint. We treat it as if it
|
||||
// doesn't exist at all. That way when the program runs the next time, we'll actually
|
||||
// create it.
|
||||
return false
|
||||
}
|
||||
|
||||
// If the URN of this resource has changed, we must write the checkpoint. This should only be possible when a
|
||||
// resource is aliased.
|
||||
if old.URN != new.URN {
|
||||
|
@ -246,7 +257,7 @@ func (ssm *sameSnapshotMutation) End(step deploy.Step, successful bool) error {
|
|||
//
|
||||
// As such, we diff all of the non-input properties of the resource here and write the snapshot if we find any
|
||||
// changes.
|
||||
if !ssm.mustWrite(step.Old(), step.New()) {
|
||||
if !ssm.mustWrite(step.(*deploy.SameStep)) {
|
||||
logging.V(9).Infof("SnapshotManager: sameSnapshotMutation.End() eliding write")
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -72,6 +72,11 @@ func GetCannotDeleteParentResourceWithoutAlsoDeletingChildError(urn resource.URN
|
|||
return newError(urn, 2012, "Cannot delete parent resource '%v' without also deleting child '%v'.")
|
||||
}
|
||||
|
||||
func GetResourceIsBeingCreatedButWasNotSpecifiedInTargetList(urn resource.URN) *Diag {
|
||||
return newError(urn, 2013, "Resource '%v' is being created but was not specified in -target list.")
|
||||
func GetResourceWillBeCreatedButWasNotSpecifiedInTargetList(urn resource.URN) *Diag {
|
||||
return newError(urn, 2013, `Resource '%v' depends on '%v' which was was not specified in --target list.`)
|
||||
}
|
||||
|
||||
func GetResourceWillBeDestroyedButWasNotSpecifiedInTargetList(urn resource.URN) *Diag {
|
||||
return newError(urn, 2014, `Resource '%v' will be destroyed but was not specified in --target list.
|
||||
Either include resource in --target list or pass --target-dependents to proceed.`)
|
||||
}
|
||||
|
|
|
@ -4404,7 +4404,7 @@ func TestImportUpdatedID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestDeleteTarget(t *testing.T) {
|
||||
func TestDestroyTarget(t *testing.T) {
|
||||
// Try refreshing a stack with combinations of the above resources as target to destroy.
|
||||
subsets := combinations.All(complexTestDependencyGraphNames)
|
||||
|
||||
|
@ -4412,26 +4412,33 @@ func TestDeleteTarget(t *testing.T) {
|
|||
// limit to up to 3 resources to destroy. This keeps the test running time under
|
||||
// control as it only generates a few hundred combinations instead of several thousand.
|
||||
if len(subset) <= 3 {
|
||||
deleteSpecificTargets(t, subset, func(urns []resource.URN, deleted map[resource.URN]bool) {})
|
||||
destroySpecificTargets(t, subset, true, /*targetDependents*/
|
||||
func(urns []resource.URN, deleted map[resource.URN]bool) {})
|
||||
}
|
||||
}
|
||||
|
||||
deleteSpecificTargets(t, []string{"A"}, func(urns []resource.URN, deleted map[resource.URN]bool) {
|
||||
// when deleting 'A' we expect A, B, C, E, F, and K to be deleted
|
||||
names := complexTestDependencyGraphNames
|
||||
assert.Equal(t, map[resource.URN]bool{
|
||||
pickURN(t, urns, names, "A"): true,
|
||||
pickURN(t, urns, names, "B"): true,
|
||||
pickURN(t, urns, names, "C"): true,
|
||||
pickURN(t, urns, names, "E"): true,
|
||||
pickURN(t, urns, names, "F"): true,
|
||||
pickURN(t, urns, names, "K"): true,
|
||||
}, deleted)
|
||||
})
|
||||
destroySpecificTargets(
|
||||
t, []string{"A"}, true, /*targetDependents*/
|
||||
func(urns []resource.URN, deleted map[resource.URN]bool) {
|
||||
// when deleting 'A' we expect A, B, C, E, F, and K to be deleted
|
||||
names := complexTestDependencyGraphNames
|
||||
assert.Equal(t, map[resource.URN]bool{
|
||||
pickURN(t, urns, names, "A"): true,
|
||||
pickURN(t, urns, names, "B"): true,
|
||||
pickURN(t, urns, names, "C"): true,
|
||||
pickURN(t, urns, names, "E"): true,
|
||||
pickURN(t, urns, names, "F"): true,
|
||||
pickURN(t, urns, names, "K"): true,
|
||||
}, deleted)
|
||||
})
|
||||
|
||||
destroySpecificTargets(
|
||||
t, []string{"A"}, false, /*targetDependents*/
|
||||
func(urns []resource.URN, deleted map[resource.URN]bool) {})
|
||||
}
|
||||
|
||||
func deleteSpecificTargets(
|
||||
t *testing.T, targets []string,
|
||||
func destroySpecificTargets(
|
||||
t *testing.T, targets []string, targetDependents bool,
|
||||
validate func(urns []resource.URN, deleted map[resource.URN]bool)) {
|
||||
|
||||
// A
|
||||
|
@ -4472,6 +4479,7 @@ func deleteSpecificTargets(
|
|||
}
|
||||
|
||||
p.Options.host = deploytest.NewPluginHost(nil, nil, program, loaders...)
|
||||
p.Options.TargetDependents = targetDependents
|
||||
|
||||
destroyTargets := []resource.URN{}
|
||||
for _, target := range targets {
|
||||
|
@ -4481,9 +4489,11 @@ func deleteSpecificTargets(
|
|||
p.Options.DestroyTargets = destroyTargets
|
||||
t.Logf("Destroying targets: %v", destroyTargets)
|
||||
|
||||
// If we're not forcing the targets to be destroyed, then expect to get a failure here as
|
||||
// we'll have downstream resources to delete that weren't specified explicitly.
|
||||
p.Steps = []TestStep{{
|
||||
Op: Destroy,
|
||||
ExpectFailure: false,
|
||||
ExpectFailure: !targetDependents,
|
||||
Validate: func(project workspace.Project, target deploy.Target, j *Journal,
|
||||
evts []Event, res result.Result) result.Result {
|
||||
|
||||
|
@ -4520,6 +4530,8 @@ func TestUpdateTarget(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
updateSpecificTargets(t, []string{"A"})
|
||||
|
||||
// Also update a target that doesn't exist to make sure we don't crash or otherwise go off the rails.
|
||||
updateInvalidTarget(t)
|
||||
}
|
||||
|
@ -4603,7 +4615,6 @@ func updateSpecificTargets(t *testing.T, targets []string) {
|
|||
return res
|
||||
},
|
||||
}}
|
||||
|
||||
p.Run(t, old)
|
||||
}
|
||||
|
||||
|
@ -4648,6 +4659,236 @@ func updateInvalidTarget(t *testing.T) {
|
|||
p.Run(t, old)
|
||||
}
|
||||
|
||||
func TestCreateDuringTargetedUpdate_CreateMentionedAsTarget(t *testing.T) {
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
|
||||
|
||||
p := &TestPlan{
|
||||
Options: UpdateOptions{host: host1},
|
||||
}
|
||||
|
||||
p.Steps = []TestStep{{Op: Update}}
|
||||
snap1 := p.Run(t, nil)
|
||||
|
||||
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
|
||||
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
|
||||
|
||||
resA := p.NewURN("pkgA:m:typA", "resA", "")
|
||||
resB := p.NewURN("pkgA:m:typA", "resB", "")
|
||||
p.Options.host = host2
|
||||
p.Options.UpdateTargets = []resource.URN{resA, resB}
|
||||
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)
|
||||
|
||||
for _, entry := range j.Entries {
|
||||
if entry.Step.URN() == resA {
|
||||
assert.Equal(t, deploy.OpSame, entry.Step.Op())
|
||||
} else if entry.Step.URN() == resB {
|
||||
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}}
|
||||
p.Run(t, snap1)
|
||||
}
|
||||
|
||||
func TestCreateDuringTargetedUpdate_UntargetedCreateNotReferenced(t *testing.T) {
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
|
||||
|
||||
p := &TestPlan{
|
||||
Options: UpdateOptions{host: host1},
|
||||
}
|
||||
|
||||
p.Steps = []TestStep{{Op: Update}}
|
||||
snap1 := p.Run(t, nil)
|
||||
|
||||
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
|
||||
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
|
||||
|
||||
resA := p.NewURN("pkgA:m:typA", "resA", "")
|
||||
|
||||
p.Options.host = host2
|
||||
p.Options.UpdateTargets = []resource.URN{resA}
|
||||
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)
|
||||
|
||||
for _, entry := range j.Entries {
|
||||
// everything should be a same op here.
|
||||
assert.Equal(t, deploy.OpSame, entry.Step.Op())
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}}
|
||||
p.Run(t, snap1)
|
||||
}
|
||||
|
||||
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByTarget(t *testing.T) {
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
|
||||
|
||||
p := &TestPlan{
|
||||
Options: UpdateOptions{host: host1},
|
||||
}
|
||||
|
||||
p.Steps = []TestStep{{Op: Update}}
|
||||
p.Run(t, nil)
|
||||
|
||||
resA := p.NewURN("pkgA:m:typA", "resA", "")
|
||||
resB := p.NewURN("pkgA:m:typA", "resB", "")
|
||||
|
||||
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
|
||||
// satisfy.
|
||||
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true,
|
||||
deploytest.ResourceOptions{
|
||||
Dependencies: []resource.URN{resB},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
|
||||
|
||||
p.Options.host = host2
|
||||
p.Options.UpdateTargets = []resource.URN{resA}
|
||||
p.Steps = []TestStep{{
|
||||
Op: Update,
|
||||
ExpectFailure: true,
|
||||
}}
|
||||
p.Run(t, nil)
|
||||
}
|
||||
|
||||
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByUntargetedCreate(t *testing.T) {
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
|
||||
|
||||
p := &TestPlan{
|
||||
Options: UpdateOptions{host: host1},
|
||||
}
|
||||
|
||||
p.Steps = []TestStep{{Op: Update}}
|
||||
snap1 := p.Run(t, nil)
|
||||
|
||||
resA := p.NewURN("pkgA:m:typA", "resA", "")
|
||||
resB := p.NewURN("pkgA:m:typA", "resB", "")
|
||||
|
||||
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
|
||||
// satisfy.
|
||||
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true,
|
||||
deploytest.ResourceOptions{
|
||||
Dependencies: []resource.URN{resB},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
|
||||
|
||||
p.Options.host = host2
|
||||
p.Options.UpdateTargets = []resource.URN{resA}
|
||||
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)
|
||||
|
||||
for _, entry := range j.Entries {
|
||||
assert.Equal(t, deploy.OpSame, entry.Step.Op())
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
}}
|
||||
p.Run(t, snap1)
|
||||
}
|
||||
|
||||
func TestDependencyChangeDBR(t *testing.T) {
|
||||
p := &TestPlan{}
|
||||
|
||||
|
|
|
@ -185,6 +185,7 @@ func (planResult *planResult) Walk(cancelCtx *Context, events deploy.Events, pre
|
|||
ReplaceTargets: planResult.Options.ReplaceTargets,
|
||||
DestroyTargets: planResult.Options.DestroyTargets,
|
||||
UpdateTargets: planResult.Options.UpdateTargets,
|
||||
TargetDependents: planResult.Options.TargetDependents,
|
||||
TrustDependencies: planResult.Options.trustDependencies,
|
||||
UseLegacyDiff: planResult.Options.UseLegacyDiff,
|
||||
}
|
||||
|
|
|
@ -74,6 +74,10 @@ type UpdateOptions struct {
|
|||
// Specific resources to update during an update operation.
|
||||
UpdateTargets []resource.URN
|
||||
|
||||
// true if we're allowing dependent targets to change, even if not specified in one of the above
|
||||
// XXXTargets lists.
|
||||
TargetDependents bool
|
||||
|
||||
// true if the engine should use legacy diffing behavior during an update.
|
||||
UseLegacyDiff bool
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ type Options struct {
|
|||
ReplaceTargets []resource.URN // Specific resources to replace.
|
||||
DestroyTargets []resource.URN // Specific resources to destroy.
|
||||
UpdateTargets []resource.URN // Specific resources to update.
|
||||
TargetDependents bool // true if we're allowing things to proceed, even with unspecified targets
|
||||
TrustDependencies bool // whether or not to trust the resource dependency graph.
|
||||
UseLegacyDiff bool // whether or not to use legacy diffing behavior.
|
||||
}
|
||||
|
|
|
@ -19,12 +19,10 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy/providers"
|
||||
"github.com/pulumi/pulumi/pkg/resource/graph"
|
||||
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
"github.com/pulumi/pulumi/pkg/util/logging"
|
||||
"github.com/pulumi/pulumi/pkg/util/result"
|
||||
|
@ -269,32 +267,14 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
// If the step generator and step executor were both successful, then we send all the resources
|
||||
// observed to be analyzed. Otherwise, this step is skipped.
|
||||
if res == nil && !pe.stepExec.Errored() {
|
||||
resourcesSeen := pe.stepGen.resourceStates
|
||||
resources := make([]plugin.AnalyzerResource, 0, len(resourcesSeen))
|
||||
for _, v := range resourcesSeen {
|
||||
resources = append(resources, plugin.AnalyzerResource{
|
||||
Type: v.Type,
|
||||
// Unlike Analyze, AnalyzeStack is called on the final outputs of each resource,
|
||||
// to verify the final stack is in a compliant state.
|
||||
Properties: v.Outputs,
|
||||
})
|
||||
}
|
||||
|
||||
analyzers := pe.stepGen.plan.ctx.Host.ListAnalyzers()
|
||||
for _, analyzer := range analyzers {
|
||||
diagnostics, aErr := analyzer.AnalyzeStack(resources)
|
||||
if aErr != nil {
|
||||
return result.FromError(aErr)
|
||||
}
|
||||
for _, d := range diagnostics {
|
||||
pe.stepGen.hasPolicyViolations = pe.stepGen.hasPolicyViolations || (d.EnforcementLevel == apitype.Mandatory)
|
||||
pe.stepGen.opts.Events.OnPolicyViolation("" /* don't associate with any particular URN */, d)
|
||||
}
|
||||
res := pe.stepGen.AnalyzeResources()
|
||||
if res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out if execution failed and why. Step generation and execution errors trump cancellation.
|
||||
if res != nil || pe.stepExec.Errored() || pe.stepGen.hasPolicyViolations {
|
||||
if res != nil || pe.stepExec.Errored() || pe.stepGen.Errored() {
|
||||
// TODO(cyrusn): We seem to be losing any information about the original 'res's errors. Should
|
||||
// we be doing a merge here?
|
||||
pe.reportExecResult("failed", preview)
|
||||
|
|
|
@ -61,6 +61,10 @@ type SameStep struct {
|
|||
reg RegisterResourceEvent // the registration intent to convey a URN back to.
|
||||
old *resource.State // the state of the resource before this step.
|
||||
new *resource.State // the state of the resource after this step.
|
||||
|
||||
// If this is a same-step for a resource being created but which was not --target'ed by the user
|
||||
// (and thus was skipped).
|
||||
skippedCreate bool
|
||||
}
|
||||
|
||||
var _ Step = (*SameStep)(nil)
|
||||
|
@ -84,6 +88,28 @@ func NewSameStep(plan *Plan, reg RegisterResourceEvent, old *resource.State, new
|
|||
}
|
||||
}
|
||||
|
||||
// NewSkippedCreateStep produces a SameStep for a resource that was created but not targeted
|
||||
// by the user (and thus was skipped). These act as no-op steps (hence 'same') since we are not
|
||||
// actually creating the resource, but ensure that we complete resource-registration and convey the
|
||||
// right information downstream. For example, we will not write these into the checkpoint file.
|
||||
func NewSkippedCreateStep(plan *Plan, reg RegisterResourceEvent, new *resource.State) Step {
|
||||
contract.Assert(new != nil)
|
||||
contract.Assert(new.URN != "")
|
||||
contract.Assert(new.ID == "")
|
||||
contract.Assert(!new.Custom || new.Provider != "" || providers.IsProviderType(new.Type))
|
||||
contract.Assert(!new.Delete)
|
||||
|
||||
// Make the old state here a direct copy of the new state
|
||||
old := *new
|
||||
return &SameStep{
|
||||
plan: plan,
|
||||
reg: reg,
|
||||
old: &old,
|
||||
new: new,
|
||||
skippedCreate: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SameStep) Op() StepOp { return OpSame }
|
||||
func (s *SameStep) Plan() *Plan { return s.plan }
|
||||
func (s *SameStep) Type() tokens.Type { return s.new.Type }
|
||||
|
@ -102,6 +128,10 @@ func (s *SameStep) Apply(preview bool) (resource.Status, StepCompleteFunc, error
|
|||
return resource.StatusOK, complete, nil
|
||||
}
|
||||
|
||||
func (s *SameStep) IsSkippedCreate() bool {
|
||||
return s.skippedCreate
|
||||
}
|
||||
|
||||
// CreateStep is a mutating step that creates an entirely new resource.
|
||||
type CreateStep struct {
|
||||
plan *Plan // the current plan.
|
||||
|
|
|
@ -182,7 +182,7 @@ func (se *stepExecutor) ExecuteRegisterResourceOutputs(e RegisterResourceOutputs
|
|||
e.Done()
|
||||
}
|
||||
|
||||
// Errored returnes whether or not this step executor saw a step whose execution ended in failure.
|
||||
// Errored returns whether or not this step executor saw a step whose execution ended in failure.
|
||||
func (se *stepExecutor) Errored() bool {
|
||||
return se.sawError.Load().(bool)
|
||||
}
|
||||
|
|
|
@ -43,28 +43,39 @@ type stepGenerator struct {
|
|||
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.
|
||||
hasPolicyViolations bool
|
||||
// signals that one or more errors 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.
|
||||
sawError bool
|
||||
|
||||
urns map[resource.URN]bool // set of URNs discovered for this plan
|
||||
reads map[resource.URN]bool // set of URNs read for this plan
|
||||
deletes map[resource.URN]bool // set of URNs deleted in this plan
|
||||
replaces map[resource.URN]bool // set of URNs replaced in this plan
|
||||
updates map[resource.URN]bool // set of URNs updated in this plan
|
||||
creates map[resource.URN]bool // set of URNs created in this plan
|
||||
sames map[resource.URN]bool // set of URNs that were not changed in this plan
|
||||
|
||||
// set of URNs that would have been created, but were filtered out because the user didn't
|
||||
// specify them with --target
|
||||
skippedCreates map[resource.URN]bool
|
||||
|
||||
urns map[resource.URN]bool // set of URNs discovered for this plan
|
||||
reads map[resource.URN]bool // set of URNs read for this plan
|
||||
deletes map[resource.URN]bool // set of URNs deleted in this plan
|
||||
replaces map[resource.URN]bool // set of URNs replaced in this plan
|
||||
updates map[resource.URN]bool // set of URNs updated in this plan
|
||||
creates map[resource.URN]bool // set of URNs created in this plan
|
||||
sames map[resource.URN]bool // set of URNs that were not changed in this plan
|
||||
pendingDeletes map[*resource.State]bool // set of resources (not URNs!) that are pending deletion
|
||||
providers map[resource.URN]*resource.State // URN map of providers that we have seen so far.
|
||||
resourceStates map[resource.URN]*resource.State // URN map of state for ALL resources we have seen so far.
|
||||
|
||||
// a map from URN to a list of property keys that caused the replacement of a dependent resource during a
|
||||
// delete-before-replace.
|
||||
dependentReplaceKeys map[resource.URN][]resource.PropertyKey
|
||||
|
||||
// a map from old names (aliased URNs) to the new URN that aliased to them.
|
||||
aliased map[resource.URN]resource.URN
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) isTargetedUpdate() bool {
|
||||
return sg.updateTargetsOpt != nil || sg.replaceTargetsOpt != nil
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) isTargetedForUpdate(urn resource.URN) bool {
|
||||
return sg.updateTargetsOpt == nil || sg.updateTargetsOpt[urn]
|
||||
}
|
||||
|
@ -73,6 +84,10 @@ func (sg *stepGenerator) isTargetedReplace(urn resource.URN) bool {
|
|||
return sg.replaceTargetsOpt != nil && sg.replaceTargetsOpt[urn]
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) Errored() bool {
|
||||
return sg.sawError
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -137,7 +152,55 @@ 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(event RegisterResourceEvent) ([]Step, result.Result) {
|
||||
steps, res := sg.generateSteps(event)
|
||||
if res != nil {
|
||||
contract.Assert(len(steps) == 0)
|
||||
return nil, res
|
||||
}
|
||||
if !sg.isTargetedUpdate() {
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
// We got a set of steps to perfom during a targeted update. If any of the steps are not same steps and depend on
|
||||
// creates we skipped because they were not in the --target list, issue an error that that the create was necessary
|
||||
// and that the user must target the resource to create.
|
||||
for _, step := range steps {
|
||||
if step.Op() == OpSame || step.New() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, urn := range step.New().Dependencies {
|
||||
if sg.skippedCreates[urn] {
|
||||
// Targets were specified, but didn't include this resource to create. And a
|
||||
// resource we are producing a step for does depend on this created resource.
|
||||
// Give a particular error in that case to let them know. Also mark that we're
|
||||
// in an error state so that we eventually will error out of the entire
|
||||
// application run.
|
||||
d := diag.GetResourceWillBeCreatedButWasNotSpecifiedInTargetList(step.URN())
|
||||
|
||||
sg.plan.Diag().Errorf(d, step.URN(), urn)
|
||||
sg.sawError = true
|
||||
|
||||
if !sg.plan.preview {
|
||||
// In preview we keep going so that the user will hear about all the problems and can then
|
||||
// fix up their command once (as opposed to adding a target, rerunning, adding a target,
|
||||
// rerunning, etc. etc.).
|
||||
//
|
||||
// Doing a normal run. We should not proceed here at all. We don't want to create
|
||||
// something the user didn't ask for.
|
||||
return nil, result.Bail()
|
||||
}
|
||||
|
||||
// Remove the resource from the list of skipped creates so that we do not issue duplicate diagnostics.
|
||||
delete(sg.skippedCreates, urn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, result.Result) {
|
||||
var invalid bool // will be set to true if this object fails validation.
|
||||
|
||||
goal := event.Goal()
|
||||
|
@ -274,18 +337,16 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res
|
|||
Type: new.Type,
|
||||
Properties: inputs,
|
||||
}
|
||||
diagnostics, aErr := analyzer.Analyze(r)
|
||||
if aErr != nil {
|
||||
return nil, result.FromError(aErr)
|
||||
diagnostics, err := analyzer.Analyze(r)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
}
|
||||
for _, d := range diagnostics {
|
||||
// TODO(hausdorff): Batch up failures and report them all at once during preview. This code here
|
||||
// will cause them to fail eagerly, and stop the plan immediately.
|
||||
if d.EnforcementLevel == apitype.Mandatory {
|
||||
if !sg.plan.preview {
|
||||
invalid = true
|
||||
}
|
||||
sg.hasPolicyViolations = true
|
||||
sg.sawError = true
|
||||
}
|
||||
sg.opts.Events.OnPolicyViolation(new.URN, d)
|
||||
}
|
||||
|
@ -390,26 +451,38 @@ func (sg *stepGenerator) GenerateSteps(event RegisterResourceEvent) ([]Step, res
|
|||
return []Step{NewSameStep(sg.plan, event, old, new)}, nil
|
||||
}
|
||||
|
||||
if !sg.isTargetedForUpdate(urn) {
|
||||
d := diag.GetResourceIsBeingCreatedButWasNotSpecifiedInTargetList(urn)
|
||||
|
||||
if sg.plan.preview {
|
||||
// During preview we only warn here but let the planning proceed. This allows the user
|
||||
// to hear about *all* the potential resources they'd need to add a -target arg for.
|
||||
// This prevents the annoying scenario where you get notified about a problem, fix it,
|
||||
// rerun and then get notified about the very next problem.
|
||||
sg.plan.Diag().Warningf(d, urn)
|
||||
} else {
|
||||
// Targets were specified, but didn't include this resource to create. Give a particular
|
||||
// error in that case and stop immediately.
|
||||
sg.plan.Diag().Errorf(d, urn)
|
||||
return nil, result.Bail()
|
||||
}
|
||||
}
|
||||
|
||||
// Case 4: Not Case 1, 2, or 3
|
||||
// If a resource isn't being recreated and it's not being updated or replaced,
|
||||
// it's just being created.
|
||||
|
||||
// We're in the create stage now. In a normal run just issue a 'create step'. If, however, the
|
||||
// user is doing a run with `--target`s, then we need to operate specially here.
|
||||
//
|
||||
// 1. If the user did include this resource urn in the --target list, then we can proceed
|
||||
// normally and issue a create step for this.
|
||||
//
|
||||
// 2. However, if they did not include the resource in the --target list, then we want to flat
|
||||
// out ignore it (just like we ignore updates to resource not in the --target list). This has
|
||||
// interesting implications though. Specifically, what to do if a prop from this resource is
|
||||
// then actually needed by a property we *are* doing a targeted create/update for.
|
||||
//
|
||||
// In that case, we want to error to force the user to be explicit about wanting this resource
|
||||
// to be created. However, we can't issue the error until later on when the resource is
|
||||
// referenced. So, to support this we create a special "same" step here for this resource. That
|
||||
// "same" step has a bit on it letting us know that it is for this case. If we then later see a
|
||||
// resource that depends on this resource, we will issue an error letting the user know.
|
||||
//
|
||||
// We will also not record this non-created resource into the checkpoint as it doesn't actually
|
||||
// exist.
|
||||
|
||||
if !sg.isTargetedForUpdate(urn) &&
|
||||
!providers.IsProviderType(goal.Type) {
|
||||
|
||||
sg.sames[urn] = true
|
||||
sg.skippedCreates[urn] = true
|
||||
return []Step{NewSkippedCreateStep(sg.plan, event, new)}, nil
|
||||
}
|
||||
|
||||
sg.creates[urn] = true
|
||||
logging.V(7).Infof("Planner decided to create '%v' (inputs=%v)", urn, new.Inputs)
|
||||
return []Step{NewCreateStep(sg.plan, event, new)}, nil
|
||||
|
@ -644,6 +717,34 @@ func (sg *stepGenerator) GenerateDeletes(targetsOpt map[resource.URN]bool) ([]St
|
|||
dels = filtered
|
||||
}
|
||||
|
||||
deletingUnspecifiedTarget := false
|
||||
for _, step := range dels {
|
||||
urn := step.URN()
|
||||
if targetsOpt != nil && !targetsOpt[urn] && !sg.opts.TargetDependents {
|
||||
d := diag.GetResourceWillBeDestroyedButWasNotSpecifiedInTargetList(urn)
|
||||
|
||||
// Targets were specified, but didn't include this resource to create. Report all the
|
||||
// problematic targets so the user doesn't have to keep adding them one at a time and
|
||||
// re-running the operation.
|
||||
//
|
||||
// Mark that step generation entered an error state so that the entire app run fails.
|
||||
sg.plan.Diag().Errorf(d, urn)
|
||||
sg.sawError = true
|
||||
|
||||
deletingUnspecifiedTarget = true
|
||||
}
|
||||
}
|
||||
|
||||
if deletingUnspecifiedTarget && !sg.plan.preview {
|
||||
// In preview we keep going so that the user will hear about all the problems and can then
|
||||
// fix up their command once (as opposed to adding a target, rerunning, adding a target,
|
||||
// rerunning, etc. etc.).
|
||||
//
|
||||
// Doing a normal run. We should not proceed here at all. We don't want to delete
|
||||
// something the user didn't ask for.
|
||||
return nil, result.Bail()
|
||||
}
|
||||
|
||||
return dels, nil
|
||||
}
|
||||
|
||||
|
@ -1153,6 +1254,33 @@ func (sg *stepGenerator) calculateDependentReplacements(root *resource.State) ([
|
|||
return toReplace, nil
|
||||
}
|
||||
|
||||
func (sg *stepGenerator) AnalyzeResources() result.Result {
|
||||
resourcesSeen := sg.resourceStates
|
||||
resources := make([]plugin.AnalyzerResource, 0, len(resourcesSeen))
|
||||
for _, v := range resourcesSeen {
|
||||
resources = append(resources, plugin.AnalyzerResource{
|
||||
Type: v.Type,
|
||||
// Unlike Analyze, AnalyzeStack is called on the final outputs of each resource,
|
||||
// to verify the final stack is in a compliant state.
|
||||
Properties: v.Outputs,
|
||||
})
|
||||
}
|
||||
|
||||
analyzers := sg.plan.ctx.Host.ListAnalyzers()
|
||||
for _, analyzer := range analyzers {
|
||||
diagnostics, aErr := analyzer.AnalyzeStack(resources)
|
||||
if aErr != nil {
|
||||
return result.FromError(aErr)
|
||||
}
|
||||
for _, d := range diagnostics {
|
||||
sg.sawError = sg.sawError || (d.EnforcementLevel == apitype.Mandatory)
|
||||
sg.opts.Events.OnPolicyViolation("" /* don't associate with any particular URN */, d)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newStepGenerator creates a new step generator that operates on the given plan.
|
||||
func newStepGenerator(
|
||||
plan *Plan, opts Options, updateTargetsOpt, replaceTargetsOpt map[resource.URN]bool) *stepGenerator {
|
||||
|
@ -1169,6 +1297,7 @@ func newStepGenerator(
|
|||
replaces: make(map[resource.URN]bool),
|
||||
updates: make(map[resource.URN]bool),
|
||||
deletes: make(map[resource.URN]bool),
|
||||
skippedCreates: make(map[resource.URN]bool),
|
||||
pendingDeletes: make(map[*resource.State]bool),
|
||||
providers: make(map[resource.URN]*resource.State),
|
||||
resourceStates: make(map[resource.URN]*resource.State),
|
||||
|
|
Loading…
Reference in a new issue