Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't check update plans against invalid resources #9254

Merged
merged 2 commits into from Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG_PENDING.md
Expand Up @@ -27,4 +27,7 @@
[#9197](https://github.com/pulumi/pulumi/issues/9197)

- [cli/engine] - Fix a panic due to passing `""` as the ID for a resource read.
[#9243](https://github.com/pulumi/pulumi/pull/9243)
[#9243](https://github.com/pulumi/pulumi/pull/9243)

- [cli/engine] - Fix a panic due to `Check` failing while using update plans.
[#9254](https://github.com/pulumi/pulumi/pull/9254)
83 changes: 82 additions & 1 deletion pkg/engine/lifeycletest/pulumi_test.go
Expand Up @@ -3646,7 +3646,7 @@ func TestExpectedUnneededDelete(t *testing.T) {
func TestResoucesWithSames(t *testing.T) {
t.Parallel()

// This test checks that if between generating a constriant and running the update that if new resources have been
// This test checks that if between generating a constraint and running the update that if new resources have been
// added to the stack that the update doesn't change those resources in any way that they don't cause constraint
// errors.

Expand Down Expand Up @@ -4500,3 +4500,84 @@ func TestInvalidGetIDReportsUserError(t *testing.T) {
assert.NotNil(t, snap)
assert.Len(t, snap.Resources, 1)
}

func TestPlannedUpdateWithCheckFailure(t *testing.T) {
// Regression test for https://github.com/pulumi/pulumi/issues/9247

t.Parallel()

loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
return news, resource.StatusOK, nil
},
CheckF: func(urn resource.URN, olds, news resource.PropertyMap,
sequenceNumber int) (resource.PropertyMap, []plugin.CheckFailure, error) {
if news["foo"].StringValue() == "bad" {
return nil, []plugin.CheckFailure{
{Property: resource.PropertyKey("foo"), Reason: "Bad foo"},
}, nil
}
return news, nil, nil
},
}, nil
}),
}

var ins resource.PropertyMap
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: ins,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)

p := &TestPlan{
Options: UpdateOptions{Host: host, ExperimentalPlans: true},
}

project := p.GetProject()

// Generate a plan with bad inputs
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "bad",
})
validate := ExpectDiagMessage(t,
"<{%reset%}>pkgA:m:typA resource 'resA': property foo value {bad} has a problem: Bad foo<{%reset%}>\n")
plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, validate)
assert.Nil(t, plan)
assert.Nil(t, res)

// Generate a plan with good inputs
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "good",
})
plan, res = TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil)
assert.NotNil(t, plan)
assert.Contains(t, plan.ResourcePlans, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA"))
assert.Nil(t, res)

// Try and run against the plan with inputs that will fail Check
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "bad",
})
p.Options.Plan = plan.Clone()
validate = ExpectDiagMessage(t,
"<{%reset%}>pkgA:m:typA resource 'resA': property foo value {bad} has a problem: Bad foo<{%reset%}>\n")
snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate)
assert.Nil(t, res)
assert.NotNil(t, snap)

// Check the resource's state.
if !assert.Len(t, snap.Resources, 1) {
return
}
}
7 changes: 4 additions & 3 deletions pkg/resource/deploy/step_generator.go
Expand Up @@ -447,8 +447,8 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
new.Inputs = inputs
}

// If we're in experimental mode generate a plan
if sg.opts.ExperimentalPlans {
// If the resource is valid and we're in experimental mode generate a plan
if !invalid && sg.opts.ExperimentalPlans {
if recreating || wasExternal || sg.isTargetedReplace(urn) || !hasOld {
oldInputs = nil
}
Expand All @@ -463,7 +463,8 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res

// If there is a plan for this resource, validate that the program goal conforms to the plan.
// If theres no plan for this resource check that nothing has been changed.
if sg.deployment.plan != nil {
// We don't check plans if the resource is invalid, it's going to fail anyway.
if !invalid && sg.deployment.plan != nil {
resourcePlan, ok := sg.deployment.plan.ResourcePlans[urn]
if !ok {
if old == nil {
Expand Down