2020-12-15 22:24:46 +00:00
|
|
|
package lifecycletest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"strconv"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/blang/semver"
|
|
|
|
combinations "github.com/mxschmitt/golang-combinations"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
2023-11-21 15:16:13 +00:00
|
|
|
. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
2020-12-15 22:24:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestParallelRefresh(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a program that registers four resources, each of which depends on the resource that immediately precedes
|
|
|
|
// it.
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
resA, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
resB, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
|
|
|
|
Dependencies: []resource.URN{resA},
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
resC, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
|
|
|
|
Dependencies: []resource.URN{resB},
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resD", true, deploytest.ResourceOptions{
|
|
|
|
Dependencies: []resource.URN{resC},
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
p := &TestPlan{
|
2023-09-28 21:50:18 +00:00
|
|
|
Options: TestUpdateOptions{HostF: hostF, UpdateOptions: UpdateOptions{Parallel: 4}},
|
2020-12-15 22:24:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Update}}
|
|
|
|
snap := p.Run(t, nil)
|
|
|
|
|
|
|
|
assert.Len(t, snap.Resources, 5)
|
2023-11-20 08:59:00 +00:00
|
|
|
assert.Equal(t, snap.Resources[0].URN.Name(), "default") // provider
|
|
|
|
assert.Equal(t, snap.Resources[1].URN.Name(), "resA")
|
|
|
|
assert.Equal(t, snap.Resources[2].URN.Name(), "resB")
|
|
|
|
assert.Equal(t, snap.Resources[3].URN.Name(), "resC")
|
|
|
|
assert.Equal(t, snap.Resources[4].URN.Name(), "resD")
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Refresh}}
|
|
|
|
snap = p.Run(t, snap)
|
|
|
|
|
|
|
|
assert.Len(t, snap.Resources, 5)
|
2023-11-20 08:59:00 +00:00
|
|
|
assert.Equal(t, snap.Resources[0].URN.Name(), "default") // provider
|
|
|
|
assert.Equal(t, snap.Resources[1].URN.Name(), "resA")
|
|
|
|
assert.Equal(t, snap.Resources[2].URN.Name(), "resB")
|
|
|
|
assert.Equal(t, snap.Resources[3].URN.Name(), "resC")
|
|
|
|
assert.Equal(t, snap.Resources[4].URN.Name(), "resD")
|
2020-12-15 22:24:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestExternalRefresh(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Our program reads a resource and exits.
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
[engine] Add support for source positions
These changes add support for passing source position information in
gRPC metadata and recording the source position that corresponds to a
resource registration in the statefile.
Enabling source position information in the resource model can provide
substantial benefits, including but not limited to:
- Better errors from the Pulumi CLI
- Go-to-defintion for resources in state
- Editor integration for errors, etc. from `pulumi preview`
Source positions are (file, line) or (file, line, column) tuples
represented as URIs. The line and column are stored in the fragment
portion of the URI as "line(,column)?". The scheme of the URI and the
form of its path component depends on the context in which it is
generated or used:
- During an active update, the URI's scheme is `file` and paths are
absolute filesystem paths. This allows consumers to easily access
arbitrary files that are available on the host.
- In a statefile, the URI's scheme is `project` and paths are relative
to the project root. This allows consumers to resolve source positions
relative to the project file in different contexts irrespective of the
location of the project itself (e.g. given a project-relative path and
the URL of the project's root on GitHub, one can build a GitHub URL for
the source position).
During an update, source position information may be attached to gRPC
calls as "source-position" metadata. This allows arbitrary calls to be
associated with source positions without changes to their protobuf
payloads. Modifying the protobuf payloads is also a viable approach, but
is somewhat more invasive than attaching metadata, and requires changes
to every call signature.
Source positions should reflect the position in user code that initiated
a resource model operation (e.g. the source position passed with
`RegisterResource` for `pet` in the example above should be the source
position in `index.ts`, _not_ the source position in the Pulumi SDK). In
general, the Pulumi SDK should be able to infer the source position of
the resource registration, as the relationship between a resource
registration and its corresponding user code should be static per SDK.
Source positions in state files will be stored as a new `registeredAt`
property on each resource. This property is optional.
2023-06-29 18:41:19 +00:00
|
|
|
_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "", "")
|
2020-12-15 22:24:46 +00:00
|
|
|
if !assert.NoError(t, err) {
|
|
|
|
t.FailNow()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
p := &TestPlan{
|
2023-09-28 21:50:18 +00:00
|
|
|
Options: TestUpdateOptions{HostF: hostF},
|
2020-12-15 22:24:46 +00:00
|
|
|
Steps: []TestStep{{Op: Update}},
|
|
|
|
}
|
|
|
|
|
|
|
|
// The read should place "resA" in the snapshot with the "External" bit set.
|
|
|
|
snap := p.Run(t, nil)
|
|
|
|
assert.Len(t, snap.Resources, 2)
|
2023-11-20 08:59:00 +00:00
|
|
|
assert.Equal(t, snap.Resources[0].URN.Name(), "default") // provider
|
|
|
|
assert.Equal(t, snap.Resources[1].URN.Name(), "resA")
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.True(t, snap.Resources[1].External)
|
|
|
|
|
|
|
|
p = &TestPlan{
|
2023-09-28 21:50:18 +00:00
|
|
|
Options: TestUpdateOptions{HostF: hostF},
|
2020-12-15 22:24:46 +00:00
|
|
|
Steps: []TestStep{{Op: Refresh}},
|
|
|
|
}
|
|
|
|
|
|
|
|
snap = p.Run(t, snap)
|
|
|
|
// A refresh should leave "resA" as it is in the snapshot. The External bit should still be set.
|
|
|
|
assert.Len(t, snap.Resources, 2)
|
2023-11-20 08:59:00 +00:00
|
|
|
assert.Equal(t, snap.Resources[0].URN.Name(), "default") // provider
|
|
|
|
assert.Equal(t, snap.Resources[1].URN.Name(), "resA")
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.True(t, snap.Resources[1].External)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRefreshInitFailure(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
res2URN := p.NewURN("pkgA:m:typA", "resB", "")
|
|
|
|
|
|
|
|
res2Outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Refresh will persist any initialization errors that are returned by `Read`. This provider
|
|
|
|
// will error out or not based on the value of `refreshShouldFail`.
|
|
|
|
//
|
|
|
|
refreshShouldFail := false
|
|
|
|
|
|
|
|
//
|
|
|
|
// Set up test environment to use `readFailProvider` as the underlying resource provider.
|
|
|
|
//
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(
|
|
|
|
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
|
|
|
if refreshShouldFail && urn == resURN {
|
|
|
|
err := &plugin.InitError{
|
|
|
|
Reasons: []string{"Refresh reports continued to fail to initialize"},
|
|
|
|
}
|
|
|
|
return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusPartialFailure, err
|
|
|
|
} else if urn == res2URN {
|
|
|
|
return plugin.ReadResult{Outputs: res2Outputs}, resource.StatusOK, nil
|
|
|
|
}
|
|
|
|
return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
|
|
})
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
p.Options.HostF = hostF
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Create an old snapshot with a single initialization failure.
|
|
|
|
//
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: []*resource.State{
|
|
|
|
{
|
|
|
|
Type: resURN.Type(),
|
|
|
|
URN: resURN,
|
|
|
|
Custom: true,
|
|
|
|
ID: "0",
|
|
|
|
Inputs: resource.PropertyMap{},
|
|
|
|
Outputs: resource.PropertyMap{},
|
|
|
|
InitErrors: []string{"Resource failed to initialize"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Type: res2URN.Type(),
|
|
|
|
URN: res2URN,
|
|
|
|
Custom: true,
|
|
|
|
ID: "1",
|
|
|
|
Inputs: resource.PropertyMap{},
|
|
|
|
Outputs: resource.PropertyMap{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Refresh DOES NOT fail, causing the initialization error to disappear.
|
|
|
|
//
|
|
|
|
p.Steps = []TestStep{{Op: Refresh}}
|
|
|
|
snap := p.Run(t, old)
|
|
|
|
|
|
|
|
for _, resource := range snap.Resources {
|
|
|
|
switch urn := resource.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
// break
|
|
|
|
case resURN:
|
|
|
|
assert.Empty(t, resource.InitErrors)
|
|
|
|
case res2URN:
|
|
|
|
assert.Equal(t, res2Outputs, resource.Outputs)
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Refresh again, see the resource is in a partial state of failure, but the refresh operation
|
|
|
|
// DOES NOT fail. The initialization error is still persisted.
|
|
|
|
//
|
|
|
|
refreshShouldFail = true
|
|
|
|
p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
|
|
|
|
snap = p.Run(t, old)
|
|
|
|
for _, resource := range snap.Resources {
|
|
|
|
switch urn := resource.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
// break
|
|
|
|
case resURN:
|
|
|
|
assert.Equal(t, []string{"Refresh reports continued to fail to initialize"}, resource.InitErrors)
|
|
|
|
case res2URN:
|
|
|
|
assert.Equal(t, res2Outputs, resource.Outputs)
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that tests that Refresh can detect that resources have been deleted and removes them
|
|
|
|
// from the snapshot.
|
|
|
|
func TestRefreshWithDelete(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
|
2020-12-15 22:24:46 +00:00
|
|
|
for _, parallelFactor := range []int{1, 4} {
|
2022-03-04 08:17:41 +00:00
|
|
|
parallelFactor := parallelFactor
|
2020-12-15 22:24:46 +00:00
|
|
|
t.Run(fmt.Sprintf("parallel-%d", parallelFactor), func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(
|
|
|
|
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
|
|
|
// This thing doesn't exist. Returning nil from Read should trigger
|
|
|
|
// the engine to delete it from the snapshot.
|
|
|
|
return plugin.ReadResult{}, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := &TestPlan{Options: TestUpdateOptions{HostF: hostF, UpdateOptions: UpdateOptions{Parallel: parallelFactor}}}
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Update}}
|
|
|
|
snap := p.Run(t, nil)
|
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Refresh}}
|
|
|
|
snap = p.Run(t, snap)
|
|
|
|
|
|
|
|
// Refresh succeeds and records that the resource in the snapshot doesn't exist anymore
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
assert.Len(t, snap.Resources, 1)
|
|
|
|
assert.Equal(t, provURN, snap.Resources[0].URN)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tests that dependencies are correctly rewritten when refresh removes deleted resources.
|
|
|
|
func TestRefreshDeleteDependencies(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
names := []string{"resA", "resB", "resC"}
|
|
|
|
|
|
|
|
// Try refreshing a stack with every combination of the three above resources as a target to
|
|
|
|
// refresh.
|
|
|
|
subsets := combinations.All(names)
|
|
|
|
|
|
|
|
// combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no
|
|
|
|
// targets specified)
|
|
|
|
validateRefreshDeleteCombination(t, names, []string{})
|
|
|
|
|
|
|
|
for _, subset := range subsets {
|
|
|
|
validateRefreshDeleteCombination(t, names, subset)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-09 09:09:48 +00:00
|
|
|
// Looks up the provider ID in newResources and sets "Provider" to reference that in every resource in oldResources.
|
|
|
|
func setProviderRef(t *testing.T, oldResources, newResources []*resource.State, provURN resource.URN) {
|
|
|
|
for _, r := range newResources {
|
|
|
|
if r.URN == provURN {
|
|
|
|
provRef, err := providers.NewReference(r.URN, r.ID)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
for i := range oldResources {
|
|
|
|
oldResources[i].Provider = provRef.String()
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
func validateRefreshDeleteCombination(t *testing.T, names []string, targets []string) {
|
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
const resType = "pkgA:m:typA"
|
|
|
|
|
|
|
|
urnA := p.NewURN(resType, names[0], "")
|
|
|
|
urnB := p.NewURN(resType, names[1], "")
|
|
|
|
urnC := p.NewURN(resType, names[2], "")
|
|
|
|
urns := []resource.URN{urnA, urnB, urnC}
|
|
|
|
|
|
|
|
refreshTargets := []resource.URN{}
|
|
|
|
|
|
|
|
t.Logf("Refreshing targets: %v", targets)
|
|
|
|
for _, target := range targets {
|
|
|
|
refreshTargets = append(refreshTargets, pickURN(t, urns, names, target))
|
|
|
|
}
|
|
|
|
|
2023-05-23 20:17:59 +00:00
|
|
|
p.Options.Targets = deploy.NewUrnTargetsFromUrns(refreshTargets)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
oldResources := []*resource.State{
|
|
|
|
newResource(urnA, "0", false),
|
|
|
|
newResource(urnB, "1", false, urnA),
|
|
|
|
newResource(urnC, "2", false, urnA, urnB),
|
|
|
|
newResource(urnA, "3", true),
|
|
|
|
newResource(urnA, "4", true),
|
|
|
|
newResource(urnC, "5", true, urnA, urnB),
|
|
|
|
}
|
|
|
|
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: oldResources,
|
|
|
|
}
|
|
|
|
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(urn resource.URN, id resource.ID,
|
2023-03-03 16:36:39 +00:00
|
|
|
inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
2020-12-15 22:24:46 +00:00
|
|
|
switch id {
|
|
|
|
case "0", "4":
|
|
|
|
// We want to delete resources A::0 and A::4.
|
|
|
|
return plugin.ReadResult{}, resource.StatusOK, nil
|
|
|
|
default:
|
|
|
|
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, nil, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
p.Steps = []TestStep{
|
|
|
|
{
|
|
|
|
Op: Refresh,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
2023-10-11 14:44:09 +00:00
|
|
|
_ []Event, err error,
|
|
|
|
) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
// Should see only refreshes.
|
|
|
|
for _, entry := range entries {
|
|
|
|
if len(refreshTargets) > 0 {
|
|
|
|
// should only see changes to urns we explicitly asked to change
|
|
|
|
assert.Containsf(t, refreshTargets, entry.Step.URN(),
|
|
|
|
"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
|
|
|
|
}
|
|
|
|
|
2023-10-11 14:44:09 +00:00
|
|
|
return err
|
2020-12-15 22:24:46 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
snap := p.Run(t, old)
|
|
|
|
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
|
2021-12-09 09:09:48 +00:00
|
|
|
// The new resources will have had their default provider urn filled in. We fill this in on
|
|
|
|
// the old resources here as well so that the equal checks below pass
|
|
|
|
setProviderRef(t, oldResources, snap.Resources, provURN)
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
for _, r := range snap.Resources {
|
|
|
|
switch urn := r.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
continue
|
|
|
|
case urnA, urnB, urnC:
|
|
|
|
// break
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(refreshTargets) == 0 || containsURN(refreshTargets, urnA) {
|
|
|
|
// 'A' was deleted, so we should see the impact downstream.
|
|
|
|
|
|
|
|
switch r.ID {
|
|
|
|
case "1":
|
|
|
|
// A::0 was deleted, so B's dependency list should be empty.
|
|
|
|
assert.Equal(t, urnB, r.URN)
|
|
|
|
assert.Empty(t, r.Dependencies)
|
|
|
|
case "2":
|
|
|
|
// A::0 was deleted, so C's dependency list should only contain B.
|
|
|
|
assert.Equal(t, urnC, r.URN)
|
|
|
|
assert.Equal(t, []resource.URN{urnB}, r.Dependencies)
|
|
|
|
case "3":
|
|
|
|
// A::3 should not have changed.
|
|
|
|
assert.Equal(t, oldResources[3], r)
|
|
|
|
case "5":
|
|
|
|
// A::4 was deleted but A::3 was still refernceable by C, so C should not have changed.
|
|
|
|
assert.Equal(t, oldResources[5], r)
|
|
|
|
default:
|
|
|
|
t.Fatalf("Unexpected changed resource when refreshing %v: %v::%v", refreshTargets, r.URN, r.ID)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// A was not deleted. So nothing should be impacted.
|
|
|
|
id, err := strconv.Atoi(r.ID.String())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, oldResources[id], r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func containsURN(urns []resource.URN, urn resource.URN) bool {
|
|
|
|
for _, val := range urns {
|
|
|
|
if val == urn {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tests basic refresh functionality.
|
|
|
|
func TestRefreshBasics(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
names := []string{"resA", "resB", "resC"}
|
|
|
|
|
|
|
|
// Try refreshing a stack with every combination of the three above resources as a target to
|
|
|
|
// refresh.
|
|
|
|
subsets := combinations.All(names)
|
|
|
|
|
|
|
|
// combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no
|
|
|
|
// targets specified)
|
2023-03-03 16:36:39 +00:00
|
|
|
// validateRefreshBasicsCombination(t, names, []string{})
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
for _, subset := range subsets {
|
|
|
|
validateRefreshBasicsCombination(t, names, subset)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateRefreshBasicsCombination(t *testing.T, names []string, targets []string) {
|
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
const resType = "pkgA:m:typA"
|
|
|
|
|
|
|
|
urnA := p.NewURN(resType, names[0], "")
|
|
|
|
urnB := p.NewURN(resType, names[1], "")
|
|
|
|
urnC := p.NewURN(resType, names[2], "")
|
|
|
|
urns := []resource.URN{urnA, urnB, urnC}
|
|
|
|
|
|
|
|
refreshTargets := []resource.URN{}
|
|
|
|
|
|
|
|
for _, target := range targets {
|
2023-05-23 20:17:59 +00:00
|
|
|
refreshTargets = append(p.Options.Targets.Literals(), pickURN(t, urns, names, target))
|
2020-12-15 22:24:46 +00:00
|
|
|
}
|
|
|
|
|
2023-05-23 20:17:59 +00:00
|
|
|
p.Options.Targets = deploy.NewUrnTargetsFromUrns(refreshTargets)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
oldResources := []*resource.State{
|
|
|
|
newResource(urnA, "0", false),
|
|
|
|
newResource(urnB, "1", false, urnA),
|
|
|
|
newResource(urnC, "2", false, urnA, urnB),
|
|
|
|
newResource(urnA, "3", true),
|
|
|
|
newResource(urnA, "4", true),
|
|
|
|
newResource(urnC, "5", true, urnA, urnB),
|
|
|
|
}
|
|
|
|
|
|
|
|
newStates := map[resource.ID]plugin.ReadResult{
|
|
|
|
// A::0 and A::3 will have no changes.
|
|
|
|
"0": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
|
|
|
|
"3": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}},
|
|
|
|
|
|
|
|
// B::1 and A::4 will have changes. The latter will also have input changes.
|
|
|
|
"1": {Outputs: resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, Inputs: resource.PropertyMap{}},
|
|
|
|
"4": {
|
|
|
|
Outputs: resource.PropertyMap{"baz": resource.NewStringProperty("qux")},
|
|
|
|
Inputs: resource.PropertyMap{"oof": resource.NewStringProperty("zab")},
|
|
|
|
},
|
|
|
|
|
|
|
|
// C::2 and C::5 will be deleted.
|
|
|
|
"2": {},
|
|
|
|
"5": {},
|
|
|
|
}
|
|
|
|
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: oldResources,
|
|
|
|
}
|
|
|
|
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(urn resource.URN, id resource.ID,
|
2023-03-03 16:36:39 +00:00
|
|
|
inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
2020-12-15 22:24:46 +00:00
|
|
|
new, hasNewState := newStates[id]
|
|
|
|
assert.True(t, hasNewState)
|
|
|
|
return new, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, nil, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
p.Steps = []TestStep{{
|
|
|
|
Op: Refresh,
|
|
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
2023-10-11 14:44:09 +00:00
|
|
|
_ []Event, err error,
|
|
|
|
) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
// Should see only refreshes.
|
|
|
|
for _, entry := range entries {
|
|
|
|
if len(refreshTargets) > 0 {
|
|
|
|
// should only see changes to urns we explicitly asked to change
|
|
|
|
assert.Containsf(t, refreshTargets, entry.Step.URN(),
|
|
|
|
"Refreshed a resource that wasn't a target: %v", entry.Step.URN())
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
|
|
|
|
resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
|
|
|
|
|
|
|
|
old := entry.Step.Old()
|
|
|
|
if !old.Custom || providers.IsProviderType(old.Type) {
|
|
|
|
// Component and provider resources should never change.
|
|
|
|
assert.Equal(t, deploy.OpSame, resultOp)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
expected, new := newStates[old.ID], entry.Step.New()
|
|
|
|
if expected.Outputs == nil {
|
|
|
|
// If the resource was deleted, we want the result op to be an OpDelete.
|
|
|
|
assert.Nil(t, new)
|
|
|
|
assert.Equal(t, deploy.OpDelete, resultOp)
|
|
|
|
} else {
|
|
|
|
// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
|
|
|
|
// an OpSame.
|
|
|
|
if reflect.DeepEqual(old.Outputs, expected.Outputs) {
|
|
|
|
assert.Equal(t, deploy.OpSame, resultOp)
|
|
|
|
} else {
|
|
|
|
assert.Equal(t, deploy.OpUpdate, resultOp)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only the inputs and outputs should have changed (if anything changed).
|
|
|
|
old.Inputs = expected.Inputs
|
|
|
|
old.Outputs = expected.Outputs
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
|
|
|
|
// Discard timestamps for refresh test.
|
|
|
|
new.Modified = nil
|
|
|
|
old.Modified = nil
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.Equal(t, old, new)
|
|
|
|
}
|
|
|
|
}
|
2023-10-11 14:44:09 +00:00
|
|
|
return err
|
2020-12-15 22:24:46 +00:00
|
|
|
},
|
|
|
|
}}
|
|
|
|
snap := p.Run(t, old)
|
|
|
|
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
|
2021-12-09 09:09:48 +00:00
|
|
|
// The new resources will have had their default provider urn filled in. We fill this in on
|
|
|
|
// the old resources here as well so that the equal checks below pass
|
|
|
|
setProviderRef(t, oldResources, snap.Resources, provURN)
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
for _, r := range snap.Resources {
|
|
|
|
switch urn := r.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
continue
|
|
|
|
case urnA, urnB, urnC:
|
|
|
|
// break
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The only resources left in the checkpoint should be those that were not deleted by the refresh.
|
|
|
|
expected := newStates[r.ID]
|
|
|
|
assert.NotNil(t, expected)
|
|
|
|
|
|
|
|
idx, err := strconv.ParseInt(string(r.ID), 0, 0)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
2021-12-09 09:09:48 +00:00
|
|
|
targetedForRefresh := false
|
|
|
|
for _, targetUrn := range refreshTargets {
|
|
|
|
if targetUrn == r.URN {
|
|
|
|
targetedForRefresh = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If targeted for refresh the new resources should be equal to the old resources + the new inputs and outputs
|
2020-12-15 22:24:46 +00:00
|
|
|
old := oldResources[int(idx)]
|
2021-12-09 09:09:48 +00:00
|
|
|
if targetedForRefresh {
|
|
|
|
old.Inputs = expected.Inputs
|
|
|
|
old.Outputs = expected.Outputs
|
|
|
|
}
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.Equal(t, old, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tests that an interrupted refresh leaves behind an expected state.
|
|
|
|
func TestCanceledRefresh(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
const resType = "pkgA:m:typA"
|
|
|
|
|
|
|
|
urnA := p.NewURN(resType, "resA", "")
|
|
|
|
urnB := p.NewURN(resType, "resB", "")
|
|
|
|
urnC := p.NewURN(resType, "resC", "")
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
oldResources := []*resource.State{
|
|
|
|
newResource(urnA, "0", false),
|
|
|
|
newResource(urnB, "1", false),
|
|
|
|
newResource(urnC, "2", false),
|
|
|
|
}
|
|
|
|
|
|
|
|
newStates := map[resource.ID]resource.PropertyMap{
|
|
|
|
// A::0 and B::1 will have changes; D::3 will be deleted.
|
|
|
|
"0": {"foo": resource.NewStringProperty("bar")},
|
|
|
|
"1": {"baz": resource.NewStringProperty("qux")},
|
|
|
|
"2": nil,
|
|
|
|
}
|
|
|
|
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: oldResources,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set up a cancelable context for the refresh operation.
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
|
|
|
// Serialize all refreshes s.t. we can cancel after the first is issued.
|
|
|
|
refreshes, cancelled := make(chan resource.ID), make(chan bool)
|
|
|
|
go func() {
|
|
|
|
<-refreshes
|
|
|
|
cancel()
|
|
|
|
}()
|
|
|
|
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(urn resource.URN, id resource.ID,
|
2023-03-03 16:36:39 +00:00
|
|
|
inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
2020-12-15 22:24:46 +00:00
|
|
|
refreshes <- id
|
|
|
|
<-cancelled
|
|
|
|
|
|
|
|
new, hasNewState := newStates[id]
|
|
|
|
assert.True(t, hasNewState)
|
|
|
|
return plugin.ReadResult{Outputs: new}, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
CancelF: func() error {
|
|
|
|
close(cancelled)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshed := make(map[resource.ID]bool)
|
|
|
|
op := TestOp(Refresh)
|
2023-09-28 21:50:18 +00:00
|
|
|
options := TestUpdateOptions{
|
|
|
|
HostF: deploytest.NewPluginHostF(nil, nil, nil, loaders...),
|
|
|
|
UpdateOptions: UpdateOptions{
|
|
|
|
Parallel: 1,
|
|
|
|
},
|
2020-12-15 22:24:46 +00:00
|
|
|
}
|
2021-12-09 09:09:48 +00:00
|
|
|
project, target := p.GetProject(), p.GetTarget(t, old)
|
2020-12-15 22:24:46 +00:00
|
|
|
validate := func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
2023-10-11 14:44:09 +00:00
|
|
|
_ []Event, err error,
|
|
|
|
) error {
|
2020-12-15 22:24:46 +00:00
|
|
|
for _, entry := range entries {
|
|
|
|
assert.Equal(t, deploy.OpRefresh, entry.Step.Op())
|
|
|
|
resultOp := entry.Step.(*deploy.RefreshStep).ResultOp()
|
|
|
|
|
|
|
|
old := entry.Step.Old()
|
|
|
|
if !old.Custom || providers.IsProviderType(old.Type) {
|
|
|
|
// Component and provider resources should never change.
|
|
|
|
assert.Equal(t, deploy.OpSame, resultOp)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
refreshed[old.ID] = true
|
|
|
|
|
|
|
|
expected, new := newStates[old.ID], entry.Step.New()
|
|
|
|
if expected == nil {
|
|
|
|
// If the resource was deleted, we want the result op to be an OpDelete.
|
|
|
|
assert.Nil(t, new)
|
|
|
|
assert.Equal(t, deploy.OpDelete, resultOp)
|
|
|
|
} else {
|
|
|
|
// If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want
|
|
|
|
// an OpSame.
|
|
|
|
if reflect.DeepEqual(old.Outputs, expected) {
|
|
|
|
assert.Equal(t, deploy.OpSame, resultOp)
|
|
|
|
} else {
|
|
|
|
assert.Equal(t, deploy.OpUpdate, resultOp)
|
|
|
|
}
|
|
|
|
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
// Only the outputs and Modified timestamp should have changed (if anything changed).
|
2020-12-15 22:24:46 +00:00
|
|
|
old.Outputs = expected
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
old.Modified = new.Modified
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.Equal(t, old, new)
|
|
|
|
}
|
|
|
|
}
|
2023-10-11 14:44:09 +00:00
|
|
|
return err
|
2020-12-15 22:24:46 +00:00
|
|
|
}
|
|
|
|
|
2023-10-11 14:44:09 +00:00
|
|
|
snap, err := op.RunWithContext(ctx, project, target, options, false, nil, validate)
|
2023-11-16 09:58:30 +00:00
|
|
|
assert.ErrorContains(t, err, "BAIL: canceled")
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.Equal(t, 1, len(refreshed))
|
|
|
|
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
|
2021-12-09 09:09:48 +00:00
|
|
|
// The new resources will have had their default provider urn filled in. We fill this in on
|
|
|
|
// the old resources here as well so that the equal checks below pass
|
|
|
|
setProviderRef(t, oldResources, snap.Resources, provURN)
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
for _, r := range snap.Resources {
|
|
|
|
switch urn := r.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
continue
|
|
|
|
case urnA, urnB, urnC:
|
|
|
|
// break
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
|
|
|
|
idx, err := strconv.ParseInt(string(r.ID), 0, 0)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
if refreshed[r.ID] {
|
|
|
|
// The refreshed resource should have its new state.
|
|
|
|
expected := newStates[r.ID]
|
|
|
|
if expected == nil {
|
|
|
|
assert.Fail(t, "refreshed resource was not deleted")
|
|
|
|
} else {
|
|
|
|
old := oldResources[int(idx)]
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
|
|
|
|
// Only the outputs and Modified timestamp should have changed (if anything changed).
|
2020-12-15 22:24:46 +00:00
|
|
|
old.Outputs = expected
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
old.Modified = r.Modified
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.Equal(t, old, r)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Any resources that were not refreshed should retain their original state.
|
|
|
|
old := oldResources[int(idx)]
|
|
|
|
assert.Equal(t, old, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRefreshStepWillPersistUpdatedIDs(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-15 22:24:46 +00:00
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
provURN := p.NewProviderURN("pkgA", "default", "")
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
idBefore := resource.ID("myid")
|
|
|
|
idAfter := resource.ID("mynewid")
|
|
|
|
outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
|
|
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(
|
|
|
|
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
|
|
|
return plugin.ReadResult{ID: idAfter, Outputs: outputs, Inputs: resource.PropertyMap{}}, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
2020-12-19 07:03:40 +00:00
|
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
2020-12-15 22:24:46 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
|
|
})
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
2020-12-15 22:24:46 +00:00
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
p.Options.HostF = hostF
|
2020-12-15 22:24:46 +00:00
|
|
|
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: []*resource.State{
|
|
|
|
{
|
|
|
|
Type: resURN.Type(),
|
|
|
|
URN: resURN,
|
|
|
|
Custom: true,
|
|
|
|
ID: idBefore,
|
|
|
|
Inputs: resource.PropertyMap{},
|
|
|
|
Outputs: outputs,
|
|
|
|
InitErrors: []string{"Resource failed to initialize"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}}
|
|
|
|
snap := p.Run(t, old)
|
|
|
|
|
|
|
|
for _, resource := range snap.Resources {
|
|
|
|
switch urn := resource.URN; urn {
|
|
|
|
case provURN:
|
|
|
|
// break
|
|
|
|
case resURN:
|
|
|
|
assert.Empty(t, resource.InitErrors)
|
|
|
|
assert.Equal(t, idAfter, resource.ID)
|
|
|
|
default:
|
|
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-12-19 07:03:40 +00:00
|
|
|
|
|
|
|
// TestRefreshUpdateWithDeletedResource validates that the engine handles a deleted resource without error on an
|
|
|
|
// update with refresh.
|
|
|
|
func TestRefreshUpdateWithDeletedResource(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-12-19 07:03:40 +00:00
|
|
|
p := &TestPlan{}
|
|
|
|
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
idBefore := resource.ID("myid")
|
|
|
|
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
|
|
return &deploytest.Provider{
|
|
|
|
ReadF: func(
|
|
|
|
urn resource.URN, id resource.ID, inputs, state resource.PropertyMap,
|
|
|
|
) (plugin.ReadResult, resource.Status, error) {
|
|
|
|
return plugin.ReadResult{}, resource.StatusOK, nil
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
2020-12-19 07:03:40 +00:00
|
|
|
return nil
|
|
|
|
})
|
2023-09-28 21:50:18 +00:00
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
2020-12-19 07:03:40 +00:00
|
|
|
|
2023-09-28 21:50:18 +00:00
|
|
|
p.Options.HostF = hostF
|
2020-12-19 07:03:40 +00:00
|
|
|
p.Options.Refresh = true
|
|
|
|
|
|
|
|
old := &deploy.Snapshot{
|
|
|
|
Resources: []*resource.State{
|
|
|
|
{
|
|
|
|
Type: resURN.Type(),
|
|
|
|
URN: resURN,
|
|
|
|
Custom: true,
|
|
|
|
ID: idBefore,
|
|
|
|
Inputs: resource.PropertyMap{},
|
|
|
|
Outputs: resource.PropertyMap{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
p.Steps = []TestStep{{Op: Update}}
|
|
|
|
snap := p.Run(t, old)
|
|
|
|
assert.Equal(t, 0, len(snap.Resources))
|
|
|
|
}
|