mirror of https://github.com/pulumi/pulumi.git
5583 lines
180 KiB
Go
5583 lines
180 KiB
Go
// Copyright 2016-2024, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package lifecycletest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/blang/semver"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/display"
|
|
"github.com/pulumi/pulumi/pkg/v3/engine"
|
|
. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
|
|
lt "github.com/pulumi/pulumi/pkg/v3/engine/lifecycletest/framework"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/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/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 display.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 ExpectDiagMessage(t *testing.T, messagePattern string) lt.ValidateFunc {
|
|
validate := func(
|
|
project workspace.Project, target deploy.Target,
|
|
entries JournalEntries, events []Event,
|
|
err error,
|
|
) error {
|
|
assert.Error(t, err)
|
|
|
|
for i := range events {
|
|
if events[i].Type == "diag" {
|
|
payload := events[i].Payload().(engine.DiagEventPayload)
|
|
match, err := regexp.MatchString(messagePattern, payload.Message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if match {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Unexpected diag message: %s", payload.Message)
|
|
}
|
|
}
|
|
return errors.New("Expected a diagnostic message, got none")
|
|
}
|
|
return validate
|
|
}
|
|
|
|
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-plugins", false, "enable or disable gRPC providers by default")
|
|
|
|
flag.Parse()
|
|
|
|
if *grpcDefault {
|
|
deploytest.UseGrpcPluginsByDefault = true
|
|
}
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestEmptyProgramLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 0),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestSingleResourceDiffUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{}, plugin.DiffUnavailable("diff unavailable")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// Now run a preview. Expect a warning because the diff is unavailable.
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == DiagEvent {
|
|
p := e.Payload().(DiagEventPayload)
|
|
if p.URN == resURN && p.Severity == diag.Warning && p.Message == "<{%reset%}>diff unavailable<{%reset%}>\n" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return err
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// 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) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CheckF: func(
|
|
_ context.Context,
|
|
req plugin.CheckRequest,
|
|
) (plugin.CheckResponse, error) {
|
|
return plugin.CheckResponse{}, errors.New("oh no, check had an error")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
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 err
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test that checks that we emit diagnostics for properties that check says are invalid.
|
|
func TestCheckFailureInvalidPropertyRecord(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CheckF: func(
|
|
_ context.Context,
|
|
req plugin.CheckRequest,
|
|
) (plugin.CheckResponse, error) {
|
|
return plugin.CheckResponse{
|
|
Failures: []plugin.CheckFailure{{
|
|
Property: "someprop",
|
|
Reason: "field is not valid",
|
|
}},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
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 err
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Tests that errors returned directly from the language host get logged by the engine.
|
|
func TestLanguageHostDiagnostics(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
errorText := "oh no"
|
|
programF := deploytest.NewLanguageRuntimeF(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)
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.Error(t, err)
|
|
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 err
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
type brokenDecrypter struct {
|
|
ErrorMessage string
|
|
}
|
|
|
|
func (b brokenDecrypter) DecryptValue(_ context.Context, _ string) (string, error) {
|
|
return "", errors.New(b.ErrorMessage)
|
|
}
|
|
|
|
func (b brokenDecrypter) BulkDecrypt(_ context.Context, _ []string) (map[string]string, error) {
|
|
return nil, errors.New(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) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error {
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
key := config.MustMakeKey("foo", "bar")
|
|
msg := "decryption failed"
|
|
configMap := make(config.Map)
|
|
configMap[key] = config.NewSecureValue("hunter2")
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Decrypter: brokenDecrypter{ErrorMessage: msg},
|
|
Config: configMap,
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.Error(t, err)
|
|
decryptErr := err.(DecryptError)
|
|
assert.Equal(t, key, decryptErr.Key)
|
|
assert.ErrorContains(t, decryptErr.Err, msg)
|
|
return err
|
|
},
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestConfigPropertyMapMatches(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
// Check that the config property map matches what we expect.
|
|
assert.Equal(t, 8, len(info.Config))
|
|
assert.Equal(t, 8, len(info.ConfigPropertyMap))
|
|
|
|
assert.Equal(t, "hunter2", info.Config[config.MustMakeKey("pkgA", "secret")])
|
|
assert.True(t, info.ConfigPropertyMap["pkgA:secret"].IsSecret())
|
|
assert.Equal(t, "hunter2", info.ConfigPropertyMap["pkgA:secret"].SecretValue().Element.StringValue())
|
|
|
|
assert.Equal(t, "all I see is ******", info.Config[config.MustMakeKey("pkgA", "plain")])
|
|
assert.False(t, info.ConfigPropertyMap["pkgA:plain"].IsSecret())
|
|
assert.Equal(t, "all I see is ******", info.ConfigPropertyMap["pkgA:plain"].StringValue())
|
|
|
|
assert.Equal(t, "1234", info.Config[config.MustMakeKey("pkgA", "int")])
|
|
assert.Equal(t, 1234.0, info.ConfigPropertyMap["pkgA:int"].NumberValue())
|
|
|
|
assert.Equal(t, "12.34", info.Config[config.MustMakeKey("pkgA", "float")])
|
|
// This is a string because adjustObjectValue only parses integers, not floats.
|
|
assert.Equal(t, "12.34", info.ConfigPropertyMap["pkgA:float"].StringValue())
|
|
|
|
assert.Equal(t, "012345", info.Config[config.MustMakeKey("pkgA", "string")])
|
|
assert.Equal(t, "012345", info.ConfigPropertyMap["pkgA:string"].StringValue())
|
|
|
|
assert.Equal(t, "true", info.Config[config.MustMakeKey("pkgA", "bool")])
|
|
assert.Equal(t, true, info.ConfigPropertyMap["pkgA:bool"].BoolValue())
|
|
|
|
assert.Equal(t, "[1,2,3]", info.Config[config.MustMakeKey("pkgA", "array")])
|
|
assert.Equal(t, 1.0, info.ConfigPropertyMap["pkgA:array"].ArrayValue()[0].NumberValue())
|
|
assert.Equal(t, 2.0, info.ConfigPropertyMap["pkgA:array"].ArrayValue()[1].NumberValue())
|
|
assert.Equal(t, 3.0, info.ConfigPropertyMap["pkgA:array"].ArrayValue()[2].NumberValue())
|
|
|
|
assert.Equal(t, `{"bar":"02","foo":1}`, info.Config[config.MustMakeKey("pkgA", "map")])
|
|
assert.Equal(t, 1.0, info.ConfigPropertyMap["pkgA:map"].ObjectValue()["foo"].NumberValue())
|
|
assert.Equal(t, "02", info.ConfigPropertyMap["pkgA:map"].ObjectValue()["bar"].StringValue())
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF)
|
|
|
|
crypter := config.NewSymmetricCrypter(make([]byte, 32))
|
|
secret, err := crypter.EncryptValue(context.Background(), "hunter2")
|
|
assert.NoError(t, err)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 0),
|
|
Config: config.Map{
|
|
config.MustMakeKey("pkgA", "secret"): config.NewSecureValue(secret),
|
|
config.MustMakeKey("pkgA", "plain"): config.NewValue("all I see is ******"),
|
|
config.MustMakeKey("pkgA", "int"): config.NewValue("1234"),
|
|
config.MustMakeKey("pkgA", "float"): config.NewValue("12.34"),
|
|
config.MustMakeKey("pkgA", "string"): config.NewValue("012345"),
|
|
config.MustMakeKey("pkgA", "bool"): config.NewValue("true"),
|
|
config.MustMakeKey("pkgA", "array"): config.NewObjectValue("[1, 2, 3]"),
|
|
config.MustMakeKey("pkgA", "map"): config.NewObjectValue(`{"foo": 1, "bar": "02"}`),
|
|
},
|
|
Decrypter: crypter,
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestBadResourceType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
}},
|
|
}
|
|
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Tests that provider cancellation occurs as expected.
|
|
func TestProviderCancellation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
// Inform the waiter that we've entered a provider op and wait for cancellation.
|
|
ops.Done()
|
|
<-provCtx.Done()
|
|
|
|
return plugin.CreateResponse{
|
|
ID: resource.ID(req.URN.Name()),
|
|
Properties: resource.PropertyMap{},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
CancelF: func() error {
|
|
provCancel()
|
|
return nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
done := make(chan bool)
|
|
programF := deploytest.NewLanguageRuntimeF(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 := lt.TestOp(Update)
|
|
options := lt.TestUpdateOptions{
|
|
T: t,
|
|
|
|
HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...),
|
|
UpdateOptions: UpdateOptions{
|
|
Parallel: resourceCount,
|
|
},
|
|
}
|
|
project, target := p.GetProject(), p.GetTarget(t, nil)
|
|
|
|
_, err := op.RunWithContext(ctx, project, target, options, false, nil, nil)
|
|
assert.Error(t, err)
|
|
|
|
// Wait for the program to finish.
|
|
<-done
|
|
}
|
|
|
|
// Tests that a preview works for a stack with pending operations.
|
|
func TestPreviewWithPendingOperations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
|
|
op := lt.TestOp(Update)
|
|
|
|
options := lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
project, target := p.GetProject(), p.GetTarget(t, old)
|
|
|
|
// A preview should succeed despite the pending operations.
|
|
_, err := op.Run(project, target, options, true, nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Tests that a refresh works for a stack with pending operations.
|
|
func TestRefreshWithPendingOperations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
|
|
op := lt.TestOp(Update)
|
|
options := lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
project, target := p.GetProject(), p.GetTarget(t, old)
|
|
|
|
// With a refresh, the update should succeed.
|
|
withRefresh := options
|
|
withRefresh.Refresh = true
|
|
new, err := op.RunStep(project, target, withRefresh, false, nil, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, new.PendingOperations, 0)
|
|
|
|
// Similarly, the update should succeed if performed after a separate refresh.
|
|
new, err = lt.TestOp(Refresh).RunStep(project, target, options, false, nil, nil, "1")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, new.PendingOperations, 0)
|
|
|
|
_, err = op.RunStep(project, p.GetTarget(t, new), options, false, nil, nil, "2")
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Test to make sure that if we pulumi refresh
|
|
// while having pending CREATE operations,
|
|
// that these are preserved after the refresh.
|
|
func TestRefreshPreservesPendingCreateOperations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := <.TestPlan{}
|
|
|
|
const resType = "pkgA:m:typA"
|
|
urnA := p.NewURN(resType, "resA", "")
|
|
urnB := p.NewURN(resType, "resB", "")
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
// Notice here, we have two pending operations: update and create
|
|
// After a refresh, only the pending CREATE operation should
|
|
// be in the updated snapshot
|
|
resA := newResource(urnA, "0", false)
|
|
resB := newResource(urnB, "0", false)
|
|
old := &deploy.Snapshot{
|
|
PendingOperations: []resource.Operation{
|
|
{
|
|
Resource: resA,
|
|
Type: resource.OperationTypeUpdating,
|
|
},
|
|
{
|
|
Resource: resB,
|
|
Type: resource.OperationTypeCreating,
|
|
},
|
|
},
|
|
Resources: []*resource.State{
|
|
resA,
|
|
},
|
|
}
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
|
|
op := lt.TestOp(Update)
|
|
options := lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
project, target := p.GetProject(), p.GetTarget(t, old)
|
|
|
|
// With a refresh, the update should succeed.
|
|
withRefresh := options
|
|
withRefresh.Refresh = true
|
|
new, err := op.Run(project, target, withRefresh, false, nil, nil)
|
|
assert.NoError(t, err)
|
|
// Assert that pending CREATE operation was preserved
|
|
assert.Len(t, new.PendingOperations, 1)
|
|
assert.Equal(t, resource.OperationTypeCreating, new.PendingOperations[0].Type)
|
|
assert.Equal(t, urnB, new.PendingOperations[0].Resource.URN)
|
|
}
|
|
|
|
func findPendingOperationsByType(opType resource.OperationType, snapshot *deploy.Snapshot) []resource.Operation {
|
|
var operations []resource.Operation
|
|
for _, operation := range snapshot.PendingOperations {
|
|
if operation.Type == opType {
|
|
operations = append(operations, operation)
|
|
}
|
|
}
|
|
return operations
|
|
}
|
|
|
|
// Update succeeds but gives a warning when there are pending operations
|
|
func TestUpdateShowsWarningWithPendingOperations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := <.TestPlan{}
|
|
|
|
const resType = "pkgA:m:typA"
|
|
urnA := p.NewURN(resType, "resA", "")
|
|
urnB := p.NewURN(resType, "resB", "")
|
|
|
|
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,
|
|
},
|
|
{
|
|
Resource: newResource(urnB, "1", false),
|
|
Type: resource.OperationTypeCreating,
|
|
},
|
|
},
|
|
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
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
|
|
op := lt.TestOp(Update)
|
|
options := lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
project, target := p.GetProject(), p.GetTarget(t, old)
|
|
|
|
// The update should succeed but give a warning
|
|
initialPartOfMessage := "Attempting to deploy or update resources with 1 pending operations from previous deployment."
|
|
validate := func(
|
|
project workspace.Project, target deploy.Target,
|
|
entries JournalEntries, events []Event,
|
|
err error,
|
|
) error {
|
|
for i := range events {
|
|
if events[i].Type == "diag" {
|
|
payload := events[i].Payload().(engine.DiagEventPayload)
|
|
|
|
if payload.Severity == "warning" && strings.Contains(payload.Message, initialPartOfMessage) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Unexpected warning diag message: %s", payload.Message)
|
|
}
|
|
}
|
|
return errors.New("Expected a diagnostic message, got none")
|
|
}
|
|
|
|
new, _ := op.Run(project, target, options, false, nil, validate)
|
|
assert.NotNil(t, new)
|
|
|
|
assert.Equal(t, resource.OperationTypeCreating, new.PendingOperations[0].Type)
|
|
|
|
// Assert that CREATE pending operations are retained
|
|
// TODO: should revisit whether non-CREATE pending operations should also be retained
|
|
assert.Equal(t, 1, len(new.PendingOperations))
|
|
createOperations := findPendingOperationsByType(resource.OperationTypeCreating, new)
|
|
assert.Equal(t, 1, len(createOperations))
|
|
assert.Equal(t, urnB, createOperations[0].Resource.URN)
|
|
}
|
|
|
|
// Tests that a failed partial update causes the engine to persist the resource's old inputs and new outputs.
|
|
func TestUpdatePartialFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{Changes: plugin.DiffSome}, nil
|
|
},
|
|
|
|
UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
outputs := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"output_prop": 42,
|
|
})
|
|
|
|
return plugin.UpdateResponse{
|
|
Properties: outputs,
|
|
Status: resource.StatusPartialFailure,
|
|
}, errors.New("update failed to apply")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{Options: lt.TestUpdateOptions{T: t, HostF: hostF}}
|
|
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
p.Steps = []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.Error(t, err)
|
|
for _, entry := range entries {
|
|
switch urn := entry.Step.URN(); urn {
|
|
case resURN:
|
|
assert.Equal(t, deploy.OpUpdate, entry.Step.Op())
|
|
//nolint:exhaustive // default case signifies testing failure
|
|
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 err
|
|
},
|
|
}}
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{}
|
|
|
|
// Test that the normal lifecycle works correctly.
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, state, err := mon.ReadResource("pulumi:pulumi:StackReference", "other", "other", "",
|
|
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: lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)},
|
|
Steps: lt.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",
|
|
External: true,
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other2",
|
|
}),
|
|
Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other2",
|
|
"outputs": resource.PropertyMap{},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
p.Steps = []lt.TestStep{{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
for _, entry := range entries {
|
|
switch urn := entry.Step.URN(); urn {
|
|
case resURN:
|
|
switch entry.Step.Op() {
|
|
case deploy.OpRead:
|
|
// OK
|
|
default:
|
|
t.Fatalf("unexpected journal operation: %v", entry.Step.Op())
|
|
}
|
|
}
|
|
}
|
|
|
|
return err
|
|
},
|
|
}}
|
|
p.Options.SkipDisplayTests = true
|
|
p.Run(t, old)
|
|
p.Options.SkipDisplayTests = false
|
|
|
|
// Test that unknown stacks are handled appropriately.
|
|
programF = deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, err := mon.ReadResource("pulumi:pulumi:StackReference", "other", "other", "",
|
|
resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "rehto",
|
|
}), "", "", "", "")
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
p.Options = lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
p.Steps = []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
}}
|
|
p.Run(t, nil)
|
|
|
|
// Test that unknown properties cause errors.
|
|
programF = deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
_, _, err := mon.ReadResource("pulumi:pulumi:StackReference", "other", "other", "",
|
|
resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"name": "other",
|
|
"foo": "bar",
|
|
}), "", "", "", "")
|
|
assert.Error(t, err)
|
|
return err
|
|
})
|
|
p.Options = lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Tests that registering (rather than reading) a StackReference resource works as intended, but warns the user that
|
|
// it's deprecated.
|
|
func TestStackReferenceRegister(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{}
|
|
|
|
// Test that the normal lifecycle works correctly.
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error {
|
|
resp, 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", resp.Outputs["outputs"].ObjectValue()["foo"].StringValue())
|
|
}
|
|
return nil
|
|
})
|
|
|
|
steps := lt.MakeBasicLifecycleSteps(t, 2)
|
|
// Add an extra validate stage to each step to check we get the diagnostic that this use of stack reference is
|
|
// obsolete if a stack resource was registered.
|
|
for i := range steps {
|
|
v := steps[i].Validate
|
|
steps[i].Validate = func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
// Check if we registered a stack reference resource (i.e. same/update/create). Ideally we'd warn on refresh
|
|
// as well but that's just a Read so it's hard to tell in the built-in provider if that's a Read for a
|
|
// ReadResource or a Read for a refresh, so we don't worry about that case.
|
|
registered := false
|
|
for _, entry := range entries {
|
|
if entry.Step.URN().Type() == "pulumi:pulumi:StackReference" &&
|
|
(entry.Step.Op() == deploy.OpCreate ||
|
|
entry.Step.Op() == deploy.OpUpdate ||
|
|
entry.Step.Op() == deploy.OpSame) {
|
|
registered = true
|
|
}
|
|
}
|
|
|
|
if registered {
|
|
found := false
|
|
for _, evt := range evts {
|
|
if evt.Type == DiagEvent {
|
|
payload := evt.Payload().(DiagEventPayload)
|
|
|
|
ok := payload.Severity == "warning" &&
|
|
payload.URN.Type() == "pulumi:pulumi:StackReference" &&
|
|
strings.Contains(
|
|
payload.Message,
|
|
"The \"pulumi:pulumi:StackReference\" resource type is deprecated.")
|
|
found = found || ok
|
|
}
|
|
}
|
|
assert.True(t, found, "diagnostic warning not found in: %+v", evts)
|
|
}
|
|
|
|
return v(project, target, entries, evts, err)
|
|
}
|
|
}
|
|
|
|
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: lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)},
|
|
Steps: steps,
|
|
}
|
|
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 = []lt.TestStep{{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
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 err
|
|
},
|
|
}}
|
|
p.Run(t, old)
|
|
|
|
// Test that unknown stacks are handled appropriately.
|
|
programF = deploytest.NewLanguageRuntimeF(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 = lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, loaders...)}
|
|
p.Steps = []lt.TestStep{{
|
|
Op: Update,
|
|
ExpectFailure: true,
|
|
SkipPreview: true,
|
|
}}
|
|
p.Run(t, nil)
|
|
|
|
// Test that unknown properties cause errors.
|
|
programF = deploytest.NewLanguageRuntimeF(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 = lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(nil, nil, programF, 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) {
|
|
t.Parallel()
|
|
|
|
// 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.
|
|
//
|
|
// The engine architecture has changed since this issue was discovered, and the test has been updated to
|
|
// reflect that. Registry creation no longer configures providers up front, so the program below tries to
|
|
// register two providers instead.
|
|
|
|
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(context.Context, plugin.ConfigureRequest) (plugin.ConfigureResponse, error) {
|
|
go func() {
|
|
<-release
|
|
host.Log(diag.Info, "", "configuring pkgA provider...", 0)
|
|
close(done)
|
|
}()
|
|
return plugin.ConfigureResponse{}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return nil, errors.New("pkgB load failure")
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource(providers.MakeProviderType("pkgB"), "provB", true)
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
|
|
op := lt.TestOp(Update)
|
|
sink := diag.DefaultSink(sinkWriter, sinkWriter, diag.FormatOptions{Color: colors.Raw})
|
|
options := lt.TestUpdateOptions{T: t, HostF: deploytest.NewPluginHostF(sink, sink, programF, loaders...)}
|
|
p := <.TestPlan{}
|
|
project, target := p.GetProject(), p.GetTarget(t, nil)
|
|
|
|
_, err := op.Run(project, target, options, true, nil, nil)
|
|
assert.Error(t, err)
|
|
|
|
close(sinkWriter.channel)
|
|
close(release)
|
|
<-done
|
|
}
|
|
|
|
func TestSingleResourceIgnoreChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var expectedIgnoreChanges []string
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
assert.Equal(t, expectedIgnoreChanges, req.IgnoreChanges)
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
UpdateF: func(_ context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
assert.Equal(t, expectedIgnoreChanges, req.IgnoreChanges)
|
|
return plugin.UpdateResponse{}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, ignoreChanges []string,
|
|
allowedOps []display.StepOp, name string,
|
|
) *deploy.Snapshot {
|
|
expectedIgnoreChanges = ignoreChanges
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
// Skip display tests because secrets are serialized with the blinding crypter and can't be restored
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
for _, event := range events {
|
|
if event.Type == ResourcePreEvent {
|
|
payload := event.Payload().(ResourcePreEventPayload)
|
|
if payload.Metadata.URN == "urn:pulumi:test::test::pkgA:m:typA::resA" {
|
|
assert.Subset(t,
|
|
allowedOps, []display.StepOp{payload.Metadata.Op},
|
|
"event operation unexpected: %v", payload)
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return p.RunWithName(t, snap, name)
|
|
}
|
|
|
|
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 1,
|
|
"b": map[string]interface{}{
|
|
"c": "foo",
|
|
},
|
|
"d": []interface{}{1},
|
|
"e": []interface{}{1},
|
|
"f": map[string]interface{}{
|
|
"g": map[string]interface{}{
|
|
"h": "bar",
|
|
},
|
|
},
|
|
}), []string{"a", "b.c", "d", "e[0]", "f.g[\"h\"]"}, []display.StepOp{deploy.OpCreate}, "initial")
|
|
|
|
// 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",
|
|
},
|
|
"d": []interface{}{2},
|
|
"e": []interface{}{2},
|
|
"f": map[string]interface{}{
|
|
"g": map[string]interface{}{
|
|
"h": "baz",
|
|
},
|
|
},
|
|
}), []string{"a", "b.c", "d", "e[0]", "f.g[\"h\"]"}, []display.StepOp{deploy.OpSame}, "ignored-property")
|
|
|
|
// 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",
|
|
},
|
|
"d": []interface{}{3},
|
|
"e": []interface{}{3},
|
|
"f": map[string]interface{}{
|
|
"g": map[string]interface{}{
|
|
"h": "qux",
|
|
},
|
|
},
|
|
}), nil, []display.StepOp{deploy.OpUpdate}, "unignored-property")
|
|
|
|
// Ensure that a removing an ignored property results in an OpSame
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"e": []interface{}{},
|
|
}), []string{"a", "b", "d", "e", "f"}, []display.StepOp{deploy.OpSame}, "ignored-property-removed")
|
|
|
|
// Ensure that a removing an un-ignored property results in an OpUpdate
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"e": []interface{}{},
|
|
}), nil, []display.StepOp{deploy.OpUpdate}, "unignored-property-removed")
|
|
|
|
// 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",
|
|
},
|
|
"d": []interface{}{4},
|
|
"e": []interface{}{},
|
|
}), []string{"a", "b", "d", "e[0]", "f"}, []display.StepOp{deploy.OpSame}, "ignored-property-added")
|
|
|
|
// Ensure that adding an un-ignored property results in an OpUpdate
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"e": []interface{}{},
|
|
"i": 4,
|
|
}), []string{"a", "b", "d", "e", "f"}, []display.StepOp{deploy.OpUpdate}, "unignored-property-added")
|
|
|
|
// Ensure that sub-elements of arrays can be ignored, first reset to a simple state
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 3,
|
|
"b": []string{"foo", "bar"},
|
|
}), nil, []display.StepOp{deploy.OpUpdate}, "sub-elements-ignored")
|
|
|
|
// Check that ignoring a specific sub-element of an array works
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 3,
|
|
"b": []string{"foo", "baz"},
|
|
}), []string{"b[1]"}, []display.StepOp{deploy.OpSame}, "ignore-specific-subelement")
|
|
|
|
// Check that ignoring all sub-elements of an array works
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 3,
|
|
"b": []string{"foo", "baz"},
|
|
}), []string{"b[*]"}, []display.StepOp{deploy.OpSame}, "ignore-all-subelements")
|
|
|
|
// Check that ignoring a secret value works, first update to make foo, bar secret
|
|
snap = updateProgramWithProps(snap, resource.PropertyMap{
|
|
"a": resource.NewNumberProperty(3),
|
|
"b": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewStringProperty("foo"),
|
|
resource.NewStringProperty("bar"),
|
|
})),
|
|
}, nil, []display.StepOp{deploy.OpUpdate}, "ignore-secret")
|
|
|
|
// Now check that changing a value (but not secretness) can be ignored
|
|
_ = updateProgramWithProps(snap, resource.PropertyMap{
|
|
"a": resource.NewNumberProperty(3),
|
|
"b": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewStringProperty("foo"),
|
|
resource.NewStringProperty("baz"),
|
|
})),
|
|
}, []string{"b[1]"}, []display.StepOp{deploy.OpSame}, "change-value-not-secretness")
|
|
}
|
|
|
|
func TestIgnoreChangesInvalidPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
program := func(monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewObjectProperty(resource.PropertyMap{
|
|
"bar": resource.NewStringProperty("baz"),
|
|
}),
|
|
"qux": resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewStringProperty("zed"),
|
|
}),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
}
|
|
|
|
runtimeF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
return program(monitor)
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, runtimeF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
|
|
program = func(monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{},
|
|
IgnoreChanges: []string{"foo.bar"},
|
|
})
|
|
assert.Error(t, err)
|
|
return nil
|
|
}
|
|
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.Error(t, err)
|
|
|
|
program = func(monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"qux": resource.NewArrayProperty([]resource.PropertyValue{}),
|
|
},
|
|
IgnoreChanges: []string{"qux[0]"},
|
|
})
|
|
assert.Error(t, err)
|
|
return nil
|
|
}
|
|
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.Error(t, err)
|
|
|
|
program = func(monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{},
|
|
IgnoreChanges: []string{"qux[0]"},
|
|
})
|
|
assert.Error(t, err)
|
|
return nil
|
|
}
|
|
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "3")
|
|
assert.Error(t, err)
|
|
|
|
program = func(monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"qux": resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewStringProperty("zed"),
|
|
resource.NewStringProperty("zob"),
|
|
}),
|
|
},
|
|
IgnoreChanges: []string{"qux[1]"},
|
|
})
|
|
assert.Error(t, err)
|
|
return nil
|
|
}
|
|
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "4")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
type DiffFunc = func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error)
|
|
|
|
func replaceOnChangesTest(t *testing.T, name string, diffFunc DiffFunc) {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: diffFunc,
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: resource.ID("id123"),
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string,
|
|
allowedOps []display.StepOp,
|
|
) *deploy.Snapshot {
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
for _, event := range events {
|
|
if event.Type == ResourcePreEvent {
|
|
payload := event.Payload().(ResourcePreEventPayload)
|
|
// Ignore any events for default providers
|
|
if !payload.Internal {
|
|
assert.Subset(t, allowedOps, []display.StepOp{payload.Metadata.Op})
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return p.RunWithName(t, snap, strings.ReplaceAll(fmt.Sprintf("%v", props), " ", "_"))
|
|
}
|
|
|
|
snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 1,
|
|
"b": map[string]interface{}{
|
|
"c": "foo",
|
|
},
|
|
}), []string{"a", "b.c"}, []display.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"}, []display.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"}, []display.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{"*"}, []display.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, []display.StepOp{deploy.OpUpdate})
|
|
|
|
// We ensure that we are listing to the engine diff function only when the provider function
|
|
// is nil. We do this by adding some weirdness to the provider diff function.
|
|
allowed := []display.StepOp{deploy.OpCreateReplacement, deploy.OpReplace, deploy.OpDeleteReplaced}
|
|
if diffFunc != nil {
|
|
allowed = []display.StepOp{deploy.OpSame}
|
|
}
|
|
snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"a": 42, // 42 is a special value in the "provider" diff function.
|
|
"b": map[string]interface{}{
|
|
"c": "qux",
|
|
},
|
|
}), []string{"a"}, allowed)
|
|
|
|
_ = snap
|
|
})
|
|
}
|
|
|
|
func TestReplaceOnChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We simulate a provider that has it's own diff function.
|
|
replaceOnChangesTest(t, "provider diff",
|
|
func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
// To establish a observable difference between the provider and engine diff function,
|
|
// we treat 42 as an OpSame. We use this to check that the right diff function is being
|
|
// used.
|
|
for k, v := range req.NewInputs {
|
|
if v == resource.NewNumberProperty(42) {
|
|
req.NewInputs[k] = req.OldOutputs[k]
|
|
}
|
|
}
|
|
diff := req.OldOutputs.Diff(req.NewInputs)
|
|
if diff == nil {
|
|
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
|
|
}
|
|
detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff, false)
|
|
changedKeys := diff.ChangedKeys()
|
|
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ChangedKeys: changedKeys,
|
|
DetailedDiff: detailedDiff,
|
|
}, nil
|
|
})
|
|
|
|
// We simulate a provider that does not have it's own diff function. This tests the engines diff
|
|
// function instead.
|
|
replaceOnChangesTest(t, "engine diff", nil)
|
|
}
|
|
|
|
func TestPersistentDiff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{Changes: plugin.DiffSome}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
|
|
// provider diffing.
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
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 err
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Next, enable legacy diff behavior. We should see no changes to the resource.
|
|
p.Options.UseLegacyDiff = true
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
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 err
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestDetailedDiffReplace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"prop": {Kind: plugin.DiffAddReplace},
|
|
},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{}
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// First, make no change to the inputs and run a preview. We should see an update to the resource due to
|
|
// provider diffing.
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
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 err
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCustomTimeouts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
p.Steps = []lt.TestStep{{Op: Update}}
|
|
snap := p.Run(t, nil)
|
|
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, snap.Resources[0].URN.Name(), "default")
|
|
assert.Equal(t, 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) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffConfigF: func(
|
|
_ context.Context,
|
|
req plugin.DiffConfigRequest,
|
|
) (plugin.DiffResult, error) {
|
|
// Always require replacement if any diff exists.
|
|
if !req.OldOutputs.DeepEquals(req.NewInputs) {
|
|
keys := []resource.PropertyKey{}
|
|
for k := range req.NewInputs {
|
|
keys = append(keys, k)
|
|
}
|
|
return plugin.DiffResult{Changes: plugin.DiffSome, ReplaceKeys: keys}, nil
|
|
}
|
|
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Config: config.Map{
|
|
config.MustMakeKey("pkgA", "foo"): config.NewValue("bar"),
|
|
},
|
|
}
|
|
|
|
// Build a basic lifecycle.
|
|
steps := lt.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 = []lt.TestStep{{
|
|
Op: Update,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
_ []Event, err error,
|
|
) error {
|
|
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 err
|
|
},
|
|
}}
|
|
p.Run(t, snap)
|
|
}
|
|
|
|
func TestMissingRead(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ReadF: func(context.Context, plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
// Our program reads a resource and exits.
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{{Op: Update, ExpectFailure: true}},
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestProviderPreview(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sawPreview := false
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, req.Preview, req.Properties.ContainsUnknowns())
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
UpdateF: func(_ context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, req.Preview, req.NewInputs.ContainsUnknowns())
|
|
return plugin.UpdateResponse{
|
|
Properties: req.NewInputs,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
preview := true
|
|
programF := deploytest.NewLanguageRuntimeF(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,
|
|
})
|
|
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, resp.Outputs.DeepEquals(ins))
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run a preview. The inputs should be propagated to the outputs by the provider during the create.
|
|
preview, sawPreview = true, false
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run another preview. The inputs should be propagated to the outputs during the update.
|
|
preview, sawPreview = true, false
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, sawPreview)
|
|
}
|
|
|
|
func TestProviderPreviewGrpc(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sawPreview := false
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, req.Preview, req.Properties.ContainsUnknowns())
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
UpdateF: func(_ context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, req.Preview, req.NewInputs.ContainsUnknowns())
|
|
return plugin.UpdateResponse{
|
|
Properties: req.NewInputs,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithGrpc),
|
|
}
|
|
|
|
preview := true
|
|
programF := deploytest.NewLanguageRuntimeF(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,
|
|
})
|
|
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, resp.Outputs.DeepEquals(ins))
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run a preview. The inputs should be propagated to the outputs by the provider during the create.
|
|
preview, sawPreview = true, false
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run another preview. The inputs should be propagated to the outputs during the update.
|
|
preview, sawPreview = true, false
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, sawPreview)
|
|
}
|
|
|
|
func TestProviderPreviewUnknowns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sawPreview := false
|
|
loaders := []*deploytest.ProviderLoader{
|
|
// NOTE: it is important that this test uses a gRPC-wrapped 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{
|
|
InvokeF: func(_ context.Context, req plugin.InvokeRequest) (plugin.InvokeResponse, error) {
|
|
name := req.Args["name"]
|
|
ret := "unexpected"
|
|
if name.IsString() {
|
|
ret = "Hello, " + name.StringValue() + "!"
|
|
}
|
|
|
|
return plugin.InvokeResponse{
|
|
Properties: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"message": ret,
|
|
}),
|
|
}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
UpdateF: func(_ context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
if req.Preview {
|
|
sawPreview = true
|
|
}
|
|
|
|
return plugin.UpdateResponse{
|
|
Properties: req.NewInputs,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
if req.Info.DryRun {
|
|
sawPreview = true
|
|
}
|
|
|
|
assert.Equal(t, []resource.URN{"urn:pulumi:test::test::pkgA:m:typB::resB"}, req.Options.ArgDependencies["name"])
|
|
|
|
ret := "unexpected"
|
|
if req.Args["name"].IsString() {
|
|
ret = "Hello, " + req.Args["name"].StringValue() + "!"
|
|
}
|
|
|
|
return plugin.CallResponse{
|
|
Return: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"message": ret,
|
|
}),
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
if req.Info.DryRun {
|
|
sawPreview = true
|
|
}
|
|
|
|
var err error
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{
|
|
Parent: req.Parent,
|
|
Aliases: aliasesFromAliases(req.Options.Aliases),
|
|
Protect: req.Options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", req.Name+"-resB", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": req.Inputs["name"]}
|
|
err = monitor.RegisterResourceOutputs(resp.URN, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: outs,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithGrpc),
|
|
}
|
|
|
|
preview := true
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")})
|
|
if !preview {
|
|
computed = "alpha"
|
|
}
|
|
|
|
resp, err := monitor.RegisterResource("pulumi:providers:pkgA", "provA", true,
|
|
deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{"foo": computed}),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
provID := resp.ID
|
|
if provID == "" {
|
|
provID = providers.UnknownID
|
|
}
|
|
|
|
provRef, err := providers.NewReference(resp.URN, provID)
|
|
assert.NoError(t, err)
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": map[string]interface{}{
|
|
"a": 42,
|
|
},
|
|
"qux": []interface{}{
|
|
24,
|
|
},
|
|
})
|
|
|
|
resp, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
Provider: provRef.String(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
if preview {
|
|
assert.True(t, resp.Outputs.DeepEquals(resource.PropertyMap{}))
|
|
} else {
|
|
assert.True(t, resp.Outputs.DeepEquals(ins))
|
|
}
|
|
|
|
respC, err := monitor.RegisterResource("pkgA:m:typB", "resB", false, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"name": resp.Outputs["foo"],
|
|
},
|
|
Remote: true,
|
|
Provider: provRef.String(),
|
|
})
|
|
if preview {
|
|
// We expect construction of remote component resources to fail during previews if the provider is
|
|
// configured with unknowns.
|
|
assert.NoError(t, err)
|
|
assert.True(t, respC.Outputs.DeepEquals(resource.PropertyMap{}))
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.True(t, respC.Outputs.DeepEquals(resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
}))
|
|
}
|
|
|
|
var outs resource.PropertyMap
|
|
if preview {
|
|
// We can't send any args or dependencies in preview because the RegisterResource call above failed.
|
|
outs, _, _, err = monitor.Call("pkgA:m:typA/methodA", nil, nil, provRef.String(), "", "")
|
|
assert.NoError(t, err)
|
|
} else {
|
|
outs, _, _, err = monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{
|
|
"name": respC.Outputs["foo"],
|
|
}, map[resource.PropertyKey][]resource.URN{
|
|
"name": {respC.URN},
|
|
}, provRef.String(), "", "")
|
|
assert.NoError(t, err)
|
|
}
|
|
if preview {
|
|
assert.True(t, outs.DeepEquals(resource.PropertyMap{}), "outs was %v", outs)
|
|
} else {
|
|
assert.True(t, outs.DeepEquals(resource.PropertyMap{
|
|
"message": resource.NewStringProperty("Hello, bar!"),
|
|
}), "outs was %v", outs)
|
|
}
|
|
|
|
if preview {
|
|
outs, _, err = monitor.Invoke("pkgA:m:invokeA", resource.PropertyMap{
|
|
"name": resource.PropertyValue{},
|
|
}, provRef.String(), "", "")
|
|
} else {
|
|
outs, _, err = monitor.Invoke("pkgA:m:invokeA", resource.PropertyMap{
|
|
"name": respC.Outputs["foo"],
|
|
}, provRef.String(), "", "")
|
|
}
|
|
assert.NoError(t, err)
|
|
if preview {
|
|
assert.True(t, outs.DeepEquals(resource.PropertyMap{}), "outs was %v", outs)
|
|
} else {
|
|
assert.True(t, outs.DeepEquals(resource.PropertyMap{
|
|
"message": resource.NewStringProperty("Hello, bar!"),
|
|
}), "outs was %v", outs)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
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
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, sawPreview)
|
|
|
|
// Run an update.
|
|
preview, sawPreview = false, false
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil)
|
|
require.NoError(t, err)
|
|
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
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil)
|
|
require.NoError(t, err)
|
|
assert.False(t, sawPreview)
|
|
}
|
|
|
|
func TestSingleComponentDefaultProviderLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{
|
|
Parent: req.Parent,
|
|
Aliases: aliasesFromAliases(req.Options.Aliases),
|
|
Protect: req.Options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
err = monitor.RegisterResourceOutputs(resp.URN, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, 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"),
|
|
}, resp.Outputs)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because different ordering makes the colouring different.
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 3),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
type updateContext struct {
|
|
pulumirpc.UnimplementedLanguageRuntimeServer
|
|
|
|
*deploytest.ResourceMonitor
|
|
|
|
resmon chan *deploytest.ResourceMonitor
|
|
programErr chan error
|
|
snap chan *deploy.Snapshot
|
|
updateResult chan error
|
|
}
|
|
|
|
func startUpdate(t *testing.T, hostF deploytest.PluginHostFactory) (*updateContext, error) {
|
|
ctx := &updateContext{
|
|
resmon: make(chan *deploytest.ResourceMonitor),
|
|
programErr: make(chan error),
|
|
snap: make(chan *deploy.Snapshot),
|
|
updateResult: make(chan error),
|
|
}
|
|
|
|
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: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Runtime: "client",
|
|
RuntimeOptions: map[string]interface{}{
|
|
"address": fmt.Sprintf("127.0.0.1:%d", port),
|
|
},
|
|
}
|
|
|
|
go func() {
|
|
snap, err := lt.TestOp(Update).Run(p.GetProject(), p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
ctx.snap <- snap
|
|
close(ctx.snap)
|
|
ctx.updateResult <- err
|
|
close(ctx.updateResult)
|
|
stop <- true
|
|
}()
|
|
|
|
ctx.ResourceMonitor = <-ctx.resmon
|
|
return ctx, nil
|
|
}
|
|
|
|
func (ctx *updateContext) Finish(err error) (*deploy.Snapshot, error) {
|
|
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.NewClient(
|
|
req.MonitorAddress,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
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 *emptypb.Empty) (*pulumirpc.PluginInfo, error) {
|
|
return &pulumirpc.PluginInfo{
|
|
Version: "1.0.0",
|
|
}, nil
|
|
}
|
|
|
|
func (ctx *updateContext) InstallDependencies(
|
|
req *pulumirpc.InstallDependenciesRequest,
|
|
server pulumirpc.LanguageRuntime_InstallDependenciesServer,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func TestLanguageClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
update, err := startUpdate(t, deploytest.NewPluginHostF(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, err := update.Finish(nil)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, snap.Resources, 2)
|
|
}
|
|
|
|
func TestSingleComponentGetResourceDefaultProviderLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{
|
|
Parent: req.Parent,
|
|
Protect: req.Options.Protect,
|
|
Aliases: aliasesFromAliases(req.Options.Aliases),
|
|
Dependencies: req.Options.Dependencies,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
respB, err := monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
Inputs: resource.PropertyMap{
|
|
"bar": resource.NewStringProperty("baz"),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
urnB, idB = respB.URN, respB.ID
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
"res": resource.MakeCustomResourceReference(urnB, idB, ""),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: construct,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, 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, ""),
|
|
}, resp.Outputs)
|
|
|
|
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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because different ordering makes the colouring different.
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 4),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestConfigSecrets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
crypter := config.NewSymmetricCrypter(make([]byte, 32))
|
|
secret, err := crypter.EncryptValue(context.Background(), "hunter2")
|
|
assert.NoError(t, err)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because secrets are serialized with the blinding crypter and can't be restored
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 2),
|
|
Config: config.Map{
|
|
config.MustMakeKey("pkgA", "secret"): config.NewSecureValue(secret),
|
|
},
|
|
Decrypter: crypter,
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
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) {
|
|
t.Parallel()
|
|
|
|
// 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.
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("component", "resA", false)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{}, resp.Outputs)
|
|
|
|
err = monitor.RegisterResourceOutputs(resp.URN, resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("bar"),
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 1),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test calling a method.
|
|
func TestSingleComponentMethodDefaultProviderLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var urn resource.URN
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
var err error
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{
|
|
Parent: req.Parent,
|
|
Aliases: aliasesFromAliases(req.Options.Aliases),
|
|
Protect: req.Options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
urn = resp.URN
|
|
|
|
_, 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.ConstructResponse{
|
|
URN: urn,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
call := func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"name": resource.NewStringProperty("Alice"),
|
|
}, req.Args)
|
|
name := req.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.CallResponse{
|
|
Return: resource.PropertyMap{
|
|
"message": resource.NewStringProperty(message),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
CallF: call,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, 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"),
|
|
}, resp.Outputs)
|
|
|
|
outs, _, _, err := monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{
|
|
"name": resource.NewStringProperty("Alice"),
|
|
}, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"message": resource.NewStringProperty("Alice, bar!"),
|
|
}, outs)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because different ordering makes the colouring different.
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: lt.MakeBasicLifecycleSteps(t, 4),
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
// Test creating a resource from a method.
|
|
func TestSingleComponentMethodResourceDefaultProviderLifecycle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var urn resource.URN
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
construct := func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
var err error
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{
|
|
Parent: req.Parent,
|
|
Aliases: aliasesFromAliases(req.Options.Aliases),
|
|
Protect: req.Options.Protect,
|
|
})
|
|
assert.NoError(t, err)
|
|
urn = resp.URN
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")}
|
|
err = monitor.RegisterResourceOutputs(resp.URN, outs)
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: outs,
|
|
}, nil
|
|
}
|
|
|
|
call := func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
_, err := monitor.RegisterResource("pkgA:m:typC", "resA", true, deploytest.ResourceOptions{
|
|
Parent: urn,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.CallResponse{}, nil
|
|
}
|
|
|
|
return &deploytest.Provider{
|
|
ConstructF: construct,
|
|
CallF: call,
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, 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"),
|
|
}, resp.Outputs)
|
|
|
|
_, _, _, err = monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{}, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because different ordering makes the colouring different.
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: lt.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) {
|
|
t.Parallel()
|
|
|
|
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(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
switch string(req.Type) {
|
|
case "pkgB:m:first":
|
|
resp, err := monitor.RegisterResource("pkgB:m:first", req.Name, false)
|
|
require.NoError(t, err)
|
|
firstURN = resp.URN
|
|
|
|
resp, err = monitor.RegisterResource("nested", "nested", false,
|
|
deploytest.ResourceOptions{
|
|
Parent: firstURN,
|
|
})
|
|
require.NoError(t, err)
|
|
nestedURN = resp.URN
|
|
|
|
resp, err = monitor.RegisterResource("pkgA:m:sg", "sg", true, deploytest.ResourceOptions{
|
|
Parent: nestedURN,
|
|
})
|
|
require.NoError(t, err)
|
|
sgURN = resp.URN
|
|
|
|
err = monitor.RegisterResourceOutputs(nestedURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterResourceOutputs(firstURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{URN: firstURN}, nil
|
|
case "pkgB:m:second":
|
|
resp, err := monitor.RegisterResource("pkgB:m:second", req.Name, false,
|
|
deploytest.ResourceOptions{
|
|
Dependencies: req.Options.Dependencies,
|
|
})
|
|
require.NoError(t, err)
|
|
secondURN = resp.URN
|
|
|
|
resp, err = monitor.RegisterResource("pkgA:m:rule", "rule", true,
|
|
deploytest.ResourceOptions{
|
|
Parent: secondURN,
|
|
Dependencies: req.Options.PropertyDependencies["sgID"],
|
|
})
|
|
require.NoError(t, err)
|
|
ruleURN = resp.URN
|
|
|
|
err = monitor.RegisterResourceOutputs(secondURN, resource.PropertyMap{})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{URN: secondURN}, nil
|
|
default:
|
|
return plugin.ConstructResponse{}, fmt.Errorf("unexpected type %v", req.Type)
|
|
}
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{Options: lt.TestUpdateOptions{T: t, HostF: hostF}}
|
|
|
|
p.Steps = []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
},
|
|
{
|
|
Op: Destroy,
|
|
SkipPreview: true,
|
|
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
assert.NoError(t, err)
|
|
|
|
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 err
|
|
},
|
|
},
|
|
}
|
|
p.Run(t, nil)
|
|
}
|
|
|
|
func TestProtect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
idCounter := 0
|
|
deleteCounter := 0
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["foo"].DeepEquals(req.NewInputs["foo"]) {
|
|
// If foo changes do a replace, we use this to check we don't delete on replace
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"foo"},
|
|
}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter))
|
|
idCounter = idCounter + 1
|
|
return plugin.CreateResponse{
|
|
ID: resourceID,
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DeleteF: func(context.Context, plugin.DeleteRequest) (plugin.DeleteResponse, error) {
|
|
deleteCounter = deleteCounter + 1
|
|
return plugin.DeleteResponse{}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
|
|
shouldProtect := true
|
|
createResource := true
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
if createResource {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
Protect: shouldProtect,
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resource
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-0", snap.Resources[1].ID.String())
|
|
assert.Equal(t, 0, deleteCounter)
|
|
|
|
expectedUrn := snap.Resources[1].URN
|
|
expectedMessage := ""
|
|
|
|
// Both updates below should give a diagnostic event
|
|
validate := func(project workspace.Project,
|
|
target deploy.Target, entries JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
for _, event := range events {
|
|
if event.Type == DiagEvent {
|
|
payload := event.Payload().(DiagEventPayload)
|
|
assert.Equal(t, expectedUrn, payload.URN)
|
|
assert.Equal(t, expectedMessage, payload.Message)
|
|
break
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Run a dry-run (preview) which will cause a replace, we should get an error.
|
|
// However, the preview doesn't bail, so we expect one "created" resource as a result of this operation.
|
|
expectedMessage = "<{%reset%}>unable to replace resource \"urn:pulumi:test::test::pkgA:m:typA::resA\"\n" +
|
|
"as it is currently marked for protection. To unprotect the resource, remove the `protect` flag from " +
|
|
"the resource in your Pulumi program and run `pulumi up`<{%reset%}>\n"
|
|
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
})
|
|
_, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient, validate, "1")
|
|
assert.ErrorContains(t, err, "step generator errored")
|
|
|
|
// Run an update which will cause a replace, we should get an error.
|
|
// Contrary to the preview, the error is a bail, so no resources are created.
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate, "2")
|
|
assert.Error(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-0", snap.Resources[1].ID.String())
|
|
assert.Equal(t, 0, deleteCounter)
|
|
|
|
// Run a new update which will cause a delete, we still shouldn't see a provider delete
|
|
expectedMessage = "<{%reset%}>resource \"urn:pulumi:test::test::pkgA:m:typA::resA\" cannot be deleted\n" +
|
|
"because it is protected. To unprotect the resource, either remove the `protect` flag " +
|
|
"from the resource in your Pulumi program and run `pulumi up`, or use the command:\n" +
|
|
"`pulumi state unprotect 'urn:pulumi:test::test::pkgA:m:typA::resA'`<{%reset%}>\n"
|
|
createResource = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate, "3")
|
|
assert.Error(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-0", snap.Resources[1].ID.String())
|
|
assert.Equal(t, true, snap.Resources[1].Protect)
|
|
assert.Equal(t, 0, deleteCounter)
|
|
|
|
// Run a new update to remove the protect and replace in the same update, this should delete the old one
|
|
// and create the new one
|
|
createResource = true
|
|
shouldProtect = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "4")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-2", snap.Resources[1].ID.String())
|
|
assert.Equal(t, false, snap.Resources[1].Protect)
|
|
assert.Equal(t, 1, deleteCounter)
|
|
|
|
// Run a new update to add the protect flag, nothing else should change
|
|
shouldProtect = true
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "5")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-2", snap.Resources[1].ID.String())
|
|
assert.Equal(t, true, snap.Resources[1].Protect)
|
|
assert.Equal(t, 1, deleteCounter)
|
|
|
|
// Edit the snapshot to remove the protect flag and try and replace
|
|
snap.Resources[1].Protect = false
|
|
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "daz",
|
|
})
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate, "6")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
assert.Equal(t, "created-id-3", snap.Resources[1].ID.String())
|
|
assert.Equal(t, 2, deleteCounter)
|
|
}
|
|
|
|
func TestDeletedWith(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
idCounter := 0
|
|
|
|
topURN := resource.URN("")
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if !req.OldOutputs["foo"].DeepEquals(req.NewInputs["foo"]) {
|
|
// If foo changes do a replace, we use this to check we don't delete on replace
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"foo"},
|
|
}, nil
|
|
}
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter))
|
|
idCounter = idCounter + 1
|
|
return plugin.CreateResponse{
|
|
ID: resourceID,
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DeleteF: func(_ context.Context, req plugin.DeleteRequest) (plugin.DeleteResponse, error) {
|
|
if req.URN != topURN {
|
|
// Only topURN (aURN) should be actually deleted
|
|
assert.Fail(t, "Delete was called")
|
|
}
|
|
return plugin.DeleteResponse{}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
|
|
createResource := true
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
if createResource {
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
topURN = respA.URN
|
|
|
|
respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
DeletedWith: respA.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
DeletedWith: respB.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resource
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 4)
|
|
assert.Equal(t, "created-id-0", snap.Resources[1].ID.String())
|
|
assert.Equal(t, "created-id-1", snap.Resources[2].ID.String())
|
|
assert.Equal(t, "created-id-2", snap.Resources[3].ID.String())
|
|
|
|
// Run a new update which will cause a replace, we should only see a provider delete for aURN but should
|
|
// get a new id for everything
|
|
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
})
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 4)
|
|
assert.Equal(t, "created-id-3", snap.Resources[1].ID.String())
|
|
assert.Equal(t, "created-id-4", snap.Resources[2].ID.String())
|
|
assert.Equal(t, "created-id-5", snap.Resources[3].ID.String())
|
|
|
|
// Run a new update which will cause a delete, we still shouldn't see a provider delete for anything but aURN
|
|
createResource = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 0)
|
|
}
|
|
|
|
func TestInvalidGetIDReportsUserError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "", "", resource.PropertyMap{}, "", "", "", "")
|
|
assert.Error(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
validate := ExpectDiagMessage(t, regexp.QuoteMeta(
|
|
"<{%reset%}>Expected an ID for urn:pulumi:test::test::pkgA:m:typA::resA<{%reset%}>"))
|
|
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 1)
|
|
}
|
|
|
|
func TestEventSecrets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
diff := req.OldOutputs.Diff(req.NewInputs)
|
|
if diff == nil {
|
|
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
|
|
}
|
|
detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff, false)
|
|
changedKeys := diff.ChangedKeys()
|
|
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ChangedKeys: changedKeys,
|
|
DetailedDiff: detailedDiff,
|
|
}, nil
|
|
},
|
|
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "id123",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
var inputs resource.PropertyMap
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because secrets are serialized with the blinding crypter and can't be restored
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
Steps: []lt.TestStep{{
|
|
Op: Update,
|
|
SkipPreview: true,
|
|
}},
|
|
}
|
|
|
|
inputs = resource.PropertyMap{
|
|
"webhooks": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewObjectProperty(resource.PropertyMap{
|
|
"clientConfig": resource.NewObjectProperty(resource.PropertyMap{
|
|
"service": resource.NewStringProperty("foo"),
|
|
}),
|
|
}),
|
|
})),
|
|
}
|
|
snap := p.Run(t, nil)
|
|
|
|
inputs = resource.PropertyMap{
|
|
"webhooks": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{
|
|
resource.NewObjectProperty(resource.PropertyMap{
|
|
"clientConfig": resource.NewObjectProperty(resource.PropertyMap{
|
|
"service": resource.NewStringProperty("bar"),
|
|
}),
|
|
}),
|
|
})),
|
|
}
|
|
p.Steps[0].Validate = func(project workspace.Project, target deploy.Target, entries JournalEntries,
|
|
evts []Event, err error,
|
|
) error {
|
|
for _, e := range evts {
|
|
var step StepEventMetadata
|
|
//nolint:exhaustive // We only care about a subset of events here
|
|
switch e.Type {
|
|
case ResourcePreEvent:
|
|
step = e.Payload().(ResourcePreEventPayload).Metadata
|
|
case ResourceOutputsEvent:
|
|
step = e.Payload().(ResourceOutputsEventPayload).Metadata
|
|
default:
|
|
continue
|
|
}
|
|
if step.URN.Name() != "resA" {
|
|
continue
|
|
}
|
|
|
|
assert.True(t, step.Old.Inputs["webhooks"].IsSecret())
|
|
assert.True(t, step.Old.Outputs["webhooks"].IsSecret())
|
|
assert.True(t, step.New.Inputs["webhooks"].IsSecret())
|
|
}
|
|
return err
|
|
}
|
|
p.Run(t, snap)
|
|
}
|
|
|
|
func TestAdditionalSecretOutputs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Skip("AdditionalSecretOutputs warning is currently disabled")
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "id123",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
var inputs resource.PropertyMap
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
AdditionalSecretOutputs: []resource.PropertyKey{"a", "b"},
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
inputs = resource.PropertyMap{
|
|
"a": resource.NewStringProperty("testA"),
|
|
// b is missing
|
|
"c": resource.MakeSecret(resource.NewStringProperty("testC")),
|
|
}
|
|
|
|
// Run an update to create the resource and check we warn about b
|
|
validate := func(
|
|
project workspace.Project, target deploy.Target,
|
|
entries JournalEntries, events []Event,
|
|
err error,
|
|
) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range events {
|
|
if events[i].Type == "diag" {
|
|
payload := events[i].Payload().(engine.DiagEventPayload)
|
|
if payload.Severity == "warning" &&
|
|
payload.URN == "urn:pulumi:test::test::pkgA:m:typA::resA" &&
|
|
payload.Message == "<{%reset%}>Could not find property 'b' listed in additional secret outputs.<{%reset%}>\n" {
|
|
// Found the message we expected
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return errors.New("Expected a diagnostic message, got none")
|
|
}
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate)
|
|
assert.NoError(t, err)
|
|
|
|
// Should have the provider and resA
|
|
assert.Len(t, snap.Resources, 2)
|
|
resA := snap.Resources[1]
|
|
assert.Equal(t, []resource.PropertyKey{"a", "b"}, resA.AdditionalSecretOutputs)
|
|
assert.True(t, resA.Outputs["a"].IsSecret())
|
|
assert.True(t, resA.Outputs["c"].IsSecret())
|
|
}
|
|
|
|
func TestDefaultParents(t *testing.T) {
|
|
t.Parallel()
|
|
t.Skipf("Default parents disabled due to https://github.com/pulumi/pulumi/issues/10950")
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource(
|
|
resource.RootStackType,
|
|
info.Project+"-"+info.Stack,
|
|
false,
|
|
deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource(
|
|
"pkgA:m:typA",
|
|
"resA",
|
|
true,
|
|
deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resource
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 3)
|
|
|
|
// Assert that resource 0 is the stack
|
|
assert.Equal(t, resource.RootStackType, snap.Resources[0].Type)
|
|
// Assert that the other 2 resources have the stack as a parent
|
|
assert.Equal(t, snap.Resources[0].URN, snap.Resources[1].Parent)
|
|
assert.Equal(t, snap.Resources[0].URN, snap.Resources[2].Parent)
|
|
}
|
|
|
|
func TestPendingDeleteOrder(t *testing.T) {
|
|
// Test for https://github.com/pulumi/pulumi/issues/2948 Ensure that if we have resources A and B, and we
|
|
// go to replace A but then fail to replace B that we correctly handle everything in the same order when
|
|
// we retry the update.
|
|
//
|
|
// That is normally for this operation we would do the following:
|
|
// 1. Create new A
|
|
// 2. Create new B
|
|
// 3. Delete old B
|
|
// 4. Delete old A
|
|
// So if step 2 fails to create the new B we want to see:
|
|
// 1. Create new A
|
|
// 2. Create new B (fail)
|
|
// 1. Create new B
|
|
// 2. Delete old B
|
|
// 3. Delete old A
|
|
// Currently (and what #2948 tracks) is that the engine does the following:
|
|
// 1. Create new A
|
|
// 2. Create new B (fail)
|
|
// 3. Delete old A
|
|
// 1. Create new B
|
|
// 2. Delete old B
|
|
// That delete A fails because the delete B needs to happen first.
|
|
|
|
t.Parallel()
|
|
|
|
cloudState := map[resource.ID]resource.PropertyMap{}
|
|
|
|
failCreationOfTypB := false
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
if strings.Contains(string(req.URN), "typB") && failCreationOfTypB {
|
|
return plugin.CreateResponse{}, errors.New("Could not create typB")
|
|
}
|
|
|
|
id := resource.ID(strconv.Itoa(len(cloudState)))
|
|
if !req.Preview {
|
|
cloudState[id] = req.Properties
|
|
}
|
|
return plugin.CreateResponse{
|
|
ID: id,
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DeleteF: func(_ context.Context, req plugin.DeleteRequest) (plugin.DeleteResponse, error) {
|
|
// Fail if anything in cloud state still points to us
|
|
for other, res := range cloudState {
|
|
for _, v := range res {
|
|
if v.IsString() && v.StringValue() == string(req.ID) {
|
|
return plugin.DeleteResponse{}, fmt.Errorf("Can not delete %s used by %s", req.ID, other)
|
|
}
|
|
}
|
|
}
|
|
|
|
delete(cloudState, req.ID)
|
|
return plugin.DeleteResponse{}, nil
|
|
},
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if strings.Contains(string(req.URN), "typA") {
|
|
if !req.OldOutputs["foo"].DeepEquals(req.NewInputs["foo"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"foo"},
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"foo": {
|
|
Kind: plugin.DiffUpdateReplace,
|
|
InputDiff: true,
|
|
},
|
|
},
|
|
DeleteBeforeReplace: false,
|
|
}, nil
|
|
}
|
|
}
|
|
if strings.Contains(string(req.URN), "typB") {
|
|
if !req.OldOutputs["parent"].DeepEquals(req.NewInputs["parent"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"parent"},
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"parent": {
|
|
Kind: plugin.DiffUpdateReplace,
|
|
InputDiff: true,
|
|
},
|
|
},
|
|
DeleteBeforeReplace: false,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
assert.Fail(t, "Didn't expect update to be called")
|
|
return plugin.UpdateResponse{}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"parent": resp.ID,
|
|
}),
|
|
Dependencies: []resource.URN{resp.URN},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resources
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 3)
|
|
|
|
// Trigger a replacement of A but fail to create B
|
|
failCreationOfTypB = true
|
|
ins = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
})
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
// Assert that this fails, we should have two copies of A now, one new one and one old one pending delete
|
|
assert.Error(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 4)
|
|
assert.Equal(t, snap.Resources[1].Type, tokens.Type("pkgA:m:typA"))
|
|
assert.False(t, snap.Resources[1].Delete)
|
|
assert.Equal(t, snap.Resources[2].Type, tokens.Type("pkgA:m:typA"))
|
|
assert.True(t, snap.Resources[2].Delete)
|
|
|
|
// Now allow B to create and try again
|
|
failCreationOfTypB = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 3)
|
|
}
|
|
|
|
func TestPendingDeleteReplacement(t *testing.T) {
|
|
// Test for https://github.com/pulumi/pulumi/issues/11391, check that if we
|
|
// try to replace a resource via delete before replace, but fail to delete
|
|
// it, then rerun that we don't error.
|
|
|
|
t.Parallel()
|
|
|
|
cloudID := 0
|
|
cloudState := map[resource.ID]resource.PropertyMap{}
|
|
|
|
failDeletionOfTypB := true
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
id := resource.ID("")
|
|
if !req.Preview {
|
|
id = resource.ID(strconv.Itoa(cloudID))
|
|
cloudID = cloudID + 1
|
|
cloudState[id] = req.Properties
|
|
}
|
|
return plugin.CreateResponse{
|
|
ID: id,
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DeleteF: func(_ context.Context, req plugin.DeleteRequest) (plugin.DeleteResponse, error) {
|
|
// Fail if anything in cloud state still points to us
|
|
for _, res := range cloudState {
|
|
for _, v := range res {
|
|
if v.IsString() && v.StringValue() == string(req.ID) {
|
|
return plugin.DeleteResponse{}, fmt.Errorf("Can not delete %s", req.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.Contains(string(req.URN), "typB") && failDeletionOfTypB {
|
|
return plugin.DeleteResponse{}, errors.New("Could not delete typB")
|
|
}
|
|
|
|
delete(cloudState, req.ID)
|
|
return plugin.DeleteResponse{}, nil
|
|
},
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
if strings.Contains(string(req.URN), "typA") {
|
|
if !req.OldOutputs["foo"].DeepEquals(req.NewInputs["foo"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"foo"},
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"foo": {
|
|
Kind: plugin.DiffUpdateReplace,
|
|
InputDiff: true,
|
|
},
|
|
},
|
|
DeleteBeforeReplace: true,
|
|
}, nil
|
|
}
|
|
}
|
|
if strings.Contains(string(req.URN), "typB") {
|
|
if !req.OldOutputs["parent"].DeepEquals(req.NewInputs["parent"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"parent"},
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"parent": {
|
|
Kind: plugin.DiffUpdateReplace,
|
|
InputDiff: true,
|
|
},
|
|
},
|
|
DeleteBeforeReplace: false,
|
|
}, nil
|
|
}
|
|
if !req.OldOutputs["frob"].DeepEquals(req.NewInputs["frob"]) {
|
|
return plugin.DiffResult{
|
|
Changes: plugin.DiffSome,
|
|
ReplaceKeys: []resource.PropertyKey{"frob"},
|
|
DetailedDiff: map[string]plugin.PropertyDiff{
|
|
"frob": {
|
|
Kind: plugin.DiffUpdateReplace,
|
|
InputDiff: true,
|
|
},
|
|
},
|
|
DeleteBeforeReplace: false,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
assert.Fail(t, "Didn't expect update to be called")
|
|
return plugin.UpdateResponse{}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
insA := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
inB := "active"
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: insA,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{
|
|
Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"parent": respA.ID,
|
|
"frob": inB,
|
|
}),
|
|
PropertyDeps: map[resource.PropertyKey][]resource.URN{
|
|
"parent": {respA.URN},
|
|
},
|
|
Dependencies: []resource.URN{respA.URN},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resources
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 3)
|
|
|
|
// Trigger a replacement of B but fail to delete it
|
|
inB = "inactive"
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
// Assert that this fails, we should have two B's one marked to delete
|
|
assert.Error(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 4)
|
|
assert.Equal(t, snap.Resources[1].Type, tokens.Type("pkgA:m:typA"))
|
|
assert.False(t, snap.Resources[1].Delete)
|
|
assert.Equal(t, snap.Resources[2].Type, tokens.Type("pkgA:m:typB"))
|
|
assert.False(t, snap.Resources[2].Delete)
|
|
assert.Equal(t, snap.Resources[3].Type, tokens.Type("pkgA:m:typB"))
|
|
assert.True(t, snap.Resources[3].Delete)
|
|
|
|
// Now trigger a replacment of A, which will also trigger B to replace
|
|
insA = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
})
|
|
failDeletionOfTypB = false
|
|
snap, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil)
|
|
// Assert this is ok, we should have just one A and B
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 3)
|
|
assert.Equal(t, snap.Resources[1].Type, tokens.Type("pkgA:m:typA"))
|
|
assert.False(t, snap.Resources[1].Delete)
|
|
assert.Equal(t, snap.Resources[2].Type, tokens.Type("pkgA:m:typB"))
|
|
assert.False(t, snap.Resources[2].Delete)
|
|
}
|
|
|
|
func TestTimestampTracking(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
p := <.TestPlan{}
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DiffF: func(context.Context, plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{Changes: plugin.DiffSome}, nil
|
|
},
|
|
UpdateF: func(context.Context, plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
outputs := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
return plugin.UpdateResponse{
|
|
Properties: outputs,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource(
|
|
resource.RootStackType,
|
|
info.Project+"-"+info.Stack,
|
|
false,
|
|
deploytest.ResourceOptions{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource(
|
|
"pkgA:m:typA",
|
|
"resA",
|
|
true,
|
|
deploytest.ResourceOptions{})
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
|
|
p.Options.HostF = deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p.Options.T = t
|
|
// Run an update to create the resource -- created and updated should be set and equal.
|
|
p.Steps = []lt.TestStep{{Op: Update, SkipPreview: true}}
|
|
snap := p.Run(t, nil)
|
|
require.NotEmpty(t, snap.Resources)
|
|
|
|
creationTimes := make(map[resource.URN]time.Time, len(snap.Resources))
|
|
for _, resource := range snap.Resources {
|
|
assert.NotNil(t, resource.Created, "missing created time: %v", resource.URN)
|
|
assert.NotNil(t, resource.Modified, "missing modified time: %v", resource.URN)
|
|
tz, _ := resource.Created.Zone()
|
|
assert.Equal(t, "UTC", tz, "time zone is not UTC: %v", resource.URN)
|
|
assert.Equal(t, resource.Created, resource.Modified,
|
|
"created time != modified time: %v", resource.URN)
|
|
|
|
creationTimes[resource.URN] = *resource.Created
|
|
}
|
|
|
|
// Run a refresh -- created and updated should be unchanged.
|
|
p.Steps = []lt.TestStep{{Op: Refresh, SkipPreview: true}}
|
|
snap = p.Run(t, snap)
|
|
require.NotEmpty(t, snap.Resources)
|
|
for _, resource := range snap.Resources {
|
|
assert.NotNil(t, resource.Created, "missing created time: %v", resource.URN)
|
|
assert.NotNil(t, resource.Modified, "missing modified time: %v", resource.URN)
|
|
assert.Equal(t, *resource.Created, creationTimes[resource.URN],
|
|
"created time changed: %v", resource.URN)
|
|
assert.Equal(t, resource.Created, resource.Modified,
|
|
"modified time changed: %v", resource.URN)
|
|
}
|
|
|
|
// Run another update -- updated should be greater than created for resA,
|
|
// everything else should be untouched.
|
|
p.Steps = []lt.TestStep{{Op: Update, SkipPreview: true}}
|
|
snap = p.Run(t, snap)
|
|
require.NotEmpty(t, snap.Resources)
|
|
for _, resource := range snap.Resources {
|
|
assert.NotNil(t, resource.Created, resource.URN, "missing created time: %v", resource.URN)
|
|
assert.NotNil(t, resource.Modified, resource.URN, "missing modified time: %v", resource.URN)
|
|
assert.Equal(t, creationTimes[resource.URN], *resource.Created,
|
|
"created time changed: %v", resource.URN)
|
|
|
|
//exhaustive:ignore
|
|
switch resource.Type {
|
|
case "pkgA:m:typA":
|
|
tz, _ := resource.Modified.Zone()
|
|
assert.Equal(t, "UTC", tz, "time zone is not UTC: %v", resource.URN)
|
|
assert.NotEqual(t, creationTimes[resource.URN], *resource.Modified,
|
|
"modified time did not update: %v", resource.URN)
|
|
assert.Greater(t, *resource.Modified, *resource.Created,
|
|
"modified time is too old: %v", resource.URN)
|
|
case "pulumi:providers:pkgA", "pulumi:pulumi:Stack":
|
|
tz, _ := resource.Modified.Zone()
|
|
assert.Equal(t, "UTC", tz, "time zone is not UTC: %v", resource.URN)
|
|
assert.NotNil(t, *resource.Created, "missing created time: %v", resource.URN)
|
|
assert.NotNil(t, *resource.Modified, "missing modified time: %v", resource.URN)
|
|
default:
|
|
require.FailNow(t, "unrecognized resource type", resource.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOldCheckedInputsAreSent(t *testing.T) {
|
|
// Test for https://github.com/pulumi/pulumi/issues/5973, check that the old inputs from Check are passed
|
|
// to Diff, Update, and Delete.
|
|
t.Parallel()
|
|
|
|
firstUpdate := true
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CheckF: func(
|
|
_ context.Context,
|
|
req plugin.CheckRequest,
|
|
) (plugin.CheckResponse, error) {
|
|
// Check that the old inputs are passed to CheckF
|
|
if firstUpdate {
|
|
assert.Nil(t, req.Olds)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
}), req.News)
|
|
} else {
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
}), req.Olds)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
}), req.News)
|
|
}
|
|
|
|
// Add a default property
|
|
results := resource.PropertyMap{}
|
|
for k, v := range req.News {
|
|
results[k] = v
|
|
}
|
|
results["default"] = resource.NewStringProperty("default")
|
|
|
|
return plugin.CheckResponse{Properties: results}, nil
|
|
},
|
|
DiffF: func(_ context.Context, req plugin.DiffRequest) (plugin.DiffResult, error) {
|
|
// Check that the old inputs and outputs are passed to DiffF
|
|
if firstUpdate {
|
|
assert.Nil(t, req.OldInputs)
|
|
assert.Nil(t, req.OldOutputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
}), req.NewInputs)
|
|
} else {
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
}), req.OldInputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
"computed": "computed",
|
|
}), req.OldOutputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
}), req.NewInputs)
|
|
}
|
|
|
|
// Let the engine do the diff, we just want to assert the conditions above
|
|
return plugin.DiffResult{}, nil
|
|
},
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
id := resource.ID("")
|
|
results := resource.PropertyMap{}
|
|
for k, v := range req.Properties {
|
|
results[k] = v
|
|
}
|
|
// Add a computed property
|
|
results["computed"] = resource.MakeComputed(resource.NewStringProperty(""))
|
|
|
|
if !req.Preview {
|
|
id = resource.ID("1")
|
|
results["computed"] = resource.NewStringProperty("computed")
|
|
}
|
|
return plugin.CreateResponse{
|
|
ID: id,
|
|
Properties: results,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
UpdateF: func(_ context.Context, req plugin.UpdateRequest) (plugin.UpdateResponse, error) {
|
|
// Check that the old inputs and outputs are passed to UpdateF
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
}), req.OldInputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
"computed": "computed",
|
|
}), req.OldOutputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
}), req.NewInputs)
|
|
|
|
results := resource.PropertyMap{}
|
|
for k, v := range req.NewInputs {
|
|
results[k] = v
|
|
}
|
|
// Add a computed property
|
|
results["computed"] = resource.MakeComputed(resource.NewStringProperty(""))
|
|
|
|
if !req.Preview {
|
|
results["computed"] = resource.NewStringProperty("computed")
|
|
}
|
|
|
|
return plugin.UpdateResponse{
|
|
Properties: results,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
DeleteF: func(_ context.Context, req plugin.DeleteRequest) (plugin.DeleteResponse, error) {
|
|
// Check that the old inputs and outputs are passed to UpdateF
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
}), req.Inputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
"computed": "computed",
|
|
}), req.Outputs)
|
|
|
|
return plugin.DeleteResponse{}, nil
|
|
},
|
|
}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
insA := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: insA,
|
|
})
|
|
assert.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update to create the resources
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
resA := snap.Resources[1]
|
|
assert.Equal(t, tokens.Type("pkgA:m:typA"), resA.Type)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
}), resA.Inputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
"default": "default",
|
|
"computed": "computed",
|
|
}), resA.Outputs)
|
|
|
|
// Now run another update with new inputs
|
|
insA = resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
})
|
|
firstUpdate = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
resA = snap.Resources[1]
|
|
assert.Equal(t, tokens.Type("pkgA:m:typA"), resA.Type)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
}), resA.Inputs)
|
|
assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "baz",
|
|
"default": "default",
|
|
"computed": "computed",
|
|
}), resA.Outputs)
|
|
|
|
// Now run a destroy to delete the resource and check the stored inputs and outputs are sent
|
|
snap, err = lt.TestOp(Destroy).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 0)
|
|
}
|
|
|
|
func TestResourceNames(t *testing.T) {
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/10117
|
|
t.Parallel()
|
|
|
|
cases := []string{
|
|
"foo",
|
|
":colons",
|
|
"-dashes",
|
|
"file/path.txt",
|
|
"bar|table",
|
|
"spaces in names",
|
|
"email@address",
|
|
"<output object>",
|
|
"[brackets]",
|
|
"{braces}",
|
|
"(parens)",
|
|
"C:\\windows\\paths",
|
|
"& @ $ % ^ * #",
|
|
"'quotes'",
|
|
"\"double quotes\"",
|
|
"double::colons", // https://github.com/pulumi/pulumi/issues/13968
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
tt := tt
|
|
t.Run(tt, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "1",
|
|
Properties: resource.PropertyMap{},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
// Check the name works as a provider
|
|
resp, err := monitor.RegisterResource("pulumi:providers:pkgA", tt, true)
|
|
assert.NoError(t, err)
|
|
|
|
provRef, err := providers.NewReference(resp.URN, resp.ID)
|
|
assert.NoError(t, err)
|
|
|
|
// And a custom resource
|
|
respCustom, err := monitor.RegisterResource("pkgA:m:typA", tt, true, deploytest.ResourceOptions{
|
|
Provider: provRef.String(),
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// And a component resource
|
|
_, err = monitor.RegisterResource("pkgA:m:typB", tt, false, deploytest.ResourceOptions{
|
|
// And as a URN parameter
|
|
Parent: respCustom.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
snap, err := lt.TestOp(Update).Run(p.GetProject(), p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, snap.Resources, 3)
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pulumi:providers:pkgA::"+tt), snap.Resources[0].URN)
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::"+tt), snap.Resources[1].URN)
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typB::"+tt), snap.Resources[2].URN)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSourcePositions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
const regPos = "/test/source/positions#1,2"
|
|
const readPos = "/test/source/positions#3,4"
|
|
inputs := resource.PropertyMap{}
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: inputs,
|
|
SourcePosition: "file://" + regPos,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = monitor.ReadResource("pkgA:m:typA", "resB", "id", "", inputs, "", "", "file://"+readPos, "")
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
regURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
readURN := p.NewURN("pkgA:m:typA", "resB", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, snap.Resources, 3)
|
|
|
|
reg := snap.Resources[1]
|
|
assert.Equal(t, regURN, reg.URN)
|
|
assert.Equal(t, "project://"+regPos, reg.SourcePosition)
|
|
|
|
read := snap.Resources[2]
|
|
assert.Equal(t, readURN, read.URN)
|
|
assert.Equal(t, "project://"+readPos, read.SourcePosition)
|
|
}
|
|
|
|
func TestBadResourceOptionURNs(t *testing.T) {
|
|
// Test for https://github.com/pulumi/pulumi/issues/13490, check that if a user (or SDK) sends a malformed
|
|
// URN we return an error.
|
|
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
opts deploytest.ResourceOptions
|
|
assertFn func(err error)
|
|
}{
|
|
{
|
|
name: "malformed alias urn",
|
|
opts: deploytest.ResourceOptions{
|
|
Aliases: []*pulumirpc.Alias{
|
|
makeUrnAlias("very-bad urn"),
|
|
},
|
|
},
|
|
assertFn: func(err error) {
|
|
assert.ErrorContains(t, err, "invalid alias URN: invalid URN \"very-bad urn\"")
|
|
},
|
|
},
|
|
{
|
|
name: "malformed alias parent urn",
|
|
opts: deploytest.ResourceOptions{
|
|
Aliases: []*pulumirpc.Alias{
|
|
makeSpecAliasWithParent("", "", "", "", "very-bad urn"),
|
|
},
|
|
},
|
|
assertFn: func(err error) {
|
|
assert.ErrorContains(t, err, "invalid parent alias URN: invalid URN \"very-bad urn\"")
|
|
},
|
|
},
|
|
{
|
|
name: "malformed parent urn",
|
|
opts: deploytest.ResourceOptions{
|
|
Parent: "very-bad urn",
|
|
},
|
|
assertFn: func(err error) {
|
|
assert.ErrorContains(t, err, "invalid parent URN: invalid URN \"very-bad urn\"")
|
|
},
|
|
},
|
|
{
|
|
name: "malformed deleted with urn",
|
|
opts: deploytest.ResourceOptions{
|
|
DeletedWith: "very-bad urn",
|
|
},
|
|
assertFn: func(err error) {
|
|
assert.ErrorContains(t, err, "invalid DeletedWith URN: invalid URN \"very-bad urn\"")
|
|
},
|
|
},
|
|
{
|
|
name: "malformed dependency",
|
|
opts: deploytest.ResourceOptions{
|
|
Dependencies: []resource.URN{"very-bad urn"},
|
|
},
|
|
assertFn: func(err error) {
|
|
assert.ErrorContains(t, err, "invalid dependency URN: invalid URN \"very-bad urn\"")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "res", true, tt.opts)
|
|
tt.assertFn(err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProviderChecksums(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}, deploytest.WithoutGrpc),
|
|
}
|
|
|
|
ins := resource.NewPropertyMapFromMap(map[string]interface{}{
|
|
"foo": "bar",
|
|
})
|
|
|
|
createResource := true
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
if createResource {
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: ins,
|
|
PluginChecksums: map[string][]byte{
|
|
"windows-x64": {0, 1, 2, 3, 4},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
|
|
// Run an update
|
|
snap, err := lt.TestOp(Update).RunStep(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 2)
|
|
// Check the checksum was saved in the provider resource
|
|
assert.Equal(t, tokens.Type("pulumi:providers:pkgA"), snap.Resources[0].Type)
|
|
checksums := snap.Resources[0].Inputs["__internal"].ObjectValue()["pluginChecksums"].ObjectValue()
|
|
assert.Equal(t, "0001020304", checksums["windows-x64"].StringValue())
|
|
|
|
// Delete the resource and ensure the checksums are passed to EnsurePlugins
|
|
createResource = false
|
|
snap, err = lt.TestOp(Update).RunStep(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, snap)
|
|
assert.Len(t, snap.Resources, 0)
|
|
}
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/14040, ensure the step generators automatic
|
|
// diff is tagged as an input diff.
|
|
func TestAutomaticDiff(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
inputs := resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
}
|
|
programF := deploytest.NewLanguageRuntimeF(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
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
resURN := p.NewURN("pkgA:m:typA", "resA", "")
|
|
|
|
// Run the initial update.
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// Change the inputs and run again
|
|
inputs = resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(2),
|
|
}
|
|
_, err = lt.TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient,
|
|
func(_ workspace.Project, _ deploy.Target, _ JournalEntries,
|
|
events []Event, err error,
|
|
) error {
|
|
found := false
|
|
for _, e := range events {
|
|
if e.Type == ResourcePreEvent {
|
|
p := e.Payload().(ResourcePreEventPayload).Metadata
|
|
if p.URN == resURN {
|
|
// Should find an update op with the diff set to an input diff
|
|
assert.Equal(t, deploy.OpUpdate, p.Op)
|
|
assert.Equal(t, []resource.PropertyKey{"foo"}, p.Diffs)
|
|
assert.Equal(t, map[string]plugin.PropertyDiff{
|
|
"foo": {
|
|
Kind: plugin.DiffUpdate,
|
|
InputDiff: true,
|
|
},
|
|
}, p.DetailedDiff)
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
return err
|
|
})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Test that the engine only sends OutputValues for Construct and Call
|
|
func TestConstructCallSecretsUnknowns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
// Assert that "foo" is secret and "bar" is unknown
|
|
foo := req.Inputs["foo"]
|
|
assert.True(t, foo.IsOutput())
|
|
assert.True(t, foo.OutputValue().Known)
|
|
assert.True(t, foo.OutputValue().Secret)
|
|
|
|
bar := req.Inputs["bar"]
|
|
assert.True(t, bar.IsOutput())
|
|
assert.False(t, bar.OutputValue().Known)
|
|
assert.False(t, bar.OutputValue().Secret)
|
|
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
_ *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
// Assert that "foo" is secret and "bar" is unknown
|
|
foo := req.Args["foo"]
|
|
assert.True(t, foo.IsOutput())
|
|
assert.True(t, foo.OutputValue().Known)
|
|
assert.True(t, foo.OutputValue().Secret)
|
|
|
|
bar := req.Args["bar"]
|
|
assert.True(t, bar.IsOutput())
|
|
assert.False(t, bar.OutputValue().Known)
|
|
assert.False(t, bar.OutputValue().Secret)
|
|
|
|
return plugin.CallResponse{}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
inputs := resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}
|
|
|
|
_, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
Inputs: inputs,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
_, _, _, err = monitor.Call("pkgA:m:typA", inputs, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Test that the engine propagates dependencies from Construct and Call
|
|
func TestConstructCallReturnDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
test := func(t *testing.T, opt deploytest.PluginOption) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", req.Name+"-a", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Return a secret and unknown output depending on some internal resource
|
|
deps := []resource.URN{respA.URN}
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
OutputDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
_ *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a"},
|
|
req.Options.ArgDependencies["arg"])
|
|
|
|
// Assume a single output arg that this call depends on
|
|
arg := req.Args["arg"]
|
|
deps := arg.OutputValue().Dependencies
|
|
|
|
return plugin.CallResponse{
|
|
Return: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
ReturnDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, opt),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// The urn of the internal resource the component created
|
|
urn := resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a")
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from RegisterResource.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, resp.Outputs)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, resp.Dependencies)
|
|
|
|
result, deps, _, err := monitor.Call("pkgA:m:typA", resource.PropertyMap{
|
|
// Send this as an output value using the dependencies returned.
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: resp.Outputs["foo"].SecretValue().Element,
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: []resource.URN{urn},
|
|
}),
|
|
}, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from Call.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, result)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, deps)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, true, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
t.Run("WithGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithGrpc)
|
|
})
|
|
t.Run("WithoutGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithoutGrpc)
|
|
})
|
|
}
|
|
|
|
// Test that the engine can receive OutputValues for Construct and Call
|
|
func TestConstructCallReturnOutputs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
test := func(t *testing.T, opt deploytest.PluginOption) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", req.Name+"-a", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Return a secret and unknown output depending on some internal resource
|
|
deps := []resource.URN{respA.URN}
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.NewOutputProperty(resource.Output{
|
|
Element: resource.NewStringProperty("foo"),
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: deps,
|
|
}),
|
|
"bar": resource.NewOutputProperty(resource.Output{
|
|
Dependencies: deps,
|
|
}),
|
|
},
|
|
OutputDependencies: nil, // Left blank on purpose because AcceptsOutputs is true
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
_ *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a"},
|
|
req.Options.ArgDependencies["arg"])
|
|
|
|
// Assume a single output arg that this call depends on
|
|
arg := req.Args["arg"]
|
|
deps := arg.OutputValue().Dependencies
|
|
|
|
return plugin.CallResponse{
|
|
Return: resource.PropertyMap{
|
|
"foo": resource.NewOutputProperty(resource.Output{
|
|
Element: resource.NewStringProperty("foo"),
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: deps,
|
|
}),
|
|
"bar": resource.NewOutputProperty(resource.Output{
|
|
Dependencies: deps,
|
|
}),
|
|
},
|
|
ReturnDependencies: nil, // Left blank on purpose because AcceptsOutputs is true
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, opt),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// The urn of the internal resource the component created
|
|
urn := resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a")
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from RegisterResource.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, resp.Outputs)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, resp.Dependencies)
|
|
|
|
result, deps, _, err := monitor.Call("pkgA:m:typA", resource.PropertyMap{
|
|
// Send this as an output value using the dependencies returned.
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: resp.Outputs["foo"].SecretValue().Element,
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: []resource.URN{urn},
|
|
}),
|
|
}, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from Call.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, result)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, deps)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, true, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
t.Run("WithGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithGrpc)
|
|
})
|
|
t.Run("WithoutGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithoutGrpc)
|
|
})
|
|
}
|
|
|
|
// Test that the engine fills in dependencies to Construct and Call given just OutputValues
|
|
func TestConstructCallSendDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
test := func(t *testing.T, opt deploytest.PluginOption) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typC::resC"},
|
|
req.Options.PropertyDependencies["arg"])
|
|
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", req.Name+"-a", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Return a secret and unknown output depending on some internal resource
|
|
deps := []resource.URN{respA.URN}
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
OutputDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
_ *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a"},
|
|
req.Options.ArgDependencies["arg"])
|
|
|
|
// Assume a single output arg that this call depends on
|
|
arg := req.Args["arg"]
|
|
deps := arg.OutputValue().Dependencies
|
|
|
|
return plugin.CallResponse{
|
|
Return: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
ReturnDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, opt),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
respC, err := monitor.RegisterResource("pkgA:m:typC", "resC", false, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"arg": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
Inputs: resource.PropertyMap{
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: respC.Outputs["arg"],
|
|
Known: true,
|
|
Dependencies: []resource.URN{respC.URN},
|
|
}),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// The urn of the internal resource the component created
|
|
urn := resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a")
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from RegisterResource.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, resp.Outputs)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, resp.Dependencies)
|
|
|
|
result, deps, _, err := monitor.Call("pkgA:m:typA", resource.PropertyMap{
|
|
// Send this as an output value using the dependencies returned.
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: resp.Outputs["foo"].SecretValue().Element,
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: []resource.URN{urn},
|
|
}),
|
|
}, nil, "", "", "")
|
|
assert.NoError(t, err)
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from Call.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, result)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, deps)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, true, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
t.Run("WithGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithGrpc)
|
|
})
|
|
t.Run("WithoutGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithoutGrpc)
|
|
})
|
|
}
|
|
|
|
// Test that the engine deduplicated dependencies to Construct and Call given OutputValues and dependency maps.
|
|
func TestConstructCallDependencyDedeuplication(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
test := func(t *testing.T, opt deploytest.PluginOption) {
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "created-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ReadF: func(_ context.Context, req plugin.ReadRequest) (plugin.ReadResponse, error) {
|
|
return plugin.ReadResponse{
|
|
ReadResult: plugin.ReadResult{
|
|
Inputs: req.Inputs,
|
|
Outputs: req.State,
|
|
},
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typC::resC"},
|
|
req.Options.PropertyDependencies["arg"])
|
|
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
assert.NoError(t, err)
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", req.Name+"-a", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// Return a secret and unknown output depending on some internal resource
|
|
deps := []resource.URN{respA.URN}
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
Outputs: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
OutputDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
CallF: func(
|
|
_ context.Context,
|
|
req plugin.CallRequest,
|
|
_ *deploytest.ResourceMonitor,
|
|
) (plugin.CallResponse, error) {
|
|
// Arg was sent as an output but the dependency map should still be filled in for providers to look at
|
|
assert.Equal(t,
|
|
[]resource.URN{"urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a"},
|
|
req.Options.ArgDependencies["arg"])
|
|
|
|
// Assume a single output arg that this call depends on
|
|
arg := req.Args["arg"]
|
|
deps := arg.OutputValue().Dependencies
|
|
|
|
return plugin.CallResponse{
|
|
Return: resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
},
|
|
ReturnDependencies: map[resource.PropertyKey][]resource.URN{
|
|
"foo": deps,
|
|
"bar": deps,
|
|
},
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}, opt),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
respC, err := monitor.RegisterResource("pkgA:m:typC", "resC", false, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"arg": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
Inputs: resource.PropertyMap{
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: respC.Outputs["arg"],
|
|
Known: true,
|
|
Dependencies: []resource.URN{respC.URN},
|
|
}),
|
|
},
|
|
PropertyDeps: map[resource.PropertyKey][]resource.URN{
|
|
"arg": {respC.URN},
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// The urn of the internal resource the component created
|
|
urn := resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resA-a")
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from RegisterResource.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, resp.Outputs)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, resp.Dependencies)
|
|
|
|
result, deps, _, err := monitor.Call("pkgA:m:typA", resource.PropertyMap{
|
|
// Send this as an output value using the dependencies returned.
|
|
"arg": resource.NewOutputProperty(resource.Output{
|
|
Element: resp.Outputs["foo"].SecretValue().Element,
|
|
Known: true,
|
|
Secret: true,
|
|
Dependencies: []resource.URN{urn},
|
|
}),
|
|
}, map[resource.PropertyKey][]resource.URN{
|
|
"arg": {urn},
|
|
}, "", "", "")
|
|
assert.NoError(t, err)
|
|
|
|
// Assert that the outputs are received as just plain values because SDKs don't yet support output
|
|
// values returned from Call.
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.MakeSecret(resource.NewStringProperty("foo")),
|
|
"bar": resource.MakeComputed(resource.NewStringProperty("")),
|
|
}, result)
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {urn},
|
|
"bar": {urn},
|
|
}, deps)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
_, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, true, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
t.Run("WithGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithGrpc)
|
|
})
|
|
t.Run("WithoutGrpc", func(t *testing.T) {
|
|
t.Parallel()
|
|
test(t, deploytest.WithoutGrpc)
|
|
})
|
|
}
|
|
|
|
// TestStackOutputsProgramError tests that previous stack outputs aren't deleted when an update fails because
|
|
// of a program error.
|
|
func TestStackOutputsProgramError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var step int
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource(resource.RootStackType, "test", false)
|
|
assert.NoError(t, err)
|
|
|
|
val := resource.NewProperty(fmt.Sprintf("step %v", step))
|
|
|
|
var outputs resource.PropertyMap
|
|
|
|
switch step {
|
|
case 0, 3:
|
|
outputs = resource.PropertyMap{
|
|
"first": val,
|
|
"second": val,
|
|
}
|
|
case 1:
|
|
// If an error is raised between calling `pulumi.export("first", ...)` and `pulumi.export("second", ...)`
|
|
// in SDKs like Python and Go, the first export is still registered via RegisterResourceOutputs.
|
|
// This test simulates that by not including "second" when the program will error.
|
|
outputs = resource.PropertyMap{
|
|
"first": val,
|
|
}
|
|
case 2:
|
|
// The Node.js SDK is a bit different, when an error is thrown between module exports, none of the exports
|
|
// are included. An empty set of outputs is registered via RegisterResourceOutputs.
|
|
outputs = resource.PropertyMap{}
|
|
}
|
|
|
|
err = monitor.RegisterResourceOutputs(resp.URN, outputs)
|
|
assert.NoError(t, err)
|
|
|
|
if step == 1 || step == 2 {
|
|
return errors.New("program error")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
validateSnapshot := func(snap *deploy.Snapshot, expectedResourceCount int, expectedOutputs resource.PropertyMap) {
|
|
assert.Len(t, snap.Resources, expectedResourceCount)
|
|
assert.Equal(t, resource.RootStackType, snap.Resources[0].Type)
|
|
assert.Equal(t, expectedOutputs, snap.Resources[0].Outputs)
|
|
}
|
|
|
|
// Run the initial update which sets some stack outputs.
|
|
snap, err := lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 0"),
|
|
"second": resource.NewProperty("step 0"),
|
|
})
|
|
|
|
// Run another update where the program fails before registering all of the stack outputs, simulating the behavior
|
|
// of returning an error after only the first output is set.
|
|
// Ensure the original stack outputs are preserved.
|
|
step = 1
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.ErrorContains(t, err, "program error")
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 1"),
|
|
"second": resource.NewProperty("step 0"), // Prior output is preserved
|
|
})
|
|
|
|
// Run another update that fails to update both stack updates.
|
|
// Ensure the prior stack outputs are preserved.
|
|
step = 2
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.ErrorContains(t, err, "program error")
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 1"), // Prior output is preserved
|
|
"second": resource.NewProperty("step 0"), // Prior output is preserved
|
|
})
|
|
|
|
// Run again, this time without erroring, to ensure the stack outputs are updated.
|
|
step = 3
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "3")
|
|
assert.NoError(t, err)
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 3"),
|
|
"second": resource.NewProperty("step 3"),
|
|
})
|
|
}
|
|
|
|
// TestStackOutputsResourceError tests that previous stack outputs aren't deleted when an update fails
|
|
// due to a resource operation error.
|
|
func TestStackOutputsResourceError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var step int
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{Status: resource.StatusUnknown}, errors.New("oh no")
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource(resource.RootStackType, "test", false)
|
|
assert.NoError(t, err)
|
|
|
|
val := resource.NewProperty(fmt.Sprintf("step %v", step))
|
|
|
|
switch step {
|
|
case 0, 3:
|
|
outsErr := monitor.RegisterResourceOutputs(resp.URN, resource.PropertyMap{
|
|
"first": val,
|
|
"second": val,
|
|
})
|
|
assert.NoError(t, outsErr)
|
|
|
|
case 1:
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.ErrorContains(t, err, "oh no")
|
|
// RegisterResourceOutputs not called here, simulating what happens in SDKs when an output of resA
|
|
// is exported as a stack output.
|
|
|
|
case 2:
|
|
outsErr := monitor.RegisterResourceOutputs(resp.URN, resource.PropertyMap{
|
|
"first": val,
|
|
"second": val,
|
|
})
|
|
assert.NoError(t, outsErr)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
|
|
assert.ErrorContains(t, err, "oh no")
|
|
}
|
|
|
|
return err
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
// Skip display tests because secrets are serialized with the blinding crypter and can't be restored
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
}
|
|
|
|
validateSnapshot := func(snap *deploy.Snapshot, expectedResourceCount int, expectedOutputs resource.PropertyMap) {
|
|
assert.Len(t, snap.Resources, expectedResourceCount)
|
|
assert.Equal(t, resource.RootStackType, snap.Resources[0].Type)
|
|
assert.Equal(t, expectedOutputs, snap.Resources[0].Outputs)
|
|
}
|
|
|
|
// Run the initial update which sets some stack outputs.
|
|
snap, err := lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil, "0")
|
|
assert.NoError(t, err)
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 0"),
|
|
"second": resource.NewProperty("step 0"),
|
|
})
|
|
|
|
// Run another that simulates creating a resource that will error during creation and exporting an output of that
|
|
// resource as a stack output, in which case no RegisterResourceOutputs call is made.
|
|
step = 1
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "1")
|
|
assert.ErrorContains(t, err, "oh no")
|
|
validateSnapshot(snap, 2, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 0"), // Original output is preserved
|
|
"second": resource.NewProperty("step 0"), // Original output is preserved
|
|
})
|
|
|
|
// Run another update that still registers a resource that will fail during creation, but do that after the
|
|
// stack outputs are registered, which is in-line with the behavior of real-world programs.
|
|
step = 2
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "2")
|
|
assert.ErrorContains(t, err, "oh no")
|
|
validateSnapshot(snap, 2, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 2"),
|
|
"second": resource.NewProperty("step 2"),
|
|
})
|
|
|
|
// Run again, this time without erroring, to ensure the stack outputs are updated.
|
|
step = 3
|
|
snap, err = lt.TestOp(Update).
|
|
RunStep(p.GetProject(), p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil, "3")
|
|
assert.NoError(t, err)
|
|
validateSnapshot(snap, 1, resource.PropertyMap{
|
|
"first": resource.NewProperty("step 3"),
|
|
"second": resource.NewProperty("step 3"),
|
|
})
|
|
}
|