[engine] Clear pending operations with refresh.

Just what it says on the tin. This is implemented by moving the check
for pending operations in the last statefile into the deployment
executor and making it conditional on whether or not a refresh is being
performed (either via `pulumi refresh` or `pulumi up -r`). Because
pending operations are not carried over from the base statefile, this
has the effect of clearing pending operations if a refresh is performed.

Fixes #4265.
This commit is contained in:
Pat Gavlin 2021-11-16 12:07:21 -08:00
parent 3e2f36548e
commit e07c2c2c21
3 changed files with 69 additions and 4 deletions

View file

@ -538,6 +538,73 @@ func TestPreviewWithPendingOperations(t *testing.T) {
assert.EqualError(t, res.Error(), deploy.PlanPendingOperationsError{}.Error())
}
// Tests that a refresh works for a stack with pending operations.
func TestRefreshWithPendingOperations(t *testing.T) {
p := &TestPlan{}
const resType = "pkgA:m:typA"
urnA := p.NewURN(resType, "resA", "")
newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State {
return &resource.State{
Type: urn.Type(),
URN: urn,
Custom: true,
Delete: delete,
ID: id,
Inputs: resource.PropertyMap{},
Outputs: resource.PropertyMap{},
Dependencies: dependencies,
}
}
old := &deploy.Snapshot{
PendingOperations: []resource.Operation{{
Resource: newResource(urnA, "0", false),
Type: resource.OperationTypeUpdating,
}},
Resources: []*resource.State{
newResource(urnA, "0", false),
},
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
op := TestOp(Update)
options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
project, target := p.GetProject(), p.GetTarget(old)
// Without refreshing, an update should fail.
_, res := op.Run(project, target, options, false, nil, nil)
assertIsErrorOrBailResult(t, res)
assert.EqualError(t, res.Error(), deploy.PlanPendingOperationsError{}.Error())
// With a refresh, the update should succeed.
withRefresh := options
withRefresh.Refresh = true
new, res := op.Run(project, target, withRefresh, false, nil, nil)
assert.Nil(t, res)
assert.Len(t, new.PendingOperations, 0)
// Similarly, the update should succeed if performed after a separate refresh.
new, res = TestOp(Refresh).Run(project, target, options, false, nil, nil)
assert.Nil(t, res)
assert.Len(t, new.PendingOperations, 0)
_, res = op.Run(project, p.GetTarget(new), options, false, nil, nil)
assert.Nil(t, res)
}
// Tests that a failed partial update causes the engine to persist the resource's old inputs and new outputs.
func TestUpdatePartialFailure(t *testing.T) {
loaders := []*deploytest.ProviderLoader{

View file

@ -270,10 +270,6 @@ func buildResourceMap(prev *Snapshot, preview bool) ([]*resource.State, map[reso
return nil, olds, nil
}
if prev.PendingOperations != nil && !preview {
return nil, nil, PlanPendingOperationsError{prev.PendingOperations}
}
for _, oldres := range prev.Resources {
// Ignore resources that are pending deletion; these should not be recorded in the LUT.
if oldres.Delete {

View file

@ -146,6 +146,8 @@ func (ex *deploymentExecutor) Execute(callerCtx context.Context, opts Options, p
if opts.RefreshOnly {
return nil
}
} else if len(ex.deployment.prev.PendingOperations) != 0 && !preview {
return result.FromError(PlanPendingOperationsError{ex.deployment.prev.PendingOperations})
}
// The set of -t targets provided on the command line. 'nil' means 'update everything'.