Parameterize TestReplaceOnChanges with engine diff

This commit also adds DetailedDiff to the engine diff.
This commit is contained in:
Ian Wahbe 2021-11-24 13:55:05 -08:00
parent eafc611b43
commit 702e4ba6f5
4 changed files with 179 additions and 139 deletions

View file

@ -944,156 +944,147 @@ func TestSingleResourceIgnoreChanges(t *testing.T) {
}, []string{"a", "b"}, []deploy.StepOp{deploy.OpUpdate})
}
func objectDiffToDetailedDiff(prefix string, d *resource.ObjectDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
for k, vd := range d.Updates {
var nestedPrefix string
if prefix == "" {
nestedPrefix = string(k)
} else {
nestedPrefix = fmt.Sprintf("%s.%s", prefix, string(k))
}
for kk, pd := range valueDiffToDetailedDiff(nestedPrefix, vd) {
ret[kk] = pd
}
}
return ret
}
type DiffFunc = func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error)
func arrayDiffToDetailedDiff(prefix string, d *resource.ArrayDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
for i, vd := range d.Updates {
for kk, pd := range valueDiffToDetailedDiff(fmt.Sprintf("%s[%d]", prefix, i), vd) {
ret[kk] = pd
func replaceOnChangesTest(t *testing.T, name string, diffFunc DiffFunc) {
t.Run(name, func(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: diffFunc,
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
},
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return resource.ID("id123"), inputs, resource.StatusOK, nil
},
}, nil
}),
}
}
return ret
}
func valueDiffToDetailedDiff(prefix string, vd resource.ValueDiff) map[string]plugin.PropertyDiff {
ret := map[string]plugin.PropertyDiff{}
if vd.Object != nil {
for kk, pd := range objectDiffToDetailedDiff(prefix, vd.Object) {
ret[kk] = pd
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string,
allowedOps []deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: props,
ReplaceOnChanges: replaceOnChanges,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
} else if vd.Array != nil {
for kk, pd := range arrayDiffToDetailedDiff(prefix, vd.Array) {
ret[kk] = pd
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpCreate})
// Ensure that a change to a replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to a nested replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "bar",
},
}), []string{"b.c"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to any property of a "*" replaceOnChange results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 3,
"b": map[string]interface{}{
"c": "baz",
},
}), []string{"*"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to an non-replaceOnChange property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 4,
"b": map[string]interface{}{
"c": "qux",
},
}), nil, []deploy.StepOp{deploy.OpUpdate})
// We ensure that we are listing to the engine diff function only when the provider function
// is nil. We do this by adding some weirdness to the provider diff function.
allowed := []deploy.StepOp{deploy.OpCreateReplacement, deploy.OpReplace, deploy.OpDeleteReplaced}
if diffFunc != nil {
allowed = []deploy.StepOp{deploy.OpSame}
}
} else {
ret[prefix] = plugin.PropertyDiff{Kind: plugin.DiffUpdate}
}
return ret
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 42, // 42 is a special value in the "provider" diff function.
"b": map[string]interface{}{
"c": "qux",
},
}), []string{"a"}, allowed)
_ = snap
})
}
func TestReplaceOnChanges(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
diff := olds.Diff(news)
if diff == nil {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
detailedDiff := objectDiffToDetailedDiff("", diff)
var changedKeys []resource.PropertyKey
for _, k := range diff.Keys() {
if diff.Changed(k) {
changedKeys = append(changedKeys, k)
}
}
return plugin.DiffResult{
Changes: plugin.DiffSome,
ChangedKeys: changedKeys,
DetailedDiff: detailedDiff,
}, 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
},
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return resource.ID("id123"), inputs, resource.StatusOK, nil
},
// We simulate a provider that has it's own diff function.
replaceOnChangesTest(t, "provider diff",
func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
// To establish a observable difference between the provider and engine diff function,
// we treat 42 as an OpSame. We use this to check that the right diff function is being
// used.
for k, v := range news {
if v == resource.NewNumberProperty(42) {
news[k] = olds[k]
}
}
diff := olds.Diff(news)
if diff == nil {
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
}
detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff)
changedKeys := diff.ChangedKeys()
return plugin.DiffResult{
Changes: plugin.DiffSome,
ChangedKeys: changedKeys,
DetailedDiff: detailedDiff,
}, nil
}),
}
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string,
allowedOps []deploy.StepOp) *deploy.Snapshot {
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
Inputs: props,
ReplaceOnChanges: replaceOnChanges,
})
assert.NoError(t, err)
return nil
})
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host},
Steps: []TestStep{
{
Op: Update,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
events []Event, res result.Result) result.Result {
for _, event := range events {
if event.Type == ResourcePreEvent {
payload := event.Payload().(ResourcePreEventPayload)
assert.Subset(t, allowedOps, []deploy.StepOp{payload.Metadata.Op})
}
}
return res
},
},
},
}
return p.Run(t, snap)
}
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 1,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpCreate})
// Ensure that a change to a replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "foo",
},
}), []string{"a"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to a nested replaceOnChange property results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 2,
"b": map[string]interface{}{
"c": "bar",
},
}), []string{"b.c"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to any property of a "*" replaceOnChange results in an OpReplace
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 3,
"b": map[string]interface{}{
"c": "baz",
},
}), []string{"*"}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
// Ensure that a change to an non-replaceOnChange property results in an OpUpdate
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
"a": 4,
"b": map[string]interface{}{
"c": "qux",
},
}), nil, []deploy.StepOp{deploy.OpUpdate})
_ = snap
// We simulate a provider that does not have it's own diff function. This tests the engines diff
// function instead.
replaceOnChangesTest(t, "engine diff", nil)
}
// Resource is an abstract representation of a resource graph

