e07c2c2c21
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.
2659 lines
83 KiB
Go
2659 lines
83 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.
|
|
|
|
// nolint: goconst
|
|
package lifecycletest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/blang/semver"
|
|
pbempty "github.com/golang/protobuf/ptypes/empty"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
|
|
. "github.com/pulumi/pulumi/pkg/v3/engine"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil/rpcerror"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
|
|
)
|
|
|
|
func SuccessfulSteps(entries JournalEntries) []deploy.Step {
|
|
var steps []deploy.Step
|
|
for _, entry := range entries {
|
|
if entry.Kind == JournalEntrySuccess {
|
|
steps = append(steps, entry.Step)
|
|
}
|
|
}
|
|
return steps
|
|
}
|
|
|
|
type StepSummary struct {
|
|
Op deploy.StepOp
|
|
URN resource.URN
|
|
}
|
|
|
|
func AssertSameSteps(t *testing.T, expected []StepSummary, actual []deploy.Step) bool {
|
|
assert.Equal(t, len(expected), len(actual))
|
|
for _, exp := range expected {
|
|
act := actual[0]
|
|
actual = actual[1:]
|
|
|
|
if !assert.Equal(t, exp.Op, act.Op()) || !assert.Equal(t, exp.URN, act.URN()) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func pickURN(t *testing.T, urns []resource.URN, names []string, target string) resource.URN {
|
|
assert.Equal(t, len(urns), len(names))
|
|
assert.Contains(t, names, target)
|
|
|
|
for i, name := range names {
|
|
if name == target {
|
|
return urns[i]
|
|
}
|
|
}
|
|
|
|
t.Fatalf("Could not find target: %v in %v", target, names)
|
|
return ""
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
grpcDefault := flag.Bool("grpc-providers", false, "enable or disable gRPC providers by default")
|
|
|
|
flag.Parse()
|
|
|
|
if *grpcDefault {
|
|
deploytest.UseGrpcProvidersByDefault = true
|
|
}
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestEmptyProgramLifecycle(t *testing.T) {
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 0),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestSingleResourceDiffUnavailable(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) {
|
|
|
|
return plugin.DiffResult{}, plugin.DiffUnavailable("diff unavailable")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
|
|
// Now run a preview. Expect a warning because the diff is unavailable.
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, res result.Result) result.Result {
|
|
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == DiagEvent {
|
|
p := e.Payload().(DiagEventPayload)
|
|
if p.URN == resURN && p.Severity == diag.Warning && p.Message == "diff unavailable" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return res
|
|
})
|
|
assert.Nil(t, res)
|
|
}
|
|
|
|
// Test that ensures that we log diagnostics for resources that receive an error from Check. (Note that this
|
|
// is distinct from receiving non-error failures from Check.)
|
|
func TestCheckFailureRecord(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CheckF: func(urn resource.URN,
|
|
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
|
|
return nil, nil, errors.New("oh no, check had an error")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
sawFailure := false
|
|
for _, evt := range evts {
|
|
if evt.Type == DiagEvent {
|
|
e := evt.Payload().(DiagEventPayload)
|
|
msg := colors.Never.Colorize(e.Message)
|
|
sawFailure = msg == "oh no, check had an error\n" && e.Severity == diag.Error
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawFailure)
|
|
return res
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test that checks that we emit diagnostics for properties that check says are invalid.
|
|
func TestCheckFailureInvalidPropertyRecord(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CheckF: func(urn resource.URN,
|
|
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
|
|
return nil, []plugin.CheckFailure{{
|
|
Property: "someprop",
|
|
Reason: "field is not valid",
|
|
}}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
sawFailure := false
|
|
for _, evt := range evts {
|
|
if evt.Type == DiagEvent {
|
|
e := evt.Payload().(DiagEventPayload)
|
|
msg := colors.Never.Colorize(e.Message)
|
|
sawFailure = strings.Contains(msg, "field is not valid") && e.Severity == diag.Error
|
|
if sawFailure {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawFailure)
|
|
return res
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
|
|
}
|
|
|
|
// Tests that errors returned directly from the language host get logged by the engine.
|
|
func TestLanguageHostDiagnostics(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
errorText := "oh no"
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
|
|
// Exiting immediately with an error simulates a language exiting immediately with a non-zero exit code.
|
|
return errors.New(errorText)
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
assertIsErrorOrBailResult(t, res)
|
|
sawExitCode := false
|
|
for _, evt := range evts {
|
|
if evt.Type == DiagEvent {
|
|
e := evt.Payload().(DiagEventPayload)
|
|
msg := colors.Never.Colorize(e.Message)
|
|
sawExitCode = strings.Contains(msg, errorText) && e.Severity == diag.Error
|
|
if sawExitCode {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.True(t, sawExitCode)
|
|
return res
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
type brokenDecrypter struct {
|
|
ErrorMessage string
|
|
}
|
|
|
|
func (b brokenDecrypter) DecryptValue(ciphertext string) (string, error) {
|
|
return "", fmt.Errorf(b.ErrorMessage)
|
|
}
|
|
|
|
// Tests that the engine presents a reasonable error message when a decrypter fails to decrypt a config value.
|
|
func TestBrokenDecrypter(t *testing.T) {
|
|
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, _ *deploytest.ResourceMonitor) error {
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
key := config.MustMakeKey("foo", "bar")
|
|
msg := "decryption failed"
|
|
configMap := make(config.Map)
|
|
configMap[key] = config.NewSecureValue("hunter2")
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Decrypter: brokenDecrypter{ErrorMessage: msg},
|
|
Config: configMap,
|
|
Steps: []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
assertIsErrorOrBailResult(t, res)
|
|
decryptErr := res.Error().(DecryptError)
|
|
assert.Equal(t, key, decryptErr.Key)
|
|
assert.Contains(t, decryptErr.Err.Error(), msg)
|
|
return res
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestBadResourceType(t *testing.T) {
|
|
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, mon *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := mon.RegisterResource("very:bad", "resA", true)
|
|
assert.Error(t, err)
|
|
rpcerr, ok := rpcerror.FromError(err)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, codes.InvalidArgument, rpcerr.Code())
|
|
assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token")
|
|
|
|
_, _, err = mon.ReadResource("very:bad", "someResource", "someId", "", resource.PropertyMap{}, "", "")
|
|
assert.Error(t, err)
|
|
rpcerr, ok = rpcerror.FromError(err)
|
|
assert.True(t, ok)
|
|
assert.Equal(t, codes.InvalidArgument, rpcerr.Code())
|
|
assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token")
|
|
|
|
// Component resources may have any format type.
|
|
_, _, _, noErr := mon.RegisterResource("a:component", "resB", false)
|
|
assert.NoError(t, noErr)
|
|
|
|
_, _, _, noErr = mon.RegisterResource("singlename", "resC", false)
|
|
assert.NoError(t, noErr)
|
|
|
|
return err
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Tests that provider cancellation occurs as expected.
|
|
func TestProviderCancellation(t *testing.T) {
|
|
const resourceCount = 4
|
|
|
|
// Set up a cancelable context for the refresh operation.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Wait for our resource ops, then cancel.
|
|
var ops sync.WaitGroup
|
|
ops.Add(resourceCount)
|
|
go func() {
|
|
ops.Wait()
|
|
cancel()
|
|
}()
|
|
|
|
// Set up an independent cancelable context for the provider's operations.
|
|
provCtx, provCancel := context.WithCancel(context.Background())
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
|
|
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
|
|
|
|
// Inform the waiter that we've entered a provider op and wait for cancellation.
|
|
ops.Done()
|
|
<-provCtx.Done()
|
|
|
|
return resource.ID(urn.Name()), resource.PropertyMap{}, resource.StatusOK, nil
|
|
},
|
|
CancelF: func() error {
|
|
provCancel()
|
|
return nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
done := make(chan bool)
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
errors := make([]error, resourceCount)
|
|
var resources sync.WaitGroup
|
|
resources.Add(resourceCount)
|
|
for i := 0; i < resourceCount; i++ {
|
|
go func(idx int) {
|
|
_, _, _, errors[idx] = monitor.RegisterResource("pkgA:m:typA", fmt.Sprintf("res%d", idx), true)
|
|
resources.Done()
|
|
}(i)
|
|
}
|
|
resources.Wait()
|
|
for _, err := range errors {
|
|
assert.NoError(t, err)
|
|
}
|
|
close(done)
|
|
return nil
|
|
})
|
|
|
|
p := &TestPlan{}
|
|
op := TestOp(Update)
|
|
options := UpdateOptions{
|
|
Parallel: resourceCount,
|
|
Host: deploytest.NewPluginHost(nil, nil, program, loaders...),
|
|
}
|
|
project, target := p.GetProject(), p.GetTarget(nil)
|
|
|
|
_, res := op.RunWithContext(ctx, project, target, options, false, nil, nil)
|
|
assertIsErrorOrBailResult(t, res)
|
|
|
|
// Wait for the program to finish.
|
|
<-done
|
|
}
|
|
|
|
// Tests that a preview works for a stack with pending operations.
|
|
func TestPreviewWithPendingOperations(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)
|
|
|
|
// A preview should succeed despite the pending operations.
|
|
_, res := op.Run(project, target, options, true, nil, nil)
|
|
assert.Nil(t, res)
|
|
|
|
// But an update should fail.
|
|
_, res = op.Run(project, target, options, false, nil, nil)
|
|
assertIsErrorOrBailResult(t, res)
|
|
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{
|
|
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) {
|
|
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
}, nil
|
|
},
|
|
|
|
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
|
|
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
|
|
|
|
outputs := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"output_prop": 42,
|
|
})
|
|
|
|
return outputs, resource.StatusPartialFailure, errors.New("update failed to apply")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := mon.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"input_prop": "new inputs",
|
|
}),
|
|
})
|
|
return err
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{Options: UpdateOptions{Host: host}}
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
p.Steps = []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
assertIsErrorOrBailResult(t, res)
|
|
for _, entry := range entries {
|
|
switch urn := entry.Step.URN(); urn {
|
|
case resURN:
|
|
assert.Equal(t, deploy.OpUpdate, entry.Step.Op())
|
|
switch entry.Kind {
|
|
case JournalEntryBegin:
|
|
continue
|
|
case JournalEntrySuccess:
|
|
inputs := entry.Step.New().Inputs
|
|
outputs := entry.Step.New().Outputs
|
|
assert.Len(t, inputs, 1)
|
|
assert.Len(t, outputs, 1)
|
|
assert.Equal(t,
|
|
resource.NewStringProperty("old inputs"), inputs[resource.PropertyKey("input_prop")])
|
|
assert.Equal(t,
|
|
resource.NewNumberProperty(42), outputs[resource.PropertyKey("output_prop")])
|
|
default:
|
|
t.Fatalf("unexpected journal operation: %d", entry.Kind)
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
},
|
|
}}
|
|
|
|
old := &deploy.Snapshot{
|
|
Resources: []*resource.State{
|
|
{
|
|
Type: resURN.Type(),
|
|
URN: resURN,
|
|
Custom: true,
|
|
ID: "1",
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"input_prop": "old inputs",
|
|
}),
|
|
Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"output_prop": 1,
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
p.Run(t, old)
|
|
}
|
|
|
|
// Tests that the StackReference resource works as intended,
|
|
func TestStackReference(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{}
|
|
|
|
// Test that the normal lifecycle works correctly.
|
|
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, state, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other",
|
|
}),
|
|
})
|
|
assert.NoError(t, err)
|
|
if !info.DryRun {
|
|
assert.Equal(t, "bar", state["outputs"].ObjectValue()["foo"].StringValue())
|
|
}
|
|
return nil
|
|
})
|
|
p := &TestPlan{
|
|
BackendClient: &deploytest.BackendClient{
|
|
GetStackOutputsF: func(ctx context.Context, name string) (resource.PropertyMap, error) {
|
|
switch name {
|
|
case "other":
|
|
return resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
}), nil
|
|
default:
|
|
return nil, fmt.Errorf("unknown stack \"%s\"", name)
|
|
}
|
|
},
|
|
},
|
|
Options: UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)},
|
|
Steps: MakeBasicLifecycleSteps(t, 2),
|
|
}
|
|
p.Run(t, nil)
|
|
|
|
// Test that changes to `name` cause replacement.
|
|
resURN := p.NewURN("pulumi:pulumi:StackReference", "other", "")
|
|
old := &deploy.Snapshot{
|
|
Resources: []*resource.State{
|
|
{
|
|
Type: resURN.Type(),
|
|
URN: resURN,
|
|
Custom: true,
|
|
ID: "1",
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other2",
|
|
}),
|
|
Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other2",
|
|
"outputs": resource.PropertyMap{},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
p.Steps = []TestStep{{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
|
|
assert.Nil(t, res)
|
|
for _, entry := range entries {
|
|
switch urn := entry.Step.URN(); urn {
|
|
case resURN:
|
|
switch entry.Step.Op() {
|
|
case deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReplace:
|
|
// OK
|
|
default:
|
|
t.Fatalf("unexpected journal operation: %v", entry.Step.Op())
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
},
|
|
}}
|
|
p.Run(t, old)
|
|
|
|
// Test that unknown stacks are handled appropriately.
|
|
program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "rehto",
|
|
}),
|
|
})
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
|
|
p.Steps = []TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
}}
|
|
p.Run(t, nil)
|
|
|
|
// Test that unknown properties cause errors.
|
|
program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other",
|
|
"foo": "bar",
|
|
}),
|
|
})
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
type channelWriter struct {
|
|
channel chan []byte
|
|
}
|
|
|
|
func (cw *channelWriter) Write(d []byte) (int, error) {
|
|
cw.channel <- d
|
|
return len(d), nil
|
|
}
|
|
|
|
// Tests that a failed plugin load correctly shuts down the host.
|
|
func TestLoadFailureShutdown(t *testing.T) {
|
|
|
|
// Note that the setup here is a bit baroque, and is intended to replicate the CLI architecture that lead to
|
|
// issue #2170. That issue--a panic on a closed channel--was caused by the intersection of several design choices:
|
|
//
|
|
// - The provider registry loads and configures the set of providers necessary for the resources currently in the
|
|
// checkpoint it is processing at plan creation time. Registry creation fails promptly if a provider plugin
|
|
// fails to load (e.g. because is binary is missing).
|
|
// - Provider configuration in the CLI's host happens asynchronously. This is meant to allow the engine to remain
|
|
// responsive while plugins configure.
|
|
// - Providers may call back into the CLI's host for logging. Callbacks are processed as long as the CLI's plugin
|
|
// context is open.
|
|
// - Log events from the CLI's host are delivered to the CLI's diagnostic streams via channels. The CLI closes
|
|
// these channels once the engine operation it initiated completes.
|
|
//
|
|
// These choices gave rise to the following situation:
|
|
// 1. The provider registry loads a provider for package A and kicks off its configuration.
|
|
// 2. The provider registry attempts to load a provider for package B. The load fails, and the provider registry
|
|
// creation call fails promptly.
|
|
// 3. The engine operation requested by the CLI fails promptly because provider registry creation failed.
|
|
// 4. The CLI shuts down its diagnostic channels.
|
|
// 5. The provider for package A calls back in to the host to log a message. The host then attempts to deliver
|
|
// the message to the CLI's diagnostic channels, causing a panic.
|
|
//
|
|
// The fix was to properly close the plugin host during step (3) s.t. the host was no longer accepting callbacks
|
|
// and would not attempt to send messages to the CLI's diagnostic channels.
|
|
//
|
|
// As such, this test attempts to replicate the CLI architecture by using one provider that configures
|
|
// asynchronously and attempts to call back into the engine and a second provider that fails to load.
|
|
|
|
release, done := make(chan bool), make(chan bool)
|
|
sinkWriter := &channelWriter{channel: make(chan []byte)}
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoaderWithHost("pkgA", semver.MustParse("1.0.0"),
|
|
func(host plugin.Host) (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ConfigureF: func(news resource.PropertyMap) error {
|
|
go func() {
|
|
<-release
|
|
host.Log(diag.Info, "", "configuring pkgA provider...", 0)
|
|
close(done)
|
|
}()
|
|
return nil
|
|
},
|
|
}, nil
|
|
}),
|
|
deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return nil, errors.New("pkgB load failure")
|
|
}),
|
|
}
|
|
|
|
p := &TestPlan{}
|
|
provAURN := p.NewProviderURN("pkgA", "default", "")
|
|
provBURN := p.NewProviderURN("pkgB", "default", "")
|
|
|
|
old := &deploy.Snapshot{
|
|
Resources: []*resource.State{
|
|
{
|
|
Type: provAURN.Type(),
|
|
URN: provAURN,
|
|
Custom: true,
|
|
ID: "0",
|
|
Inputs: resource.PropertyMap{},
|
|
Outputs: resource.PropertyMap{},
|
|
},
|
|
{
|
|
Type: provBURN.Type(),
|
|
URN: provBURN,
|
|
Custom: true,
|
|
ID: "1",
|
|
Inputs: resource.PropertyMap{},
|
|
Outputs: resource.PropertyMap{},
|
|
},
|
|
},
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
return nil
|
|
})
|
|
|
|
op := TestOp(Update)
|
|
sink := diag.DefaultSink(sinkWriter, sinkWriter, diag.FormatOptions{Color: colors.Raw})
|
|
options := UpdateOptions{Host: deploytest.NewPluginHost(sink, sink, program, loaders...)}
|
|
project, target := p.GetProject(), p.GetTarget(old)
|
|
|
|
_, res := op.Run(project, target, options, true, nil, nil)
|
|
assertIsErrorOrBailResult(t, res)
|
|
|
|
close(sinkWriter.channel)
|
|
close(release)
|
|
<-done
|
|
}
|
|
|
|
func TestSingleResourceIgnoreChanges(t *testing.T) {
|
|
var expectedIgnoreChanges []string
|
|
|
|
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) {
|
|
|
|
assert.Equal(t, expectedIgnoreChanges, ignoreChanges)
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
|
|
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
|
|
|
|
assert.Equal(t, expectedIgnoreChanges, ignoreChanges)
|
|
return resource.PropertyMap{}, resource.StatusOK, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, ignoreChanges []string,
|
|
allowedOps []deploy.StepOp) *deploy.Snapshot {
|
|
expectedIgnoreChanges = ignoreChanges
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: props,
|
|
IgnoreChanges: ignoreChanges,
|
|
})
|
|
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 an ignored property results in an OpSame
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 2,
|
|
"b": map[string]interface{}{
|
|
"c": "bar",
|
|
},
|
|
}), []string{"a", "b.c"}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that a change to an un-ignored property results in an OpUpdate
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 3,
|
|
"b": map[string]interface{}{
|
|
"c": "qux",
|
|
},
|
|
}), nil, []deploy.StepOp{deploy.OpUpdate})
|
|
|
|
// Ensure that a removing an ignored property results in an OpSame
|
|
snap = updateProgramWithProps(snap, resource.PropertyMap{}, []string{"a", "b"}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that a removing an un-ignored property results in an OpUpdate
|
|
snap = updateProgramWithProps(snap, resource.PropertyMap{}, nil, []deploy.StepOp{deploy.OpUpdate})
|
|
|
|
// Ensure that adding an ignored property results in an OpSame
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 4,
|
|
"b": map[string]interface{}{
|
|
"c": "zed",
|
|
},
|
|
}), []string{"a", "b"}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that adding an un-ignored property results in an OpUpdate
|
|
_ = updateProgramWithProps(snap, resource.PropertyMap{
|
|
"c": resource.NewNumberProperty(4),
|
|
}, []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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
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
|
|
}
|
|
} else if vd.Array != nil {
|
|
for kk, pd := range arrayDiffToDetailedDiff(prefix, vd.Array) {
|
|
ret[kk] = pd
|
|
}
|
|
} else {
|
|
ret[prefix] = plugin.PropertyDiff{Kind: plugin.DiffUpdate}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
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
|
|
},
|
|
}, 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
|
|
}
|
|
|
|
// Resource is an abstract representation of a resource graph
|
|
type Resource struct {
|
|
t tokens.Type
|
|
name string
|
|
children []Resource
|
|
props resource.PropertyMap
|
|
aliases []resource.URN
|
|
dependencies []resource.URN
|
|
parent resource.URN
|
|
deleteBeforeReplace bool
|
|
}
|
|
|
|
func registerResources(t *testing.T, monitor *deploytest.ResourceMonitor, resources []Resource) error {
|
|
for _, r := range resources {
|
|
_, _, _, err := monitor.RegisterResource(r.t, r.name, true, deploytest.ResourceOptions{
|
|
Parent: r.parent,
|
|
Dependencies: r.dependencies,
|
|
Inputs: r.props,
|
|
DeleteBeforeReplace: &r.deleteBeforeReplace,
|
|
Aliases: r.aliases,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = registerResources(t, monitor, r.children)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestAliases(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
// The `forcesReplacement` key forces replacement and all other keys can update in place
|
|
DiffF: func(res resource.URN, id resource.ID, olds, news resource.PropertyMap,
|
|
ignoreChanges []string) (plugin.DiffResult, error) {
|
|
|
|
replaceKeys := []resource.PropertyKey{}
|
|
old, hasOld := olds["forcesReplacement"]
|
|
new, hasNew := news["forcesReplacement"]
|
|
if hasOld && !hasNew || hasNew && !hasOld || hasOld && hasNew && old.Diff(new) != nil {
|
|
replaceKeys = append(replaceKeys, "forcesReplacement")
|
|
}
|
|
return plugin.DiffResult{ReplaceKeys: replaceKeys}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
updateProgramWithResource := func(
|
|
snap *deploy.Snapshot, resources []Resource, allowedOps []deploy.StepOp) *deploy.Snapshot {
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
err := registerResources(t, monitor, resources)
|
|
return err
|
|
})
|
|
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})
|
|
}
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.Step.Type() == "pulumi:providers:pkgA" {
|
|
continue
|
|
}
|
|
switch entry.Kind {
|
|
case JournalEntrySuccess:
|
|
assert.Subset(t, allowedOps, []deploy.StepOp{entry.Step.Op()})
|
|
case JournalEntryFailure:
|
|
assert.Fail(t, "unexpected failure in journal")
|
|
case JournalEntryBegin:
|
|
case JournalEntryOutputs:
|
|
}
|
|
}
|
|
|
|
return res
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return p.Run(t, snap)
|
|
}
|
|
|
|
snap := updateProgramWithResource(nil, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
}}, []deploy.StepOp{deploy.OpCreate})
|
|
|
|
// Ensure that rename produces Same
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n2",
|
|
aliases: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that rename produces Same with multiple aliases
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n3",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
"urn:pulumi:test::test::pkgA:index:t1::n2",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that rename produces Same with multiple aliases (reversed)
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n3",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n2",
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that aliasing back to original name is okay
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n3",
|
|
"urn:pulumi:test::test::pkgA:index:t1::n2",
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that removing aliases is okay (once old names are gone from all snapshots)
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that changing the type works
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t2",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that changing the type again works
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:othermod:t3",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
"urn:pulumi:test::test::pkgA:index:t2::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that order of aliases doesn't matter
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:othermod:t3",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
"urn:pulumi:test::test::pkgA:othermod:t3::n1",
|
|
"urn:pulumi:test::test::pkgA:index:t2::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that removing aliases is okay (once old names are gone from all snapshots)
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:othermod:t3",
|
|
name: "n1",
|
|
}}, []deploy.StepOp{deploy.OpSame})
|
|
|
|
// Ensure that changing everything (including props) leads to update not delete and re-create
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t4",
|
|
name: "n2",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("x"): resource.NewNumberProperty(42),
|
|
},
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:othermod:t3::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpUpdate})
|
|
|
|
// Ensure that changing everything again (including props) leads to update not delete and re-create
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t5",
|
|
name: "n3",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("x"): resource.NewNumberProperty(1000),
|
|
},
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t4::n2",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpUpdate})
|
|
|
|
// Ensure that changing a forceNew property while also changing type and name leads to replacement not delete+create
|
|
snap = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t6",
|
|
name: "n4",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1000),
|
|
},
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t5::n3",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
|
|
|
|
// Ensure that changing a forceNew property and deleteBeforeReplace while also changing type and name leads to
|
|
// replacement not delete+create
|
|
_ = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t7",
|
|
name: "n5",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(999),
|
|
},
|
|
deleteBeforeReplace: true,
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t6::n4",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
|
|
|
|
// Start again - this time with two resources with depends on relationship
|
|
snap = updateProgramWithResource(nil, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1),
|
|
},
|
|
deleteBeforeReplace: true,
|
|
}, {
|
|
t: "pkgA:index:t2",
|
|
name: "n2",
|
|
dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"},
|
|
}}, []deploy.StepOp{deploy.OpCreate})
|
|
|
|
_ = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1-new",
|
|
name: "n1-new",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2),
|
|
},
|
|
deleteBeforeReplace: true,
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}, {
|
|
t: "pkgA:index:t2-new",
|
|
name: "n2-new",
|
|
dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1-new::n1-new"},
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t2::n2",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
|
|
|
|
// Start again - this time with two resources with parent relationship
|
|
snap = updateProgramWithResource(nil, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1),
|
|
},
|
|
deleteBeforeReplace: true,
|
|
}, {
|
|
t: "pkgA:index:t2",
|
|
name: "n2",
|
|
parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"),
|
|
}}, []deploy.StepOp{deploy.OpCreate})
|
|
|
|
_ = updateProgramWithResource(snap, []Resource{{
|
|
t: "pkgA:index:t1-new",
|
|
name: "n1-new",
|
|
props: resource.PropertyMap{
|
|
resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2),
|
|
},
|
|
deleteBeforeReplace: true,
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}, {
|
|
t: "pkgA:index:t2-new",
|
|
name: "n2-new",
|
|
parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"),
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n2",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced})
|
|
|
|
// ensure failure when different resources use duplicate aliases
|
|
snap = updateProgramWithResource(nil, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}, {
|
|
t: "pkgA:index:t2",
|
|
name: "n2",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpCreate})
|
|
|
|
err := snap.NormalizeURNReferences()
|
|
assert.Equal(t, err.Error(),
|
|
"Two resources ('urn:pulumi:test::test::pkgA:index:t1::n1'"+
|
|
" and 'urn:pulumi:test::test::pkgA:index:t2::n2') aliased to the same: 'urn:pulumi:test::test::pkgA:index:t1::n1'")
|
|
|
|
// ensure different resources can use different aliases
|
|
snap = updateProgramWithResource(nil, []Resource{{
|
|
t: "pkgA:index:t1",
|
|
name: "n1",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n1",
|
|
},
|
|
}, {
|
|
t: "pkgA:index:t2",
|
|
name: "n2",
|
|
aliases: []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:index:t1::n2",
|
|
},
|
|
}}, []deploy.StepOp{deploy.OpCreate})
|
|
|
|
err = snap.NormalizeURNReferences()
|
|
assert.Nil(t, err)
|
|
}
|
|
|
|
func TestPersistentDiff(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) {
|
|
|
|
return plugin.DiffResult{Changes: plugin.DiffSome}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
|
|
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
|
|
// provider diffing.
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, res result.Result) result.Result {
|
|
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == ResourcePreEvent {
|
|
p := e.Payload().(ResourcePreEventPayload).Metadata
|
|
if p.URN == resURN {
|
|
assert.Equal(t, deploy.OpUpdate, p.Op)
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return res
|
|
})
|
|
assert.Nil(t, res)
|
|
|
|
// Next, enable legacy diff behavior. We should see no changes to the resource.
|
|
p.Options.UseLegacyDiff = true
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, res result.Result) result.Result {
|
|
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == ResourcePreEvent {
|
|
p := e.Payload().(ResourcePreEventPayload).Metadata
|
|
if p.URN == resURN {
|
|
assert.Equal(t, deploy.OpSame, p.Op)
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return res
|
|
})
|
|
assert.Nil(t, res)
|
|
}
|
|
|
|
func TestDetailedDiffReplace(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) {
|
|
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"prop": {Kind: plugin.DiffAddReplace},
|
|
},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
|
|
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
|
|
// provider diffing.
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, res result.Result) result.Result {
|
|
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == ResourcePreEvent {
|
|
p := e.Payload().(ResourcePreEventPayload).Metadata
|
|
if p.URN == resURN && p.Op == deploy.OpReplace {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return res
|
|
})
|
|
assert.Nil(t, res)
|
|
}
|
|
|
|
func TestCustomTimeouts(t *testing.T) {
|
|
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, deploytest.ResourceOptions{
|
|
CustomTimeouts: &resource.CustomTimeouts{
|
|
Create: 60, Delete: 60, Update: 240,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
|
|
p.Steps = []TestStep{{Op: Update}}
|
|
snap := p.Run(t, nil)
|
|
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, string(snap.Resources[0].URN.Name()), "default")
|
|
assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA")
|
|
assert.NotNil(t, snap.Resources[1].CustomTimeouts)
|
|
assert.Equal(t, snap.Resources[1].CustomTimeouts.Create, float64(60))
|
|
assert.Equal(t, snap.Resources[1].CustomTimeouts.Update, float64(240))
|
|
assert.Equal(t, snap.Resources[1].CustomTimeouts.Delete, float64(60))
|
|
}
|
|
|
|
func TestProviderDiffMissingOldOutputs(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
|
|
ignoreChanges []string) (plugin.DiffResult, error) {
|
|
// Always require replacement if any diff exists.
|
|
if !olds.DeepEquals(news) {
|
|
keys := []resource.PropertyKey{}
|
|
for k := range news {
|
|
keys = append(keys, k)
|
|
}
|
|
return plugin.DiffResult{Changes: plugin.DiffSome, ReplaceKeys: keys}, nil
|
|
}
|
|
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
|
|
},
|
|
}, 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
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Config: config.Map{
|
|
config.MustMakeKey("pkgA", "foo"): config.NewValue("bar"),
|
|
},
|
|
}
|
|
|
|
// Build a basic lifecycle.
|
|
steps := MakeBasicLifecycleSteps(t, 2)
|
|
|
|
// Run the lifecycle through its initial update and refresh.
|
|
p.Steps = steps[:2]
|
|
snap := p.Run(t, nil)
|
|
|
|
// Delete the old provider outputs (if any) from the checkpoint, then run the no-op update.
|
|
providerURN := p.NewProviderURN("pkgA", "default", "")
|
|
for _, r := range snap.Resources {
|
|
if r.URN == providerURN {
|
|
r.Outputs = nil
|
|
}
|
|
}
|
|
|
|
p.Steps = steps[2:3]
|
|
snap = p.Run(t, snap)
|
|
|
|
// Change the config, delete the old provider outputs, and run an update. We expect everything to require
|
|
// replacement.
|
|
p.Config[config.MustMakeKey("pkgA", "foo")] = config.NewValue("baz")
|
|
for _, r := range snap.Resources {
|
|
if r.URN == providerURN {
|
|
r.Outputs = nil
|
|
}
|
|
}
|
|
p.Steps = []TestStep{{
|
|
Op: Update,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
_ []Event, res result.Result) result.Result {
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Look for replace steps on the provider and the resource.
|
|
replacedProvider, replacedResource := false, false
|
|
for _, entry := range entries {
|
|
if entry.Kind != JournalEntrySuccess || entry.Step.Op() != deploy.OpDeleteReplaced {
|
|
continue
|
|
}
|
|
|
|
switch urn := entry.Step.URN(); urn {
|
|
case providerURN:
|
|
replacedProvider = true
|
|
case resURN:
|
|
replacedResource = true
|
|
default:
|
|
t.Fatalf("unexpected resource %v", urn)
|
|
}
|
|
}
|
|
assert.True(t, replacedProvider)
|
|
assert.True(t, replacedResource)
|
|
|
|
return res
|
|
},
|
|
}}
|
|
p.Run(t, snap)
|
|
}
|
|
|
|
func TestMissingRead(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ReadF: func(_ resource.URN, _ resource.ID, _, _ resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
|
|
return plugin.ReadResult{}, resource.StatusOK, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
// Our program reads a resource and exits.
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "")
|
|
assert.Error(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: []TestStep{{Op: Update, ExpectFailure: true}},
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestProviderPreview(t *testing.T) {
|
|
sawPreview := false
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, preview, news.ContainsUnknowns())
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, preview, news.ContainsUnknowns())
|
|
return news, resource.StatusOK, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
preview := true
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")})
|
|
if !preview {
|
|
computed = "alpha"
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": map[string]interface{}{
|
|
"a": 42,
|
|
"b": computed,
|
|
},
|
|
"qux": []interface{}{
|
|
computed,
|
|
24,
|
|
},
|
|
"zed": computed,
|
|
})
|
|
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, state.DeepEquals(ins))
|
|
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run a preview. The inputs should be propagated to the outputs by the provider during the create.
|
|
preview, sawPreview = true, false
|
|
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.True(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run another preview. The inputs should be propagated to the outputs during the update.
|
|
preview, sawPreview = true, false
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.True(t, sawPreview)
|
|
}
|
|
|
|
func TestProviderPreviewGrpc(t *testing.T) {
|
|
sawPreview := false
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, preview, news.ContainsUnknowns())
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, preview, news.ContainsUnknowns())
|
|
return news, resource.StatusOK, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithGrpc),
|
|
}
|
|
|
|
preview := true
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")})
|
|
if !preview {
|
|
computed = "alpha"
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": map[string]interface{}{
|
|
"a": 42,
|
|
"b": computed,
|
|
},
|
|
"qux": []interface{}{
|
|
computed,
|
|
24,
|
|
},
|
|
"zed": computed,
|
|
})
|
|
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, state.DeepEquals(ins))
|
|
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run a preview. The inputs should be propagated to the outputs by the provider during the create.
|
|
preview, sawPreview = true, false
|
|
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.True(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run another preview. The inputs should be propagated to the outputs during the update.
|
|
preview, sawPreview = true, false
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, preview, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
assert.True(t, sawPreview)
|
|
}
|
|
|
|
func TestProviderPreviewUnknowns(t *testing.T) {
|
|
sawPreview := false
|
|
loaders := []*deploytest.ProviderLoader{
|
|
// NOTE: it is important that this test uses a gRPC-wraped provider. The code that handles previews for unconfigured
|
|
// providers is specific to the gRPC layer.
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
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) {
|
|
|
|
if preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
return news, resource.StatusOK, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithGrpc),
|
|
}
|
|
|
|
preview := true
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")})
|
|
if !preview {
|
|
computed = "alpha"
|
|
}
|
|
|
|
provURN, provID, _, err := monitor.RegisterResource("pulumi:providers:pkgA", "provA", true,
|
|
deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{"foo": computed}),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": map[string]interface{}{
|
|
"a": 42,
|
|
},
|
|
"qux": []interface{}{
|
|
24,
|
|
},
|
|
})
|
|
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
Provider: fmt.Sprintf("%v::%v", provURN, provID),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
if preview {
|
|
assert.True(t, state.DeepEquals(resource.PropertyMap{}))
|
|
} else {
|
|
assert.True(t, state.DeepEquals(ins))
|
|
}
|
|
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run a preview. The inputs should not be propagated to the outputs by the provider during the create because the
|
|
// provider has unknown inputs.
|
|
preview, sawPreview = true, false
|
|
_, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
require.Nil(t, res)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, preview, p.BackendClient, nil)
|
|
require.Nil(t, res)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run another preview. The inputs should not be propagated to the outputs during the update because the provider
|
|
// has unknown inputs.
|
|
preview, sawPreview = true, false
|
|
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, preview, p.BackendClient, nil)
|
|
require.Nil(t, res)
|
|
assert.False(t, sawPreview)
|
|
}
|
|
|
|
func TestSingleComponentDefaultProviderLifecycle(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(monitor *deploytest.ResourceMonitor,
|
|
typ, name string, parent resource.URN, inputs resource.PropertyMap,
|
|
options plugin.ConstructOptions) (plugin.ConstructResult, error) {
|
|
|
|
urn, _, _, err := monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{
|
|
Parent: parent,
|
|
Aliases: options.Aliases,
|
|
Protect: options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
err = monitor.RegisterResourceOutputs(urn, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{
|
|
URN: urn,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
}, state)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 3),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
type updateContext struct {
|
|
*deploytest.ResourceMonitor
|
|
|
|
resmon chan *deploytest.ResourceMonitor
|
|
programErr chan error
|
|
snap chan *deploy.Snapshot
|
|
updateResult chan result.Result
|
|
}
|
|
|
|
func startUpdate(host plugin.Host) (*updateContext, error) {
|
|
ctx := &updateContext{
|
|
resmon: make(chan *deploytest.ResourceMonitor),
|
|
programErr: make(chan error),
|
|
snap: make(chan *deploy.Snapshot),
|
|
updateResult: make(chan result.Result),
|
|
}
|
|
|
|
stop := make(chan bool)
|
|
port, _, err := rpcutil.Serve(0, stop, []func(*grpc.Server) error{
|
|
func(srv *grpc.Server) error {
|
|
pulumirpc.RegisterLanguageRuntimeServer(srv, ctx)
|
|
return nil
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Runtime: "client",
|
|
RuntimeOptions: map[string]interface{}{
|
|
"address": fmt.Sprintf("127.0.0.1:%d", port),
|
|
},
|
|
}
|
|
|
|
go func() {
|
|
snap, res := TestOp(Update).Run(p.GetProject(), p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
|
ctx.snap <- snap
|
|
close(ctx.snap)
|
|
ctx.updateResult <- res
|
|
close(ctx.updateResult)
|
|
stop <- true
|
|
}()
|
|
|
|
ctx.ResourceMonitor = <-ctx.resmon
|
|
return ctx, nil
|
|
}
|
|
|
|
func (ctx *updateContext) Finish(err error) (*deploy.Snapshot, result.Result) {
|
|
ctx.programErr <- err
|
|
close(ctx.programErr)
|
|
|
|
return <-ctx.snap, <-ctx.updateResult
|
|
}
|
|
|
|
func (ctx *updateContext) GetRequiredPlugins(_ context.Context,
|
|
req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) {
|
|
return &pulumirpc.GetRequiredPluginsResponse{}, nil
|
|
}
|
|
|
|
func (ctx *updateContext) Run(_ context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) {
|
|
// Connect to the resource monitor and create an appropriate client.
|
|
conn, err := grpc.Dial(
|
|
req.MonitorAddress,
|
|
grpc.WithInsecure(),
|
|
rpcutil.GrpcChannelOptions(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not connect to resource monitor: %w", err)
|
|
}
|
|
defer contract.IgnoreClose(conn)
|
|
|
|
// Fire up a resource monitor client
|
|
ctx.resmon <- deploytest.NewResourceMonitor(pulumirpc.NewResourceMonitorClient(conn))
|
|
close(ctx.resmon)
|
|
|
|
// Wait for the program to terminate.
|
|
if err := <-ctx.programErr; err != nil {
|
|
return &pulumirpc.RunResponse{Error: err.Error()}, nil
|
|
}
|
|
return &pulumirpc.RunResponse{}, nil
|
|
}
|
|
|
|
func (ctx *updateContext) GetPluginInfo(_ context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
|
|
return &pulumirpc.PluginInfo{
|
|
Version: "1.0.0",
|
|
}, nil
|
|
}
|
|
|
|
func TestLanguageClient(t *testing.T) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
update, err := startUpdate(deploytest.NewPluginHost(nil, nil, nil, loaders...))
|
|
if err != nil {
|
|
t.Fatalf("failed to start update: %v", err)
|
|
}
|
|
|
|
// Register resources, etc.
|
|
_, _, _, err = update.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
|
|
snap, res := update.Finish(nil)
|
|
assert.Nil(t, res)
|
|
assert.Len(t, snap.Resources, 2)
|
|
}
|
|
|
|
func TestSingleComponentGetResourceDefaultProviderLifecycle(t *testing.T) {
|
|
var urnB resource.URN
|
|
var idB resource.ID
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(monitor *deploytest.ResourceMonitor, typ, name string, parent resource.URN,
|
|
inputs resource.PropertyMap, options plugin.ConstructOptions) (plugin.ConstructResult, error) {
|
|
|
|
urn, _, _, err := monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{
|
|
Parent: parent,
|
|
Protect: options.Protect,
|
|
Aliases: options.Aliases,
|
|
Dependencies: options.Dependencies,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
urnB, idB, _, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
Inputs: resource.PropertyMap{
|
|
"bar": resource.NewStringProperty("baz"),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{
|
|
URN: urn,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
"res": resource.MakeCustomResourceReference(urnB, idB, ""),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64,
|
|
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
|
|
return "created-id", inputs, resource.StatusOK, nil
|
|
},
|
|
ReadF: func(urn resource.URN, id resource.ID,
|
|
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
|
|
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
|
|
},
|
|
ConstructF: construct,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
"res": resource.MakeCustomResourceReference(urnB, idB, ""),
|
|
}, state)
|
|
|
|
result, _, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{
|
|
"urn": resource.NewStringProperty(string(urnB)),
|
|
}, "", "")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"urn": resource.NewStringProperty(string(urnB)),
|
|
"id": resource.NewStringProperty(string(idB)),
|
|
"state": resource.NewObjectProperty(resource.PropertyMap{
|
|
"bar": resource.NewStringProperty("baz"),
|
|
}),
|
|
}, result)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 4),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestConfigSecrets(t *testing.T) {
|
|
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
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
crypter := config.NewSymmetricCrypter(make([]byte, 32))
|
|
secret, err := crypter.EncryptValue("hunter2")
|
|
assert.NoError(t, err)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 2),
|
|
Config: config.Map{
|
|
config.MustMakeKey("pkgA", "secret"): config.NewSecureValue(secret),
|
|
},
|
|
Decrypter: crypter,
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, res := TestOp(Update).Run(project, p.GetTarget(nil), p.Options, false, p.BackendClient, nil)
|
|
assert.Nil(t, res)
|
|
|
|
if !assert.Len(t, snap.Resources, 2) {
|
|
return
|
|
}
|
|
|
|
provider := snap.Resources[0]
|
|
assert.True(t, provider.Inputs["secret"].IsSecret())
|
|
assert.True(t, provider.Outputs["secret"].IsSecret())
|
|
}
|
|
|
|
func TestComponentOutputs(t *testing.T) {
|
|
// A component's outputs should never be returned by `RegisterResource`, even if (especially if) there are
|
|
// outputs from a prior deployment and the component's inputs have not changed.
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
urn, _, state, err := monitor.RegisterResource("component", "resA", false)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{}, state)
|
|
|
|
err = monitor.RegisterResourceOutputs(urn, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 1),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test calling a method.
|
|
func TestSingleComponentMethodDefaultProviderLifecycle(t *testing.T) {
|
|
var urn resource.URN
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(monitor *deploytest.ResourceMonitor,
|
|
typ, name string, parent resource.URN, inputs resource.PropertyMap,
|
|
options plugin.ConstructOptions) (plugin.ConstructResult, error) {
|
|
|
|
var err error
|
|
urn, _, _, err = monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{
|
|
Parent: parent,
|
|
Aliases: options.Aliases,
|
|
Protect: options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
err = monitor.RegisterResourceOutputs(urn, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{
|
|
URN: urn,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
call := func(monitor *deploytest.ResourceMonitor, tok tokens.ModuleMember, args resource.PropertyMap,
|
|
info plugin.CallInfo, options plugin.CallOptions) (plugin.CallResult, error) {
|
|
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"name": resource.NewStringProperty("Alice"),
|
|
}, args)
|
|
name := args["name"].StringValue()
|
|
|
|
result, _, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{
|
|
"urn": resource.NewStringProperty(string(urn)),
|
|
}, "", "")
|
|
assert.NoError(t, err)
|
|
state := result["state"]
|
|
foo := state.ObjectValue()["foo"].StringValue()
|
|
|
|
message := fmt.Sprintf("%s, %s!", name, foo)
|
|
return plugin.CallResult{
|
|
Return: resource.PropertyMap{
|
|
"message": resource.NewStringProperty(message),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
CallF: call,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
}, state)
|
|
|
|
outs, _, _, err := monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{
|
|
"name": resource.NewStringProperty("Alice"),
|
|
}, "", "")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"message": resource.NewStringProperty("Alice, bar!"),
|
|
}, outs)
|
|
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 4),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test creating a resource from a method.
|
|
func TestSingleComponentMethodResourceDefaultProviderLifecycle(t *testing.T) {
|
|
var urn resource.URN
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(monitor *deploytest.ResourceMonitor,
|
|
typ, name string, parent resource.URN, inputs resource.PropertyMap,
|
|
options plugin.ConstructOptions) (plugin.ConstructResult, error) {
|
|
|
|
var err error
|
|
urn, _, _, err = monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{
|
|
Parent: parent,
|
|
Aliases: options.Aliases,
|
|
Protect: options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
err = monitor.RegisterResourceOutputs(urn, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{
|
|
URN: urn,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
call := func(monitor *deploytest.ResourceMonitor, tok tokens.ModuleMember, args resource.PropertyMap,
|
|
info plugin.CallInfo, options plugin.CallOptions) (plugin.CallResult, error) {
|
|
|
|
_, _, _, err := monitor.RegisterResource("pkgA:m:typC", "resA", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.CallResult{}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
CallF: call,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
}, state)
|
|
|
|
_, _, _, err = monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{}, "", "")
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
|
|
p := &TestPlan{
|
|
Options: UpdateOptions{Host: host},
|
|
Steps: MakeBasicLifecycleSteps(t, 4),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// This tests a scenario involving two remote components with interdependencies that are only represented in the
|
|
// user program.
|
|
func TestComponentDeleteDependencies(t *testing.T) {
|
|
|
|
var (
|
|
firstURN resource.URN
|
|
nestedURN resource.URN
|
|
sgURN resource.URN
|
|
secondURN resource.URN
|
|
ruleURN resource.URN
|
|
|
|
err error
|
|
)
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ConstructF: func(monitor *deploytest.ResourceMonitor, typ, name string, parent resource.URN,
|
|
inputs resource.PropertyMap, options plugin.ConstructOptions) (plugin.ConstructResult, error) {
|
|
|
|
switch typ {
|
|
case "pkgB:m:first":
|
|
firstURN, _, _, err = monitor.RegisterResource("pkgB:m:first", name, false)
|
|
require.NoError(t, err)
|
|
|
|
nestedURN, _, _, err = monitor.RegisterResource("nested", "nested", false,
|
|
deploytest.ResourceOptions{
|
|
Parent: firstURN,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
sgURN, _, _, err = monitor.RegisterResource("pkgA:m:sg", "sg", true, deploytest.ResourceOptions{
|
|
Parent: nestedURN,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterResourceOutputs(nestedURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterResourceOutputs(firstURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{URN: firstURN}, nil
|
|
case "pkgB:m:second":
|
|
secondURN, _, _, err = monitor.RegisterResource("pkgB:m:second", name, false,
|
|
deploytest.ResourceOptions{
|
|
Dependencies: options.Dependencies,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ruleURN, _, _, err = monitor.RegisterResource("pkgA:m:rule", "rule", true,
|
|
deploytest.ResourceOptions{
|
|
Parent: secondURN,
|
|
Dependencies: options.PropertyDependencies["sgID"],
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterResourceOutputs(secondURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResult{URN: secondURN}, nil
|
|
default:
|
|
return plugin.ConstructResult{}, fmt.Errorf("unexpected type %v", typ)
|
|
}
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, _, err = monitor.RegisterResource("pkgB:m:first", "first", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, _, _, err = monitor.RegisterResource("pkgB:m:second", "second", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
PropertyDeps: map[resource.PropertyKey][]resource.URN{
|
|
"sgID": {sgURN},
|
|
},
|
|
Dependencies: []resource.URN{firstURN},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
|
|
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
|
p := &TestPlan{Options: UpdateOptions{Host: host}}
|
|
|
|
p.Steps = []TestStep{
|
|
{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
},
|
|
{
|
|
Op: Destroy,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, res result.Result) result.Result {
|
|
assert.Nil(t, res)
|
|
|
|
firstIndex, nestedIndex, sgIndex, secondIndex, ruleIndex := -1, -1, -1, -1, -1
|
|
|
|
for i, entry := range entries {
|
|
switch urn := entry.Step.URN(); urn {
|
|
case firstURN:
|
|
firstIndex = i
|
|
case nestedURN:
|
|
nestedIndex = i
|
|
case sgURN:
|
|
sgIndex = i
|
|
case secondURN:
|
|
secondIndex = i
|
|
case ruleURN:
|
|
ruleIndex = i
|
|
}
|
|
}
|
|
|
|
assert.Less(t, ruleIndex, sgIndex)
|
|
assert.Less(t, ruleIndex, secondIndex)
|
|
assert.Less(t, secondIndex, firstIndex)
|
|
assert.Less(t, secondIndex, sgIndex)
|
|
assert.Less(t, sgIndex, nestedIndex)
|
|
assert.Less(t, nestedIndex, firstIndex)
|
|
|
|
return res
|
|
},
|
|
},
|
|
}
|
|
p.Run(t, nil)
|
|
}
|