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:
CyrusNajmabadi 2019-11-18 23:28:25 -05:00 committed by Pat Gavlin
parent 8547ede659
commit 1908a18d20
13 changed files with 504 additions and 86 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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