pulumi/pkg/backend/snapshot_test.go
Pat Gavlin 6e5c4a38d8
Defer all diffs to resource providers. (#2849)
Thse changes make a subtle but critical adjustment to the process the
Pulumi engine uses to determine whether or not a difference exists
between a resource's actual and desired states, and adjusts the way this
difference is calculated and displayed accordingly.

Today, the Pulumi engine get the first chance to decide whether or not
there is a difference between a resource's actual and desired states. It
does this by comparing the current set of inputs for a resource (i.e.
the inputs from the running Pulumi program) with the last set of inputs
used to update the resource. If there is no difference between the old
and new inputs, the engine decides that no change is necessary without
consulting the resource's provider. Only if there are changes does the
engine consult the resource's provider for more information about the
difference. This can be problematic for a number of reasons:

- Not all providers do input-input comparison; some do input-state
  comparison
- Not all providers are able to update the last deployed set of inputs
  when performing a refresh
- Some providers--either intentionally or due to bugs--may see changes
  in resources whose inputs have not changed

All of these situations are confusing at the very least, and the first
is problematic with respect to correctness. Furthermore, the display
code only renders diffs it observes rather than rendering the diffs
observed by the provider, which can obscure the actual changes detected
at runtime.

These changes address both of these issues:
- Rather than comparing the current inputs against the last inputs
  before calling a resource provider's Diff function, the engine calls
  the Diff function in all cases.
- Providers may now return a list of properties that differ between the
  requested and actual state and the way in which they differ. This
  information will then be used by the CLI to render the diff
  appropriately. A provider may also indicate that a particular diff is
  between old and new inputs rather than old state and new inputs.

Fixes #2453.
2019-07-01 12:34:19 -07:00

884 lines
29 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package backend
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/secrets"
"github.com/pulumi/pulumi/pkg/secrets/b64"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/version"
)
type MockRegisterResourceEvent struct {
deploy.SourceEvent
}
func (m MockRegisterResourceEvent) Goal() *resource.Goal { return nil }
func (m MockRegisterResourceEvent) Done(result *deploy.RegisterResult) {}
type MockStackPersister struct {
SavedSnapshots []*deploy.Snapshot
}
func (m *MockStackPersister) Save(snap *deploy.Snapshot) error {
m.SavedSnapshots = append(m.SavedSnapshots, snap)
return nil
}
func (m *MockStackPersister) SecretsManager() secrets.Manager {
return b64.NewBase64SecretsManager()
}
func (m *MockStackPersister) LastSnap() *deploy.Snapshot {
return m.SavedSnapshots[len(m.SavedSnapshots)-1]
}
func MockSetup(t *testing.T, baseSnap *deploy.Snapshot) (*SnapshotManager, *MockStackPersister) {
err := baseSnap.VerifyIntegrity()
if !assert.NoError(t, err) {
t.FailNow()
}
sp := &MockStackPersister{}
return NewSnapshotManager(sp, baseSnap), sp
}
func NewResource(name string, deps ...resource.URN) *resource.State {
return &resource.State{
Type: tokens.Type("test"),
URN: resource.URN(name),
Inputs: make(resource.PropertyMap),
Outputs: make(resource.PropertyMap),
Dependencies: deps,
}
}
func NewSnapshot(resources []*resource.State) *deploy.Snapshot {
return deploy.NewSnapshot(deploy.Manifest{
Time: time.Now(),
Version: version.Version,
Plugins: nil,
}, b64.NewBase64SecretsManager(), resources, nil)
}
func TestIdenticalSames(t *testing.T) {
sameState := NewResource("a-unique-urn")
snap := NewSnapshot([]*resource.State{
sameState,
})
manager, sp := MockSetup(t, snap)
// The engine generates a SameStep on sameState.
engineGeneratedSame := NewResource(string(sameState.URN))
same := deploy.NewSameStep(nil, nil, sameState, engineGeneratedSame)
mutation, err := manager.BeginMutation(same)
assert.NoError(t, err)
// No mutation was made
assert.Empty(t, sp.SavedSnapshots)
err = mutation.End(same, true)
assert.NoError(t, err)
// Identical sames do not cause a snapshot mutation as part of `End`.
assert.Empty(t, sp.SavedSnapshots)
// Close must write the snapshot.
err = manager.Close()
assert.NoError(t, err)
assert.NotEmpty(t, sp.SavedSnapshots)
assert.NotEmpty(t, sp.SavedSnapshots[0].Resources)
// Our same resource should be the first entry in the snapshot list.
inSnapshot := sp.SavedSnapshots[0].Resources[0]
assert.Equal(t, sameState.URN, inSnapshot.URN)
}
// This test challenges the naive approach of mutating resources
// that are the targets of Same steps in-place by changing the dependencies
// of two resources in the snapshot, which is perfectly legal in our system
// (and in fact is done by the `dependency_steps` integration test as well).
//
// The correctness of the `snap` function in snapshot.go is tested here.
func TestSamesWithDependencyChanges(t *testing.T) {
resourceA := NewResource("a-unique-urn-resource-a")
resourceB := NewResource("a-unique-urn-resource-b", resourceA.URN)
// The setup: the snapshot contains two resources, A and B, where
// B depends on A. We're going to begin a mutation in which B no longer
// depends on A and appears first in program order.
snap := NewSnapshot([]*resource.State{
resourceA,
resourceB,
})
manager, sp := MockSetup(t, snap)
resourceBUpdated := NewResource(string(resourceB.URN))
// note: no dependencies
resourceAUpdated := NewResource(string(resourceA.URN), resourceBUpdated.URN)
// note: now depends on B
// The engine first generates a Same for b:
bSame := deploy.NewSameStep(nil, nil, resourceB, resourceBUpdated)
mutation, err := manager.BeginMutation(bSame)
assert.NoError(t, err)
err = mutation.End(bSame, true)
assert.NoError(t, err)
// The snapshot should now look like this:
// snapshot
// resources
// b
// a
// where b does not depend on anything and neither does a.
firstSnap := sp.SavedSnapshots[0]
assert.Len(t, firstSnap.Resources, 2)
assert.Equal(t, resourceB.URN, firstSnap.Resources[0].URN)
assert.Len(t, firstSnap.Resources[0].Dependencies, 0)
assert.Equal(t, resourceA.URN, firstSnap.Resources[1].URN)
assert.Len(t, firstSnap.Resources[1].Dependencies, 0)
// The engine then generates a Same for a:
aSame := deploy.NewSameStep(nil, nil, resourceA, resourceAUpdated)
mutation, err = manager.BeginMutation(aSame)
assert.NoError(t, err)
err = mutation.End(aSame, true)
assert.NoError(t, err)
// The snapshot should now look like this:
// snapshot
// resources
// b
// a
// where b does not depend on anything and a depends on b.
secondSnap := sp.SavedSnapshots[1]
assert.Len(t, secondSnap.Resources, 2)
assert.Equal(t, resourceB.URN, secondSnap.Resources[0].URN)
assert.Len(t, secondSnap.Resources[0].Dependencies, 0)
assert.Equal(t, resourceA.URN, secondSnap.Resources[1].URN)
assert.Len(t, secondSnap.Resources[1].Dependencies, 1)
assert.Equal(t, resourceB.URN, secondSnap.Resources[1].Dependencies[0])
}
// This test exercises same steps with meaningful changes to properties _other_ than `Dependencies` in order to ensure
// that the snapshot is written.
func TestSamesWithOtherMeaningfulChanges(t *testing.T) {
provider := NewResource("urn:pulumi:foo::bar::pulumi:providers:pkgA::provider")
provider.Custom, provider.Type, provider.ID = true, "pulumi:providers:pkgA", "id"
resourceP := NewResource("a-unique-urn-resource-p")
resourceA := NewResource("a-unique-urn-resource-a")
var changes []*resource.State
// Change the "custom" bit.
changes = append(changes, NewResource(string(resourceA.URN)))
changes[0].Custom, changes[0].Provider = true, "urn:pulumi:foo::bar::pulumi:providers:pkgA::provider::id"
// Change the parent.
changes = append(changes, NewResource(string(resourceA.URN)))
changes[1].Parent = resourceP.URN
// Change the "protect" bit.
changes = append(changes, NewResource(string(resourceA.URN)))
changes[2].Protect = !resourceA.Protect
// Change the resource outputs.
changes = append(changes, NewResource(string(resourceA.URN)))
changes[3].Outputs = resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
snap := NewSnapshot([]*resource.State{
provider,
resourceP,
resourceA,
})
for _, c := range changes {
manager, sp := MockSetup(t, snap)
// Generate a same for the provider.
provUpdated := NewResource(string(provider.URN))
provUpdated.Custom, provUpdated.Type = true, provider.Type
provSame := deploy.NewSameStep(nil, nil, provider, provUpdated)
mutation, err := manager.BeginMutation(provSame)
assert.NoError(t, err)
_, _, err = provSame.Apply(false)
assert.NoError(t, err)
err = mutation.End(provSame, true)
assert.NoError(t, err)
assert.Empty(t, sp.SavedSnapshots)
// The engine generates a Same for p. This is not a meaningful change, so the snapshot is not written.
pUpdated := NewResource(string(resourceP.URN))
pSame := deploy.NewSameStep(nil, nil, resourceP, pUpdated)
mutation, err = manager.BeginMutation(pSame)
assert.NoError(t, err)
err = mutation.End(pSame, true)
assert.NoError(t, err)
assert.Empty(t, sp.SavedSnapshots)
// The engine generates a Same for a. Because this is a meaningful change, the snapshot is written:
aSame := deploy.NewSameStep(nil, nil, resourceA, c)
mutation, err = manager.BeginMutation(aSame)
assert.NoError(t, err)
err = mutation.End(aSame, true)
assert.NoError(t, err)
assert.NotEmpty(t, sp.SavedSnapshots)
assert.NotEmpty(t, sp.SavedSnapshots[0].Resources)
inSnapshot := sp.SavedSnapshots[0].Resources[2]
assert.Equal(t, c, inSnapshot)
err = manager.Close()
assert.NoError(t, err)
}
// Set up a second provider and change the resource's provider reference.
provider2 := NewResource("urn:pulumi:foo::bar::pulumi:providers:pkgA::provider2")
provider2.Custom, provider2.Type, provider2.ID = true, "pulumi:providers:pkgA", "id2"
resourceA.Custom, resourceA.ID, resourceA.Provider =
true, "id", "urn:pulumi:foo::bar::pulumi:providers:pkgA::provider::id"
snap = NewSnapshot([]*resource.State{
provider,
provider2,
resourceA,
})
changes = []*resource.State{NewResource(string(resourceA.URN))}
changes[0].Custom, changes[0].Provider = true, "urn:pulumi:foo::bar::pulumi:providers:pkgA::provider2::id2"
for _, c := range changes {
manager, sp := MockSetup(t, snap)
// Generate sames for the providers.
provUpdated := NewResource(string(provider.URN))
provUpdated.Custom, provUpdated.Type = true, provider.Type
provSame := deploy.NewSameStep(nil, nil, provider, provUpdated)
mutation, err := manager.BeginMutation(provSame)
assert.NoError(t, err)
_, _, err = provSame.Apply(false)
assert.NoError(t, err)
err = mutation.End(provSame, true)
assert.NoError(t, err)
assert.Empty(t, sp.SavedSnapshots)
// The engine generates a Same for p. This is not a meaningful change, so the snapshot is not written.
prov2Updated := NewResource(string(provider2.URN))
prov2Updated.Custom, prov2Updated.Type = true, provider.Type
prov2Same := deploy.NewSameStep(nil, nil, provider2, prov2Updated)
mutation, err = manager.BeginMutation(prov2Same)
assert.NoError(t, err)
_, _, err = prov2Same.Apply(false)
assert.NoError(t, err)
err = mutation.End(prov2Same, true)
assert.NoError(t, err)
assert.Empty(t, sp.SavedSnapshots)
// The engine generates a Same for a. Because this is a meaningful change, the snapshot is written:
aSame := deploy.NewSameStep(nil, nil, resourceA, c)
mutation, err = manager.BeginMutation(aSame)
assert.NoError(t, err)
_, _, err = aSame.Apply(false)
assert.NoError(t, err)
err = mutation.End(aSame, true)
assert.NoError(t, err)
assert.NotEmpty(t, sp.SavedSnapshots)
assert.NotEmpty(t, sp.SavedSnapshots[0].Resources)
inSnapshot := sp.SavedSnapshots[0].Resources[2]
assert.Equal(t, c, inSnapshot)
err = manager.Close()
assert.NoError(t, err)
}
}
// This test exercises the merge operation with a particularly vexing deployment
// state that was useful in shaking out bugs.
func TestVexingDeployment(t *testing.T) {
// This is the dependency graph we are going for in the base snapshot:
//
// +-+
// +--> |A|
// | +-+
// | ^
// | +-+
// | |B|
// | +-+
// | ^
// | +-+
// +--+ |C| <---+
// +-+ |
// ^ |
// +-+ |
// |D| |
// +-+ |
// |
// +-+ |
// |E| +---+
// +-+
a := NewResource("a")
b := NewResource("b", a.URN)
c := NewResource("c", a.URN, b.URN)
d := NewResource("d", c.URN)
e := NewResource("e", c.URN)
snap := NewSnapshot([]*resource.State{
a,
b,
c,
d,
e,
})
manager, sp := MockSetup(t, snap)
// This is the sequence of events that come out of the engine:
// B - Same, depends on nothing
// C - CreateReplacement, depends on B
// C - Replace
// D - Update, depends on new C
// This produces the following dependency graph in the new snapshot:
// +-+
// +---> |B|
// | +++
// | ^
// | +++
// | |C| <----+
// | +-+ |
// | |
// | +-+ |
// +---+ |C| +-------------> A (not in graph!)
// +-+ |
// |
// +-+ |
// |D| +---+
// +-+
//
// Conceptually, this is a plan that deletes A. However, we have not yet observed the
// deletion of A, presumably because the engine can't know for sure that it's been deleted
// until the eval source completes. Of note in this snapshot is that the replaced C is still in the graph,
// because it has not yet been deleted, and its dependency A is not in the graph because it
// has not been seen.
//
// Since axiomatically we assume that steps come in in a valid topological order of the dependency graph,
// we can logically assume that A is going to be deleted. (If A were not being deleted, it must have been
// the target of a Step that came before C, which depends on it.)
applyStep := func(step deploy.Step) {
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
err = mutation.End(step, true)
if !assert.NoError(t, err) {
t.FailNow()
}
}
// b now depends on nothing
bPrime := NewResource(string(b.URN))
applyStep(deploy.NewSameStep(nil, MockRegisterResourceEvent{}, b, bPrime))
// c now only depends on b
cPrime := NewResource(string(c.URN), bPrime.URN)
// mocking out the behavior of a provider indicating that this resource needs to be deleted
createReplacement := deploy.NewCreateReplacementStep(nil, MockRegisterResourceEvent{}, c, cPrime, nil, nil, nil, true)
replace := deploy.NewReplaceStep(nil, c, cPrime, nil, nil, nil, true)
c.Delete = true
applyStep(createReplacement)
applyStep(replace)
// cPrime now exists, c is now pending deletion
// dPrime now depends on cPrime, which got replaced
dPrime := NewResource(string(d.URN), cPrime.URN)
applyStep(deploy.NewUpdateStep(nil, MockRegisterResourceEvent{}, d, dPrime, nil, nil, nil))
lastSnap := sp.SavedSnapshots[len(sp.SavedSnapshots)-1]
assert.Len(t, lastSnap.Resources, 6)
res := lastSnap.Resources
// Here's what the merged snapshot should look like:
// B should be first, and it should depend on nothing
assert.Equal(t, b.URN, res[0].URN)
assert.Len(t, res[0].Dependencies, 0)
// cPrime should be next, and it should depend on B
assert.Equal(t, c.URN, res[1].URN)
assert.Len(t, res[1].Dependencies, 1)
assert.Equal(t, b.URN, res[1].Dependencies[0])
// d should be next, and it should depend on cPrime
assert.Equal(t, d.URN, res[2].URN)
assert.Len(t, res[2].Dependencies, 1)
assert.Equal(t, c.URN, res[2].Dependencies[0])
// a should be next, and it should depend on nothing
assert.Equal(t, a.URN, res[3].URN)
assert.Len(t, res[3].Dependencies, 0)
// c should be next, it should depend on A and B and should be pending deletion
// this is a critical operation of snap and the crux of this test:
// merge MUST put c after a in the snapshot, despite never having seen a in the current plan
assert.Equal(t, c.URN, res[4].URN)
assert.True(t, res[4].Delete)
assert.Len(t, res[4].Dependencies, 2)
assert.Contains(t, res[4].Dependencies, a.URN)
assert.Contains(t, res[4].Dependencies, b.URN)
// e should be last, it should depend on C and still be live
assert.Equal(t, e.URN, res[5].URN)
assert.Len(t, res[5].Dependencies, 1)
assert.Equal(t, c.URN, res[5].Dependencies[0])
}
func TestDeletion(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewDeleteStep(nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
err = mutation.End(step, true)
if !assert.NoError(t, err) {
t.FailNow()
}
// the end mutation should mark the resource as "done".
// snap should then not put resourceA in the merged snapshot, since it has been deleted.
lastSnap := sp.SavedSnapshots[len(sp.SavedSnapshots)-1]
assert.Len(t, lastSnap.Resources, 0)
}
func TestFailedDelete(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewDeleteStep(nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// since we marked the mutation as not successful, the snapshot should still contain
// the resource we failed to delete.
lastSnap := sp.SavedSnapshots[len(sp.SavedSnapshots)-1]
assert.Len(t, lastSnap.Resources, 1)
assert.Equal(t, resourceA.URN, lastSnap.Resources[0].URN)
}
func TestRecordingCreateSuccess(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot(nil)
manager, sp := MockSetup(t, snap)
step := deploy.NewCreateStep(nil, &MockRegisterResourceEvent{}, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the create step mutation should have placed a pending "creating" operation
// into the operations list
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeCreating, snap.PendingOperations[0].Type)
err = mutation.End(step, true /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A successful creation should remove the "creating" operation from the operations list
// and persist the created resource in the snapshot.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
}
func TestRecordingCreateFailure(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot(nil)
manager, sp := MockSetup(t, snap)
step := deploy.NewCreateStep(nil, &MockRegisterResourceEvent{}, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the create step mutation should have placed a pending "creating" operation
// into the operations list
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeCreating, snap.PendingOperations[0].Type)
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A failed creation should remove the "creating" operation from the operations list
// and not persist the created resource in the snapshot.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 0)
}
func TestRecordingUpdateSuccess(t *testing.T) {
resourceA := NewResource("a")
resourceA.Inputs["key"] = resource.NewStringProperty("old")
resourceANew := NewResource("a")
resourceANew.Inputs["key"] = resource.NewStringProperty("new")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewUpdateStep(nil, &MockRegisterResourceEvent{}, resourceA, resourceANew, nil, nil, nil)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the update mutation should have placed a pending "updating" operation into
// the operations list, with the resource's new inputs.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeUpdating, snap.PendingOperations[0].Type)
assert.Equal(t, resource.NewStringProperty("new"), snap.PendingOperations[0].Resource.Inputs["key"])
err = mutation.End(step, true /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// Completing the update should place the resource with the new inputs into the snapshot and clear the in
// flight operation.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("new"), snap.Resources[0].Inputs["key"])
}
func TestRecordingUpdateFailure(t *testing.T) {
resourceA := NewResource("a")
resourceA.Inputs["key"] = resource.NewStringProperty("old")
resourceANew := NewResource("a")
resourceANew.Inputs["key"] = resource.NewStringProperty("new")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewUpdateStep(nil, &MockRegisterResourceEvent{}, resourceA, resourceANew, nil, nil, nil)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the update mutation should have placed a pending "updating" operation into
// the operations list, with the resource's new inputs.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeUpdating, snap.PendingOperations[0].Type)
assert.Equal(t, resource.NewStringProperty("new"), snap.PendingOperations[0].Resource.Inputs["key"])
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// Failing the update should keep the old resource with old inputs in the snapshot while clearing the
// in flight operation.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("old"), snap.Resources[0].Inputs["key"])
}
func TestRecordingDeleteSuccess(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewDeleteStep(nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the delete mutation should have placed a pending "deleting" operation into the operations list.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeDeleting, snap.PendingOperations[0].Type)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
err = mutation.End(step, true /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A successful delete should remove the in flight operation and deleted resource from the snapshot.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 0)
}
func TestRecordingDeleteFailure(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewDeleteStep(nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the delete mutation should have placed a pending "deleting" operation into the operations list.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeDeleting, snap.PendingOperations[0].Type)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A failed delete should remove the in flight operation but leave the resource in the snapshot.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
}
func TestRecordingReadSuccessNoPreviousResource(t *testing.T) {
resourceA := NewResource("a")
resourceA.External = true
resourceA.Custom = true
snap := NewSnapshot(nil)
manager, sp := MockSetup(t, snap)
step := deploy.NewReadStep(nil, nil, nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the read mutation should have placed a pending "reading" operation into the operations list.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeReading, snap.PendingOperations[0].Type)
err = mutation.End(step, true /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A successful read should clear the in flight operation and put the new resource into the snapshot
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
}
func TestRecordingReadSuccessPreviousResource(t *testing.T) {
resourceA := NewResource("a")
resourceA.External = true
resourceA.Custom = true
resourceA.Inputs["key"] = resource.NewStringProperty("old")
resourceANew := NewResource("a")
resourceANew.External = true
resourceANew.Custom = true
resourceANew.Inputs["key"] = resource.NewStringProperty("new")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewReadStep(nil, nil, resourceA, resourceANew)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the read mutation should have placed a pending "reading" operation into the operations list
// with the inputs of the new read
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeReading, snap.PendingOperations[0].Type)
assert.Equal(t, resource.NewStringProperty("new"), snap.PendingOperations[0].Resource.Inputs["key"])
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("old"), snap.Resources[0].Inputs["key"])
err = mutation.End(step, true /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A successful read should clear the in flight operation and replace the existing resource in the snapshot.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("new"), snap.Resources[0].Inputs["key"])
}
func TestRecordingReadFailureNoPreviousResource(t *testing.T) {
resourceA := NewResource("a")
resourceA.External = true
resourceA.Custom = true
snap := NewSnapshot(nil)
manager, sp := MockSetup(t, snap)
step := deploy.NewReadStep(nil, nil, nil, resourceA)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the read mutation should have placed a pending "reading" operation into the operations list.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeReading, snap.PendingOperations[0].Type)
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A failed read should clear the in flight operation and leave the snapshot empty.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 0)
assert.Len(t, snap.PendingOperations, 0)
}
func TestRecordingReadFailurePreviousResource(t *testing.T) {
resourceA := NewResource("a")
resourceA.External = true
resourceA.Custom = true
resourceA.Inputs["key"] = resource.NewStringProperty("old")
resourceANew := NewResource("a")
resourceANew.External = true
resourceANew.Custom = true
resourceANew.Inputs["key"] = resource.NewStringProperty("new")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
step := deploy.NewReadStep(nil, nil, resourceA, resourceANew)
mutation, err := manager.BeginMutation(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// Beginning the read mutation should have placed a pending "reading" operation into the operations list
// with the inputs of the new read
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 1)
assert.Equal(t, resourceA.URN, snap.PendingOperations[0].Resource.URN)
assert.Equal(t, resource.OperationTypeReading, snap.PendingOperations[0].Type)
assert.Equal(t, resource.NewStringProperty("new"), snap.PendingOperations[0].Resource.Inputs["key"])
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("old"), snap.Resources[0].Inputs["key"])
err = mutation.End(step, false /* successful */)
if !assert.NoError(t, err) {
t.FailNow()
}
// A failed read should clear the in flight operation and leave the existing read in the snapshot with the
// old inputs.
snap = sp.LastSnap()
assert.Len(t, snap.Resources, 1)
assert.Len(t, snap.PendingOperations, 0)
assert.Equal(t, resourceA.URN, snap.Resources[0].URN)
assert.Equal(t, resource.NewStringProperty("old"), snap.Resources[0].Inputs["key"])
}
func TestRegisterOutputs(t *testing.T) {
resourceA := NewResource("a")
snap := NewSnapshot([]*resource.State{
resourceA,
})
manager, sp := MockSetup(t, snap)
// There should be zero snaps performed at the start.
assert.Len(t, sp.SavedSnapshots, 0)
// The step here is not important.
step := deploy.NewSameStep(nil, nil, resourceA, resourceA)
err := manager.RegisterResourceOutputs(step)
if !assert.NoError(t, err) {
t.FailNow()
}
// The RegisterResourceOutputs should have caused a snapshot to be written.
assert.Len(t, sp.SavedSnapshots, 1)
// It should be identical to what has already been written.
lastSnap := sp.LastSnap()
assert.Len(t, lastSnap.Resources, 1)
assert.Equal(t, resourceA.URN, lastSnap.Resources[0].URN)
}