2042 lines
64 KiB
Go
2042 lines
64 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"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/blang/semver"
|
|
pbempty "github.com/golang/protobuf/ptypes/empty"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"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 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, errors.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})
|
|
}
|
|
|
|
// 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 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, errors.Wrapf(err, "could not connect to resource monitor")
|
|
}
|
|
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)
|
|
}
|