View file

@ -1140,6 +1140,7 @@ func diffResource(urn resource.URN, id resource.ID, oldInputs, oldOutputs,
if tmp.AnyChanges() {
diff.Changes = plugin.DiffSome
diff.ChangedKeys = tmp.ChangedKeys()
diff.DetailedDiff = plugin.NewDetailedDiffFromObjectDiff(tmp)
} else {
diff.Changes = plugin.DiffNone
}

View file

@ -210,7 +210,7 @@ func TestApplyReplaceOnChangesEmptyDetailedDiff(t *testing.T) {
}
func TestEngineDiffResource(t *testing.T) {
func TestEngineDiff(t *testing.T) {
cases := []struct {
name string
oldInputs, newInputs resource.PropertyMap

View file

@ -16,6 +16,7 @@ package plugin
import (
"errors"
"fmt"
"io"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
@ -217,6 +218,53 @@ type DiffResult struct {
DeleteBeforeReplace bool // if true, this resource must be deleted before recreating it.
}
// Computes the detailed diff of Updated keys.
func NewDetailedDiffFromObjectDiff(diff *resource.ObjectDiff) map[string]PropertyDiff {
return objectDiffToDetailsDiff("", diff)
}
func objectDiffToDetailsDiff(prefix string, diff *resource.ObjectDiff) map[string]PropertyDiff {
ret := map[string]PropertyDiff{}
for k, vd := range diff.Updates {
var nestedPrefix string
if prefix == "" {
nestedPrefix = string(k)
} else {
nestedPrefix = fmt.Sprintf("%s.%s", prefix, string(k))
}
for kk, pd := range valueDiffToDetailedDiff(nestedPrefix, vd) {
ret[kk] = pd
}
}
return ret
}
func arrayDiffToDetailedDiff(prefix string, d *resource.ArrayDiff) map[string]PropertyDiff {
ret := map[string]PropertyDiff{}
for i, vd := range d.Updates {
for kk, pd := range valueDiffToDetailedDiff(fmt.Sprintf("%s[%d]", prefix, i), vd) {
ret[kk] = pd
}
}
return ret
}
func valueDiffToDetailedDiff(prefix string, vd resource.ValueDiff) map[string]PropertyDiff {
ret := map[string]PropertyDiff{}
if vd.Object != nil {
for kk, pd := range objectDiffToDetailsDiff(prefix, vd.Object) {
ret[kk] = pd
}
} else if vd.Array != nil {
for kk, pd := range arrayDiffToDetailedDiff(prefix, vd.Array) {
ret[kk] = pd
}
} else {
ret[prefix] = PropertyDiff{Kind: DiffUpdate}
}
return ret
}
// Replace returns true if this diff represents a replacement.
func (r DiffResult) Replace() bool {
for _, v := range r.DetailedDiff {