pulumi/pkg/resource/deploy/source_eval_test.go

3577 lines
103 KiB
Go

// Copyright 2016-2023, 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 deploy
import (
"bytes"
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/blang/semver"
opentracing "github.com/opentracing/opentracing-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/diagtest"
"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/workspace"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
)
type testRegEvent struct {
goal *resource.Goal
result *RegisterResult
}
var _ RegisterResourceEvent = (*testRegEvent)(nil)
func (g *testRegEvent) event() {}
func (g *testRegEvent) Goal() *resource.Goal {
return g.goal
}
func (g *testRegEvent) Done(result *RegisterResult) {
contract.Assertf(g.result == nil, "Attempt to invoke testRegEvent.Done more than once")
g.result = result
}
func fixedProgram(steps []RegisterResourceEvent) deploytest.ProgramFunc {
return func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
for _, s := range steps {
g := s.Goal()
resp, err := resmon.RegisterResource(g.Type, g.Name, g.Custom, deploytest.ResourceOptions{
Parent: g.Parent,
Protect: g.Protect,
Dependencies: g.Dependencies,
Provider: g.Provider,
Inputs: g.Properties,
PropertyDeps: g.PropertyDependencies,
})
if err != nil {
return err
}
s.Done(&RegisterResult{
State: resource.NewState(g.Type, resp.URN, g.Custom, false, resp.ID, g.Properties, resp.Outputs, g.Parent,
g.Protect, false, g.Dependencies, nil, g.Provider, g.PropertyDependencies, false, nil, nil, nil,
"", false, "", nil, nil, "", nil),
})
}
return nil
}
}
func newTestPluginContext(t testing.TB, program deploytest.ProgramFunc) (*plugin.Context, error) {
sink := diagtest.LogSink(t)
statusSink := diagtest.LogSink(t)
lang := deploytest.NewLanguageRuntime(program)
host := deploytest.NewPluginHost(sink, statusSink, lang)
return plugin.NewContext(sink, statusSink, host, nil, "", nil, false, nil)
}
type testProviderSource struct {
providers map[providers.Reference]plugin.Provider
m sync.RWMutex
// If nil, do not return a default provider. Otherwise, return this default provider
defaultProvider plugin.Provider
}
func (s *testProviderSource) registerProvider(ref providers.Reference, provider plugin.Provider) {
s.m.Lock()
defer s.m.Unlock()
s.providers[ref] = provider
}
func (s *testProviderSource) GetProvider(ref providers.Reference) (plugin.Provider, bool) {
s.m.RLock()
defer s.m.RUnlock()
provider, ok := s.providers[ref]
if !ok && s.defaultProvider != nil && providers.IsDefaultProvider(ref.URN()) {
return s.defaultProvider, true
}
return provider, ok
}
func newProviderEvent(pkg, name string, inputs resource.PropertyMap, parent resource.URN) RegisterResourceEvent {
if inputs == nil {
inputs = resource.PropertyMap{}
}
goal := &resource.Goal{
Type: providers.MakeProviderType(tokens.Package(pkg)),
ID: "id",
Name: name,
Custom: true,
Properties: inputs,
Parent: parent,
}
return &testRegEvent{goal: goal}
}
func disableDefaultProviders(runInfo *EvalRunInfo, pkgs ...string) {
if runInfo.Target.Config == nil {
runInfo.Target.Config = config.Map{}
}
c := runInfo.Target.Config
key := config.MustMakeKey("pulumi", "disable-default-providers")
if _, ok, err := c.Get(key, false); err != nil {
panic(err)
} else if ok {
panic("disableDefaultProviders cannot be called twice")
}
b, err := json.Marshal(pkgs)
if err != nil {
panic(err)
}
err = c.Set(key, config.NewValue(string(b)), false)
if err != nil {
panic(err)
}
}
func TestRegisterNoDefaultProviders(t *testing.T) {
t.Parallel()
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
newProviderURN := func(pkg tokens.Package, name string, parent resource.URN) resource.URN {
return newURN(providers.MakeProviderType(pkg), name, parent)
}
componentURN := newURN("component", "component", "")
providerARef, err := providers.NewReference(newProviderURN("pkgA", "providerA", ""), "id1")
assert.NoError(t, err)
providerBRef, err := providers.NewReference(newProviderURN("pkgA", "providerB", componentURN), "id2")
assert.NoError(t, err)
providerCRef, err := providers.NewReference(newProviderURN("pkgC", "providerC", ""), "id1")
assert.NoError(t, err)
steps := []RegisterResourceEvent{
// Register a provider.
newProviderEvent("pkgA", "providerA", nil, ""),
// Register a component resource.
&testRegEvent{
goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false,
nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
// Register a couple resources using provider A.
&testRegEvent{
goal: resource.NewGoal("pkgA:index:typA", "res1", true, resource.PropertyMap{}, componentURN, false, nil,
providerARef.String(), []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
&testRegEvent{
goal: resource.NewGoal("pkgA:index:typA", "res2", true, resource.PropertyMap{}, componentURN, false, nil,
providerARef.String(), []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
// Register two more providers.
newProviderEvent("pkgA", "providerB", nil, ""),
newProviderEvent("pkgC", "providerC", nil, componentURN),
// Register a few resources that use the new providers.
&testRegEvent{
goal: resource.NewGoal("pkgB:index:typB", "res3", true, resource.PropertyMap{}, "", false, nil,
providerBRef.String(), []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
&testRegEvent{
goal: resource.NewGoal("pkgB:index:typC", "res4", true, resource.PropertyMap{}, "", false, nil,
providerCRef.String(), []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
}
// Create and iterate an eval source.
ctx, err := newTestPluginContext(t, fixedProgram(steps))
assert.NoError(t, err)
iter, err := NewEvalSource(ctx, runInfo, nil, EvalSourceOptions{}).Iterate(context.Background(), &testProviderSource{})
assert.NoError(t, err)
processed := 0
for {
event, err := iter.Next()
assert.NoError(t, err)
if event == nil {
break
}
reg := event.(RegisterResourceEvent)
goal := reg.Goal()
if providers.IsProviderType(goal.Type) {
assert.NotEqual(t, "default", goal.Name)
}
urn := newURN(goal.Type, goal.Name, goal.Parent)
id := resource.ID("")
if goal.Custom {
id = "id"
}
reg.Done(&RegisterResult{
State: resource.NewState(goal.Type, urn, goal.Custom, false, id, goal.Properties, resource.PropertyMap{},
goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider, goal.PropertyDependencies,
false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
processed++
}
assert.Equal(t, len(steps), processed)
}
func TestRegisterDefaultProviders(t *testing.T) {
t.Parallel()
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
componentURN := newURN("component", "component", "")
steps := []RegisterResourceEvent{
// Register a component resource.
&testRegEvent{
goal: resource.NewGoal(componentURN.Type(), componentURN.Name(), false, resource.PropertyMap{}, "", false,
nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
// Register a couple resources from package A.
&testRegEvent{
goal: resource.NewGoal("pkgA:m:typA", "res1", true, resource.PropertyMap{},
componentURN, false, nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
&testRegEvent{
goal: resource.NewGoal("pkgA:m:typA", "res2", true, resource.PropertyMap{},
componentURN, false, nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
// Register a few resources from other packages.
&testRegEvent{
goal: resource.NewGoal("pkgB:m:typB", "res3", true, resource.PropertyMap{}, "", false,
nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
&testRegEvent{
goal: resource.NewGoal("pkgB:m:typC", "res4", true, resource.PropertyMap{}, "", false,
nil, "", []string{}, nil, nil, nil, nil, nil, "", nil, nil, false, "", ""),
},
}
// Create and iterate an eval source.
ctx, err := newTestPluginContext(t, fixedProgram(steps))
assert.NoError(t, err)
iter, err := NewEvalSource(ctx, runInfo, nil, EvalSourceOptions{}).Iterate(context.Background(), &testProviderSource{})
assert.NoError(t, err)
processed, defaults := 0, make(map[string]struct{})
for {
event, err := iter.Next()
assert.NoError(t, err)
if event == nil {
break
}
reg := event.(RegisterResourceEvent)
goal := reg.Goal()
urn := newURN(goal.Type, goal.Name, goal.Parent)
id := resource.ID("")
if goal.Custom {
id = "id"
}
if providers.IsProviderType(goal.Type) {
assert.Equal(t, "default", goal.Name)
ref, err := providers.NewReference(urn, id)
assert.NoError(t, err)
_, ok := defaults[ref.String()]
assert.False(t, ok)
defaults[ref.String()] = struct{}{}
} else if goal.Custom {
assert.NotEqual(t, "", goal.Provider)
_, ok := defaults[goal.Provider]
assert.True(t, ok)
}
reg.Done(&RegisterResult{
State: resource.NewState(goal.Type, urn, goal.Custom, false, id, goal.Properties, resource.PropertyMap{},
goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider, goal.PropertyDependencies,
false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
processed++
}
assert.Equal(t, len(steps)+len(defaults), processed)
}
func TestReadInvokeNoDefaultProviders(t *testing.T) {
t.Parallel()
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
newProviderURN := func(pkg tokens.Package, name string, parent resource.URN) resource.URN {
return newURN(providers.MakeProviderType(pkg), name, parent)
}
providerARef, err := providers.NewReference(newProviderURN("pkgA", "providerA", ""), "id1")
assert.NoError(t, err)
providerBRef, err := providers.NewReference(newProviderURN("pkgA", "providerB", ""), "id2")
assert.NoError(t, err)
providerCRef, err := providers.NewReference(newProviderURN("pkgC", "providerC", ""), "id1")
assert.NoError(t, err)
invokes := int32(0)
noopProvider := &deploytest.Provider{
InvokeF: func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error) {
atomic.AddInt32(&invokes, 1)
return plugin.InvokeResponse{}, nil
},
}
providerSource := &testProviderSource{
providers: map[providers.Reference]plugin.Provider{
providerARef: noopProvider,
providerBRef: noopProvider,
providerCRef: noopProvider,
},
}
expectedReads, expectedInvokes := 3, 3
program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
// Perform some reads and invokes with explicit provider references.
_, _, perr := resmon.ReadResource("pkgA:m:typA", "resA", "id1", "", nil, providerARef.String(), "", "", "")
assert.NoError(t, perr)
_, _, perr = resmon.ReadResource("pkgA:m:typB", "resB", "id1", "", nil, providerBRef.String(), "", "", "")
assert.NoError(t, perr)
_, _, perr = resmon.ReadResource("pkgC:m:typC", "resC", "id1", "", nil, providerCRef.String(), "", "", "")
assert.NoError(t, perr)
_, _, perr = resmon.Invoke("pkgA:m:funcA", nil, providerARef.String(), "", "")
assert.NoError(t, perr)
_, _, perr = resmon.Invoke("pkgA:m:funcB", nil, providerBRef.String(), "", "")
assert.NoError(t, perr)
_, _, perr = resmon.Invoke("pkgC:m:funcC", nil, providerCRef.String(), "", "")
assert.NoError(t, perr)
return nil
}
// Create and iterate an eval source.
ctx, err := newTestPluginContext(t, program)
assert.NoError(t, err)
iter, err := NewEvalSource(ctx, runInfo, nil, EvalSourceOptions{}).Iterate(context.Background(), providerSource)
assert.NoError(t, err)
reads := 0
for {
event, err := iter.Next()
assert.NoError(t, err)
if event == nil {
break
}
read := event.(ReadResourceEvent)
urn := newURN(read.Type(), read.Name(), read.Parent())
read.Done(&ReadResult{
State: resource.NewState(read.Type(), urn, true, false, read.ID(), read.Properties(),
resource.PropertyMap{}, read.Parent(), false, false, read.Dependencies(), nil, read.Provider(), nil,
false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
reads++
}
assert.Equal(t, expectedReads, reads)
assert.Equal(t, expectedInvokes, int(invokes))
}
func TestReadInvokeDefaultProviders(t *testing.T) {
t.Parallel()
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
invokes := int32(0)
noopProvider := &deploytest.Provider{
InvokeF: func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error) {
atomic.AddInt32(&invokes, 1)
return plugin.InvokeResponse{}, nil
},
}
expectedReads, expectedInvokes := 3, 3
program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
// Perform some reads and invokes with default provider references.
_, _, err := resmon.ReadResource("pkgA:m:typA", "resA", "id1", "", nil, "", "", "", "")
assert.NoError(t, err)
_, _, err = resmon.ReadResource("pkgA:m:typB", "resB", "id1", "", nil, "", "", "", "")
assert.NoError(t, err)
_, _, err = resmon.ReadResource("pkgC:m:typC", "resC", "id1", "", nil, "", "", "", "")
assert.NoError(t, err)
_, _, err = resmon.Invoke("pkgA:m:funcA", nil, "", "", "")
assert.NoError(t, err)
_, _, err = resmon.Invoke("pkgA:m:funcB", nil, "", "", "")
assert.NoError(t, err)
_, _, err = resmon.Invoke("pkgC:m:funcC", nil, "", "", "")
assert.NoError(t, err)
return nil
}
// Create and iterate an eval source.
ctx, err := newTestPluginContext(t, program)
assert.NoError(t, err)
providerSource := &testProviderSource{providers: make(map[providers.Reference]plugin.Provider)}
iter, err := NewEvalSource(ctx, runInfo, nil, EvalSourceOptions{}).Iterate(context.Background(), providerSource)
assert.NoError(t, err)
reads, registers := 0, 0
for {
event, err := iter.Next()
assert.NoError(t, err)
if event == nil {
break
}
switch e := event.(type) {
case RegisterResourceEvent:
goal := e.Goal()
urn, id := newURN(goal.Type, goal.Name, goal.Parent), resource.ID("id")
assert.True(t, providers.IsProviderType(goal.Type))
assert.Equal(t, "default", goal.Name)
ref, err := providers.NewReference(urn, id)
assert.NoError(t, err)
_, ok := providerSource.GetProvider(ref)
assert.False(t, ok)
providerSource.registerProvider(ref, noopProvider)
e.Done(&RegisterResult{
State: resource.NewState(goal.Type, urn, goal.Custom, false, id, goal.Properties, resource.PropertyMap{},
goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider, goal.PropertyDependencies,
false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
registers++
case ReadResourceEvent:
urn := newURN(e.Type(), e.Name(), e.Parent())
e.Done(&ReadResult{
State: resource.NewState(e.Type(), urn, true, false, e.ID(), e.Properties(),
resource.PropertyMap{}, e.Parent(), false, false, e.Dependencies(), nil, e.Provider(), nil, false,
nil, nil, nil, "", false, "", nil, nil, "", nil),
})
reads++
}
}
assert.Equal(t, len(providerSource.providers), registers)
assert.Equal(t, expectedReads, reads)
assert.Equal(t, expectedInvokes, int(invokes))
}
// Test that we can run operations with default providers disabled.
//
// We run against the matrix of
// - enabled vs disabled
// - explicit vs default
//
// B exists as a sanity check, to ensure that we can still perform arbitrary
// operations that belong to other packages.
func TestDisableDefaultProviders(t *testing.T) {
t.Parallel()
type TT struct {
disableDefault bool
hasExplicit bool
expectFail bool
}
cases := []TT{}
for _, disableDefault := range []bool{true, false} {
for _, hasExplicit := range []bool{true, false} {
cases = append(cases, TT{
disableDefault: disableDefault,
hasExplicit: hasExplicit,
expectFail: disableDefault && !hasExplicit,
})
}
}
//nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg
for _, tt := range cases {
tt := tt
var name []string
if tt.disableDefault {
name = append(name, "disableDefault")
}
if tt.hasExplicit {
name = append(name, "hasExplicit")
}
if tt.expectFail {
name = append(name, "expectFail")
}
if len(name) == 0 {
name = append(name, "vanilla")
}
t.Run(strings.Join(name, "+"), func(t *testing.T) {
t.Parallel()
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
if tt.disableDefault {
disableDefaultProviders(runInfo, "pkgA")
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
newProviderURN := func(pkg tokens.Package, name string, parent resource.URN) resource.URN {
return newURN(providers.MakeProviderType(pkg), name, parent)
}
providerARef, err := providers.NewReference(newProviderURN("pkgA", "providerA", ""), "id1")
assert.NoError(t, err)
providerBRef, err := providers.NewReference(newProviderURN("pkgB", "providerB", ""), "id2")
assert.NoError(t, err)
expectedReads, expectedInvokes, expectedRegisters := 3, 3, 1
reads, invokes, registers := 0, int32(0), 0
if tt.expectFail {
expectedReads--
expectedInvokes--
}
if !tt.hasExplicit && !tt.disableDefault && !tt.expectFail {
// The register is creating the default provider
expectedRegisters++
}
noopProvider := &deploytest.Provider{
InvokeF: func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error) {
atomic.AddInt32(&invokes, 1)
return plugin.InvokeResponse{}, nil
},
}
providerSource := &testProviderSource{
providers: map[providers.Reference]plugin.Provider{
providerARef: noopProvider,
providerBRef: noopProvider,
},
defaultProvider: noopProvider,
}
program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
aErrorAssert := assert.NoError
if tt.expectFail {
aErrorAssert = assert.Error
}
var aPkgProvider string
if tt.hasExplicit {
aPkgProvider = providerARef.String()
}
// Perform some reads and invokes with explicit provider references.
_, _, perr := resmon.ReadResource("pkgA:m:typA", "resA", "id1", "", nil, aPkgProvider, "", "", "")
aErrorAssert(t, perr)
_, _, perr = resmon.ReadResource("pkgB:m:typB", "resB", "id1", "", nil, providerBRef.String(), "", "", "")
assert.NoError(t, perr)
_, _, perr = resmon.ReadResource("pkgC:m:typC", "resC", "id1", "", nil, "", "", "", "")
assert.NoError(t, perr)
_, _, perr = resmon.Invoke("pkgA:m:funcA", nil, aPkgProvider, "", "")
aErrorAssert(t, perr)
_, _, perr = resmon.Invoke("pkgB:m:funcB", nil, providerBRef.String(), "", "")
assert.NoError(t, perr)
_, _, perr = resmon.Invoke("pkgC:m:funcC", nil, "", "", "")
assert.NoError(t, perr)
return nil
}
// Create and iterate an eval source.
ctx, err := newTestPluginContext(t, program)
assert.NoError(t, err)
iter, err := NewEvalSource(ctx, runInfo, nil, EvalSourceOptions{}).Iterate(context.Background(), providerSource)
assert.NoError(t, err)
for {
event, err := iter.Next()
assert.NoError(t, err)
if event == nil {
break
}
switch event := event.(type) {
case ReadResourceEvent:
urn := newURN(event.Type(), event.Name(), event.Parent())
event.Done(&ReadResult{
State: resource.NewState(event.Type(), urn, true, false, event.ID(), event.Properties(),
resource.PropertyMap{}, event.Parent(), false, false, event.Dependencies(), nil, event.Provider(), nil,
false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
reads++
case RegisterResourceEvent:
urn := newURN(event.Goal().Type, event.Goal().Name, event.Goal().Parent)
event.Done(&RegisterResult{
State: resource.NewState(event.Goal().Type, urn, true, false, "id", event.Goal().Properties,
resource.PropertyMap{}, event.Goal().Parent, false, false, event.Goal().Dependencies, nil,
event.Goal().Provider, nil, false, nil, nil, nil, "", false, "", nil, nil, "", nil),
})
registers++
default:
panic(event)
}
}
assert.Equalf(t, expectedReads, reads, "Reads")
assert.Equalf(t, expectedInvokes, int(invokes), "Invokes")
assert.Equalf(t, expectedRegisters, registers, "Registers")
})
}
}
// Validates that a resource monitor appropriately propagates
// resource options from a RegisterResourceRequest to a Construct call
// for the remote component resource (MLC).
func TestResouceMonitor_remoteComponentResourceOptions(t *testing.T) {
t.Parallel()
// Helper to keep a some test cases simple.
// Takes a pointer to a container (slice or map)
// and sets it to nil if it's empty.
nilIfEmpty := func(s any) {
// The code below is roughly equivalent to:
// if len(*s) == 0 {
// *s = nil
// }
v := reflect.ValueOf(s) // *T for some T = []T or map[T]*
v = v.Elem() // *T -> T
if v.Len() == 0 {
// Zero value of a slice or map is nil.
v.Set(reflect.Zero(v.Type()))
}
}
runInfo := &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
Target: &Target{Name: tokens.MustParseStackName("test")},
}
newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
var pt tokens.Type
if parent != "" {
pt = parent.Type()
}
return resource.NewURN(runInfo.Target.Name.Q(), runInfo.Proj.Name, pt, t, name)
}
// Used when we need a *bool.
trueValue, falseValue := true, false
tests := []struct {
desc string
give deploytest.ResourceOptions
want plugin.ConstructOptions
}{
{
desc: "AdditionalSecretOutputs",
give: deploytest.ResourceOptions{
AdditionalSecretOutputs: []resource.PropertyKey{"foo"},
},
want: plugin.ConstructOptions{
AdditionalSecretOutputs: []string{"foo"},
},
},
{
desc: "CustomTimeouts/Create",
give: deploytest.ResourceOptions{
CustomTimeouts: &resource.CustomTimeouts{Create: 5},
},
want: plugin.ConstructOptions{
CustomTimeouts: &plugin.CustomTimeouts{Create: "5s"},
},
},
{
desc: "CustomTimeouts/Update",
give: deploytest.ResourceOptions{
CustomTimeouts: &resource.CustomTimeouts{Update: 1},
},
want: plugin.ConstructOptions{
CustomTimeouts: &plugin.CustomTimeouts{Update: "1s"},
},
},
{
desc: "CustomTimeouts/Delete",
give: deploytest.ResourceOptions{
CustomTimeouts: &resource.CustomTimeouts{Delete: 3},
},
want: plugin.ConstructOptions{
CustomTimeouts: &plugin.CustomTimeouts{Delete: "3s"},
},
},
{
desc: "DeleteBeforeReplace/true",
give: deploytest.ResourceOptions{
DeleteBeforeReplace: &trueValue,
},
want: plugin.ConstructOptions{
DeleteBeforeReplace: true,
},
},
{
desc: "DeleteBeforeReplace/false",
give: deploytest.ResourceOptions{
DeleteBeforeReplace: &falseValue,
},
want: plugin.ConstructOptions{
DeleteBeforeReplace: false,
},
},
{
desc: "DeletedWith",
give: deploytest.ResourceOptions{
DeletedWith: newURN("pkgA:m:typB", "resB", ""),
},
want: plugin.ConstructOptions{
DeletedWith: newURN("pkgA:m:typB", "resB", ""),
},
},
{
desc: "IgnoreChanges",
give: deploytest.ResourceOptions{
IgnoreChanges: []string{"foo"},
},
want: plugin.ConstructOptions{
IgnoreChanges: []string{"foo"},
},
},
{
desc: "Protect",
give: deploytest.ResourceOptions{
Protect: true,
},
want: plugin.ConstructOptions{
Protect: true,
},
},
{
desc: "ReplaceOnChanges",
give: deploytest.ResourceOptions{
ReplaceOnChanges: []string{"foo"},
},
want: plugin.ConstructOptions{
ReplaceOnChanges: []string{"foo"},
},
},
{
desc: "RetainOnDelete",
give: deploytest.ResourceOptions{
RetainOnDelete: true,
},
want: plugin.ConstructOptions{
RetainOnDelete: true,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
give := tt.give
give.Remote = true
program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
_, err := resmon.RegisterResource("pkgA:m:typA", "resA", false, give)
require.NoError(t, err, "register resource")
return nil
}
pluginCtx, err := newTestPluginContext(t, program)
require.NoError(t, err, "build plugin context")
evalSource := NewEvalSource(pluginCtx, runInfo, nil, EvalSourceOptions{})
defer func() {
assert.NoError(t, evalSource.Close(), "close eval source")
}()
var got plugin.ConstructOptions
provider := &deploytest.Provider{
ConstructF: func(
_ context.Context,
req plugin.ConstructRequest,
monitor *deploytest.ResourceMonitor,
) (plugin.ConstructResponse, error) {
// To keep test cases above simple,
// nil out properties that are empty when unset.
nilIfEmpty(&req.Options.Aliases)
nilIfEmpty(&req.Options.Dependencies)
nilIfEmpty(&req.Options.PropertyDependencies)
nilIfEmpty(&req.Options.Providers)
got = req.Options
return plugin.ConstructResponse{
URN: newURN(req.Type, req.Name, req.Parent),
}, nil
},
}
ctx := context.Background()
iter, res := evalSource.Iterate(ctx, &testProviderSource{defaultProvider: provider})
require.Nil(t, res, "iterate eval source")
for ev, res := iter.Next(); ev != nil; ev, res = iter.Next() {
require.Nil(t, res, "iterate eval source")
switch ev := ev.(type) {
case RegisterResourceEvent:
goal := ev.Goal()
id := goal.ID
if id == "" {
id = "id"
}
ev.Done(&RegisterResult{
State: &resource.State{
Type: goal.Type,
URN: newURN(goal.Type, goal.Name, goal.Parent),
Custom: goal.Custom,
ID: id,
Inputs: goal.Properties,
Parent: goal.Parent,
Dependencies: goal.Dependencies,
Provider: goal.Provider,
},
})
default:
t.Fatalf("unexpected event: %#v", ev)
}
}
require.NotNil(t, got, "Provider.Construct was not called")
assert.Equal(t, tt.want, got, "Provider.Construct options")
})
}
}
// TODO[pulumi/pulumi#2753]: We should re-enable these tests (and fix them up as needed) once we have a solution
// for #2753.
// func TestReadResourceAndInvokeVersion(t *testing.T) {
// runInfo := &EvalRunInfo{
// ProjectRoot: "/",
// Pwd: "/",
// Program: ".",
// Proj: &workspace.Project{Name: "test"},
// Target: &Target{Name: "test"},
// }
// newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
// var pt tokens.Type
// if parent != "" {
// pt = parent.Type()
// }
// return resource.NewURN(runInfo.Target.Name, runInfo.Proj.Name, pt, t, tokens.QName(name))
// }
// invokes := int32(0)
// noopProvider := &deploytest.Provider{
// InvokeF: func(tokens.ModuleMember, resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
// atomic.AddInt32(&invokes, 1)
// return resource.PropertyMap{}, nil, nil
// },
// }
// // This program is designed to trigger the instantiation of two default providers:
// // 1. Provider pkgA, version 0.18.0
// // 2. Provider pkgC, version 0.18.0
// program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
// // Triggers pkgA, v0.18.0.
// _, _, err := resmon.ReadResource("pkgA:m:typA", "resA", "id1", "", nil, "", "0.18.0")
// assert.NoError(t, err)
// // Uses pkgA's already-instantiated provider.
// _, _, err = resmon.ReadResource("pkgA:m:typB", "resB", "id1", "", nil, "", "0.18.0")
// assert.NoError(t, err)
// // Triggers pkgC, v0.18.0.
// _, _, err = resmon.ReadResource("pkgC:m:typC", "resC", "id1", "", nil, "", "0.18.0")
// assert.NoError(t, err)
// // Uses pkgA and pkgC's already-instantiated provider.
// _, _, err = resmon.Invoke("pkgA:m:funcA", nil, "", "0.18.0")
// assert.NoError(t, err)
// _, _, err = resmon.Invoke("pkgA:m:funcB", nil, "", "0.18.0")
// assert.NoError(t, err)
// _, _, err = resmon.Invoke("pkgC:m:funcC", nil, "", "0.18.0")
// assert.NoError(t, err)
// return nil
// }
// ctx, err := newTestPluginContext(program)
// assert.NoError(t, err)
// providerSource := &testProviderSource{providers: make(map[providers.Reference]plugin.Provider)}
// iter, err := NewEvalSource(ctx, runInfo, nil, false).Iterate(context.Background(), Options{}, providerSource)
// assert.NoError(t, err)
// registrations, reads := 0, 0
// for {
// event, err := iter.Next()
// assert.NoError(t, err)
// if event == nil {
// break
// }
// switch e := event.(type) {
// case RegisterResourceEvent:
// goal := e.Goal()
// urn, id := newURN(goal.Type, goal.Name, goal.Parent), resource.ID("id")
// assert.True(t, providers.IsProviderType(goal.Type))
// // The name of the provider resource is derived from the version requested.
// assert.Equal(t, "default_0_18_0", goal.Name)
// ref, err := providers.NewReference(urn, id)
// assert.NoError(t, err)
// _, ok := providerSource.GetProvider(ref)
// assert.False(t, ok)
// providerSource.registerProvider(ref, noopProvider)
// e.Done(&RegisterResult{
// State: resource.NewState(goal.Type, urn, goal.Custom, false, id, goal.Properties, resource.PropertyMap{},
// goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider, goal.PropertyDependencies,
// false, nil),
// })
// registrations++
// case ReadResourceEvent:
// urn := newURN(e.Type(), string(e.Name()), e.Parent())
// e.Done(&ReadResult{
// State: resource.NewState(e.Type(), urn, true, false, e.ID(), e.Properties(),
// resource.PropertyMap{}, e.Parent(), false, false, e.Dependencies(), nil, e.Provider(), nil, false,
// nil),
// })
// reads++
// }
// }
// assert.Equal(t, 2, registrations)
// assert.Equal(t, 3, reads)
// assert.Equal(t, int32(3), invokes)
// }
// func TestRegisterResourceWithVersion(t *testing.T) {
// runInfo := &EvalRunInfo{
// Proj: &workspace.Project{Name: "test"},
// Target: &Target{Name: "test"},
// }
// newURN := func(t tokens.Type, name string, parent resource.URN) resource.URN {
// var pt tokens.Type
// if parent != "" {
// pt = parent.Type()
// }
// return resource.NewURN(runInfo.Target.Name, runInfo.Proj.Name, pt, t, tokens.QName(name))
// }
// noopProvider := &deploytest.Provider{}
// // This program is designed to trigger the instantiation of two default providers:
// // 1. Provider pkgA, version 0.18.0
// // 2. Provider pkgC, version 0.18.0
// program := func(_ plugin.RunInfo, resmon *deploytest.ResourceMonitor) error {
// // Triggers pkgA, v0.18.1.
// _, err := resmon.RegisterResource("pkgA:m:typA", "resA", true, "", false, nil, "",
// resource.PropertyMap{}, nil, false, "0.18.1", nil)
// assert.NoError(t, err)
// // Re-uses pkgA's already-instantiated provider.
// _, err = resmon.RegisterResource("pkgA:m:typA", "resB", true, "", false, nil, "",
// resource.PropertyMap{}, nil, false, "0.18.1", nil)
// assert.NoError(t, err)
// // Triggers pkgA, v0.18.2
// _, err = resmon.RegisterResource("pkgA:m:typA", "resB", true, "", false, nil, "",
// resource.PropertyMap{}, nil, false, "0.18.2", nil)
// assert.NoError(t, err)
// return nil
// }
// ctx, err := newTestPluginContext(program)
// assert.NoError(t, err)
// providerSource := &testProviderSource{providers: make(map[providers.Reference]plugin.Provider)}
// iter, err := NewEvalSource(ctx, runInfo, nil, false).Iterate(context.Background(), Options{}, providerSource)
// assert.NoError(t, err)
// registered181, registered182 := false, false
// for {
// event, err := iter.Next()
// assert.NoError(t, err)
// if event == nil {
// break
// }
// switch e := event.(type) {
// case RegisterResourceEvent:
// goal := e.Goal()
// urn, id := newURN(goal.Type, goal.Name, goal.Parent), resource.ID("id")
// if providers.IsProviderType(goal.Type) {
// switch goal.Name {
// case "default_0_18_1":
// assert.False(t, registered181)
// registered181 = true
// case "default_0_18_2":
// assert.False(t, registered182)
// registered182 = true
// }
// ref, err := providers.NewReference(urn, id)
// assert.NoError(t, err)
// _, ok := providerSource.GetProvider(ref)
// assert.False(t, ok)
// providerSource.registerProvider(ref, noopProvider)
// }
// e.Done(&RegisterResult{
// State: resource.NewState(goal.Type, urn, goal.Custom, false, id, goal.Properties, resource.PropertyMap{},
// goal.Parent, goal.Protect, false, goal.Dependencies, nil, goal.Provider, goal.PropertyDependencies,
// false, nil),
// })
// }
// }
// assert.True(t, registered181)
// assert.True(t, registered182)
// }
func TestResourceInheritsOptionsFromParent(t *testing.T) {
t.Parallel()
tests := []struct {
name string
parentDeletedWith resource.URN
childDeletedWith resource.URN
wantDeletedWith resource.URN
}{
{
// Children missing DeletedWith should inherit DeletedWith
name: "inherit",
parentDeletedWith: "parent-deleted-with",
childDeletedWith: "",
wantDeletedWith: "parent-deleted-with",
},
{
// Children with DeletedWith should not inherit DeletedWith
name: "override",
parentDeletedWith: "parent-deleted-with",
childDeletedWith: "this-value-is-set-and-should-not-change",
wantDeletedWith: "this-value-is-set-and-should-not-change",
},
{
// Children with DeletedWith should not inherit empty DeletedWith.
name: "keep",
parentDeletedWith: "",
childDeletedWith: "this-value-is-set-and-should-not-change",
wantDeletedWith: "this-value-is-set-and-should-not-change",
},
}
for _, tt := range tests {
test := tt
t.Run(test.name, func(t *testing.T) {
t.Parallel()
parentURN := resource.NewURN("a", "proj", "d:e:f", "a:b:c", "parent")
parentGoal := &resource.Goal{
Parent: "",
Type: parentURN.Type(),
DeletedWith: test.parentDeletedWith,
}
childURN := resource.NewURN("a", "proj", "d:e:f", "a:b:c", "child")
goal := &resource.Goal{
Parent: parentURN,
Type: childURN.Type(),
Name: childURN.Name(),
DeletedWith: test.childDeletedWith,
}
newGoal := inheritFromParent(*goal, *parentGoal)
assert.Equal(t, test.wantDeletedWith, newGoal.DeletedWith)
})
}
}
func TestRequestFromNodeJS(t *testing.T) {
t.Parallel()
ctx := context.Background()
newContext := func(md map[string]string) context.Context {
return metadata.NewIncomingContext(ctx, metadata.New(md))
}
tests := []struct {
name string
ctx context.Context
expected bool
}{
{
name: "no metadata",
ctx: ctx,
expected: false,
},
{
name: "empty metadata",
ctx: newContext(map[string]string{}),
expected: false,
},
{
name: "user-agent foo/1.0",
ctx: newContext(map[string]string{"user-agent": "foo/1.0"}),
expected: false,
},
{
name: "user-agent grpc-node-js/1.8.15",
ctx: newContext(map[string]string{"user-agent": "grpc-node-js/1.8.15"}),
expected: true,
},
{
name: "pulumi-runtime foo",
ctx: newContext(map[string]string{"pulumi-runtime": "foo"}),
expected: false,
},
{
name: "pulumi-runtime nodejs",
ctx: newContext(map[string]string{"pulumi-runtime": "nodejs"}),
expected: true,
},
{
// Always respect the value of pulumi-runtime, regardless of the user-agent.
name: "user-agent grpc-go/1.54.0, pulumi-runtime nodejs",
ctx: newContext(map[string]string{
"user-agent": "grpc-go/1.54.0",
"pulumi-runtime": "nodejs",
}),
expected: true,
},
{
name: "user-agent grpc-node-js/1.8.15, pulumi-runtime python",
ctx: newContext(map[string]string{
"user-agent": "grpc-node-js/1.8.15",
"pulumi-runtime": "python",
}),
expected: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := requestFromNodeJS(tt.ctx)
assert.Equal(t, tt.expected, actual)
})
}
}
func TestTransformAliasForNodeJSCompat(t *testing.T) {
t.Parallel()
sptr := func(s string) *string {
return &s
}
bptr := func(b bool) *bool {
return &b
}
makeAlias := func(parent *string, noParent *bool, name string) *pulumirpc.Alias {
spec := &pulumirpc.Alias_Spec{
Name: name,
}
if parent != nil {
spec.Parent = &pulumirpc.Alias_Spec_ParentUrn{ParentUrn: *parent}
}
if noParent != nil {
spec.Parent = &pulumirpc.Alias_Spec_NoParent{NoParent: *noParent}
}
return &pulumirpc.Alias{
Alias: &pulumirpc.Alias_Spec_{
Spec: spec,
},
}
}
tests := []struct {
name string
input *pulumirpc.Alias
expected *pulumirpc.Alias
}{
{
name: `{Parent: "", NoParent: true} (transformed)`,
input: makeAlias(nil, bptr(true), ""),
expected: makeAlias(nil, nil, ""),
},
{
name: `{Parent: "", NoParent: false} (transformed)`,
input: makeAlias(sptr(""), nil, ""),
expected: makeAlias(nil, bptr(true), ""),
},
{
name: `{Parent: "", NoParent: false, Name: "name"} (transformed)`,
input: makeAlias(sptr(""), nil, "name"),
expected: makeAlias(nil, bptr(true), "name"),
},
{
name: `{Parent: "", NoParent: true, Name: "name"} (transformed)`,
input: makeAlias(nil, bptr(true), "name"),
expected: makeAlias(nil, nil, "name"),
},
{
name: `{Parent: "foo", NoParent: false} (no transform)`,
input: makeAlias(sptr("foo"), nil, ""),
expected: makeAlias(sptr("foo"), nil, ""),
},
{
name: `{Parent: "foo", NoParent: false, Name: "name"} (no transform)`,
input: makeAlias(sptr("foo"), nil, "name"),
expected: makeAlias(sptr("foo"), nil, "name"),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := transformAliasForNodeJSCompat(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}
type streamInvokeMock struct {
SendF func(res *pulumirpc.InvokeResponse) error
SendMsgF func(m interface{}) error
RecvMsgF func(m interface{}) error
}
func (s *streamInvokeMock) Send(res *pulumirpc.InvokeResponse) error {
if s.SendF != nil {
return s.SendF(res)
}
panic("unimplemented")
}
func (s *streamInvokeMock) SendMsg(m interface{}) error {
if s.SendF != nil {
return s.SendMsgF(m)
}
panic("unimplemented")
}
func (s *streamInvokeMock) RecvMsg(m interface{}) error {
if s.RecvMsgF != nil {
return s.RecvMsgF(m)
}
panic("unimplemented")
}
func (s *streamInvokeMock) SetHeader(metadata.MD) error { panic("unimplemented") }
func (s *streamInvokeMock) SendHeader(metadata.MD) error { panic("unimplemented") }
func (s *streamInvokeMock) SetTrailer(metadata.MD) { panic("unimplemented") }
func (s *streamInvokeMock) Context() context.Context { panic("unimplemented") }
var _ pulumirpc.ResourceMonitor_StreamInvokeServer = (*streamInvokeMock)(nil)
type providerSourceMock struct {
Provider plugin.Provider
}
func (ps *providerSourceMock) GetProvider(ref providers.Reference) (plugin.Provider, bool) {
return ps.Provider, ps.Provider != nil
}
var _ ProviderSource = (*providerSourceMock)(nil)
func TestStreamInvoke(t *testing.T) {
t.Parallel()
t.Run("ok", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
StreamInvokeF: func(
_ context.Context,
req plugin.StreamInvokeRequest,
) (plugin.StreamInvokeResponse, error) {
called = true
require.NoError(t, req.OnNext(resource.PropertyMap{}))
return plugin.StreamInvokeResponse{}, nil
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error { return nil },
RecvMsgF: func(m interface{}) error { return nil },
})
// Ensure the channel is read from.
wg.Wait()
assert.NoError(t, err)
assert.True(t, called)
})
t.Run("StreamInvoke provider error", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
expectedErr := errors.New("expected error")
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
StreamInvokeF: func(
_ context.Context,
req plugin.StreamInvokeRequest,
) (plugin.StreamInvokeResponse, error) {
called = true
require.NoError(t, req.OnNext(resource.PropertyMap{}))
return plugin.StreamInvokeResponse{}, expectedErr
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error { return nil },
RecvMsgF: func(m interface{}) error { return nil },
})
// Ensure the channel is read from.
wg.Wait()
assert.ErrorIs(t, err, expectedErr)
assert.True(t, called)
})
t.Run("StreamInvoke provider failures", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
StreamInvokeF: func(
_ context.Context,
req plugin.StreamInvokeRequest,
) (plugin.StreamInvokeResponse, error) {
called = true
require.NoError(t, req.OnNext(resource.PropertyMap{}))
return plugin.StreamInvokeResponse{
Failures: []plugin.CheckFailure{
{
Property: "some-property",
Reason: "expect failure",
},
},
}, nil
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
var hasFailures bool
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error {
if len(res.Failures) > 0 {
hasFailures = true
}
return nil
},
RecvMsgF: func(m interface{}) error { return nil },
})
// Ensure the channel is read from.
wg.Wait()
assert.NoError(t, err)
assert.True(t, called)
assert.True(t, hasFailures)
})
t.Run("unknown provider", func(t *testing.T) {
t.Parallel()
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
return nil
})
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, programF)(),
nil, "", nil, false, nil)
assert.NoError(t, err)
builtins := newBuiltinProvider(&deploytest.BackendClient{}, nil, plugctx.Diag)
reg := providers.NewRegistry(plugctx.Host, false, builtins)
providerRegChan := make(chan *registerResourceEvent, 100)
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, reg, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error { return nil },
RecvMsgF: func(m interface{}) error { return nil },
})
// Ensure the channel is read from.
wg.Wait()
assert.ErrorContains(t, err, "unknown provider")
})
}
func TestStreamInvokeQuery(t *testing.T) {
t.Parallel()
t.Run("check failure", func(t *testing.T) {
t.Parallel()
var called bool
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
StreamInvokeF: func(
_ context.Context,
req plugin.StreamInvokeRequest,
) (plugin.StreamInvokeResponse, error) {
called = true
require.NoError(t, req.OnNext(resource.PropertyMap{}))
return plugin.StreamInvokeResponse{
Failures: []plugin.CheckFailure{
{
Property: resource.PropertyKey("fake-key"),
Reason: "I said so",
},
},
}, nil
},
}, nil
}),
}
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
return nil
})
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, programF, loaders...)(),
nil, "", nil, false, nil)
assert.NoError(t, err)
cancel := context.Background()
builtins := newBuiltinProvider(&deploytest.BackendClient{}, nil, plugctx.Diag)
reg := providers.NewRegistry(plugctx.Host, false, builtins)
providerRegErrChan := make(chan error)
mon, err := newQueryResourceMonitor(builtins, nil, nil, reg, plugctx,
providerRegErrChan, opentracing.SpanFromContext(cancel), &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
})
require.NoError(t, err)
var failures []*pulumirpc.CheckFailure
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error {
failures = res.GetFailures()
return nil
},
RecvMsgF: func(m interface{}) error { return nil },
})
assert.NoError(t, err)
assert.Equal(t, []*pulumirpc.CheckFailure{
{
Property: "fake-key",
Reason: "I said so",
},
}, failures)
assert.True(t, called)
})
t.Run("ok", func(t *testing.T) {
t.Parallel()
var called bool
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
StreamInvokeF: func(
_ context.Context,
req plugin.StreamInvokeRequest,
) (plugin.StreamInvokeResponse, error) {
called = true
require.NoError(t, req.OnNext(resource.PropertyMap{}))
return plugin.StreamInvokeResponse{}, nil
},
}, nil
}),
}
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
return nil
})
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, programF, loaders...)(),
nil, "", nil, false, nil)
assert.NoError(t, err)
cancel := context.Background()
builtins := newBuiltinProvider(&deploytest.BackendClient{}, nil, plugctx.Diag)
reg := providers.NewRegistry(plugctx.Host, false, builtins)
providerRegErrChan := make(chan error)
mon, err := newQueryResourceMonitor(builtins, nil, nil, reg, plugctx,
providerRegErrChan, opentracing.SpanFromContext(cancel), &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "test"},
})
require.NoError(t, err)
err = mon.StreamInvoke(&pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
}, &streamInvokeMock{
SendF: func(res *pulumirpc.InvokeResponse) error { return nil },
RecvMsgF: func(m interface{}) error { return nil },
})
assert.NoError(t, err)
assert.True(t, called)
})
}
type decrypterMock struct {
DecryptValueF func(
ctx context.Context, ciphertext string) (string, error)
BulkDecryptF func(
ctx context.Context, ciphertexts []string) (map[string]string, error)
}
var _ config.Decrypter = (*decrypterMock)(nil)
func (d *decrypterMock) DecryptValue(ctx context.Context, ciphertext string) (string, error) {
if d.DecryptValueF != nil {
return d.DecryptValueF(ctx, ciphertext)
}
panic("unimplemented")
}
func (d *decrypterMock) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
if d.BulkDecryptF != nil {
return d.BulkDecryptF(ctx, ciphertexts)
}
panic("unimplemented")
}
func TestEvalSource(t *testing.T) {
t.Parallel()
t.Run("Stack", func(t *testing.T) {
t.Parallel()
src := &evalSource{
runinfo: &EvalRunInfo{
Target: &Target{
Name: tokens.MustParseStackName("target-name"),
},
},
}
assert.Equal(t, tokens.MustParseStackName("target-name"), src.Stack())
})
t.Run("Info", func(t *testing.T) {
t.Parallel()
runinfo := &EvalRunInfo{
Target: &Target{
Name: tokens.MustParseStackName("target-name"),
},
}
src := &evalSource{
runinfo: runinfo,
}
assert.Equal(t, runinfo, src.Info())
})
t.Run("Iterate", func(t *testing.T) {
t.Parallel()
t.Run("config decrypt value error", func(t *testing.T) {
t.Parallel()
var decrypterCalled bool
src := &evalSource{
plugctx: &plugin.Context{
Diag: &deploytest.NoopSink{},
},
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("target-name"),
Config: config.Map{
config.MustMakeKey("test", "secret"): config.NewSecureValue("secret"),
},
Decrypter: &decrypterMock{
DecryptValueF: func(ctx context.Context, ciphertext string) (string, error) {
decrypterCalled = true
return "", errors.New("expected fail")
},
},
},
},
}
_, err := src.Iterate(context.Background(), &providerSourceMock{})
assert.ErrorContains(t, err, "failed to decrypt config")
assert.True(t, decrypterCalled)
})
t.Run("failed to convert config to map", func(t *testing.T) {
t.Parallel()
var called int
var decrypterCalled bool
src := &evalSource{
plugctx: &plugin.Context{
Diag: &deploytest.NoopSink{},
},
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Target: &Target{
Config: config.Map{
config.MustMakeKey("test", "secret"): config.NewSecureValue("secret"),
},
Decrypter: &decrypterMock{
DecryptValueF: func(ctx context.Context, ciphertext string) (string, error) {
decrypterCalled = true
if called == 0 {
// Will cause the next invocation to fail.
called++
return "", nil
}
return "", errors.New("expected fail")
},
},
},
},
}
_, err := src.Iterate(context.Background(), &providerSourceMock{})
assert.ErrorContains(t, err, "failed to convert config to map")
assert.True(t, decrypterCalled)
})
})
}
func TestResmonCancel(t *testing.T) {
t.Parallel()
done := make(chan error)
rm := &resmon{
cancel: make(chan bool, 10),
done: done,
}
err := errors.New("my error")
go func() {
// This ensures that cancel doesn't hang.
done <- err
}()
// Cancel always returns nil or a joinErrors.
assert.Equal(t, errors.Join(err), rm.Cancel())
}
func TestSourceEvalServeOptions(t *testing.T) {
t.Parallel()
assert.Len(t,
sourceEvalServeOptions(nil, opentracing.SpanFromContext(context.Background()), "" /* logFile */),
2,
)
assert.Len(t,
sourceEvalServeOptions(&plugin.Context{
DebugTraceMutex: &sync.Mutex{},
}, opentracing.SpanFromContext(context.Background()), "logFile.log"),
4,
)
}
func TestEvalSourceIterator(t *testing.T) {
t.Parallel()
t.Run("Close", func(t *testing.T) {
t.Parallel()
var called bool
iter := &evalSourceIterator{
mon: &mockResmon{
CancelF: func() error {
called = true
return nil
},
},
}
iter.Close()
assert.True(t, called)
})
t.Run("ResourceMonitor", func(t *testing.T) {
t.Parallel()
var called bool
mon := &mockResmon{
CancelF: func() error { called = true; return nil },
}
iter := &evalSourceIterator{
mon: mon,
}
iter.Close()
assert.Equal(t, mon, iter.ResourceMonitor())
assert.True(t, called)
})
t.Run("Next", func(t *testing.T) {
t.Parallel()
t.Run("iter.done", func(t *testing.T) {
t.Parallel()
iter := &evalSourceIterator{
done: true,
}
evt, err := iter.Next()
assert.Nil(t, evt)
assert.NoError(t, err)
})
})
t.Run("Abort", func(t *testing.T) {
t.Parallel()
abortChan := make(chan bool)
iter := &evalSourceIterator{
mon: &resmon{
abortChan: abortChan,
},
}
go func() {
abortChan <- true
}()
evt, err := iter.Next()
assert.ErrorContains(t, err, "EvalSourceIterator aborted")
assert.Nil(t, evt)
evt, err = iter.Next()
assert.ErrorContains(t, err, "EvalSourceIterator aborted")
assert.Nil(t, evt)
})
}
func TestParseSourcePosition(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
input *pulumirpc.SourcePosition
expected string
errContains string
}{
{
name: "NilInput",
input: nil,
expected: "",
errContains: "",
},
{
name: "InvalidLine",
input: &pulumirpc.SourcePosition{Line: 0},
expected: "",
errContains: "invalid line number 0",
},
{
name: "InvalidColumn",
input: &pulumirpc.SourcePosition{Line: 1, Column: -1},
expected: "",
errContains: "invalid column number -1",
},
{
name: "InvalidURI",
input: &pulumirpc.SourcePosition{Line: 1, Column: 1, Uri: ":invalid-uri:"},
expected: "",
errContains: `parse ":invalid-uri:": missing protocol scheme`,
},
{
name: "UnrecognizedScheme",
input: &pulumirpc.SourcePosition{Line: 1, Column: 1, Uri: "http://example.com/file.txt"},
expected: "",
errContains: "unrecognized scheme \"http\"",
},
{
name: "NonAbsolutePath",
input: &pulumirpc.SourcePosition{Line: 1, Column: 1, Uri: "file:relative/path/file.txt"},
expected: "",
errContains: "source positions must include absolute paths",
},
}
for _, tt := range testCases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &sourcePositions{
projectRoot: "/absolute/path/",
}
result, err := s.parseSourcePosition(tt.input)
assert.Equal(t, tt.expected, result)
if tt.errContains != "" {
assert.ErrorContains(t, err, tt.errContains, result)
assert.Equal(t, tt.expected, result)
} else {
assert.NoError(t, err)
}
})
}
}
type configSourceMock struct {
GetPackageConfigF func(pkg tokens.Package) (resource.PropertyMap, error)
}
var _ plugin.ConfigSource = (*configSourceMock)(nil)
func (c *configSourceMock) GetPackageConfig(pkg tokens.Package) (resource.PropertyMap, error) {
if c.GetPackageConfigF != nil {
return c.GetPackageConfigF(pkg)
}
panic("unimplemented")
}
func TestDefaultProviders(t *testing.T) {
t.Parallel()
t.Run("normalizeProviderRequest", func(t *testing.T) {
t.Parallel()
t.Run("use defaultProvider", func(t *testing.T) {
t.Parallel()
v1 := semver.MustParse("0.1.0")
d := &defaultProviders{
defaultProviderInfo: map[tokens.Package]workspace.PluginSpec{
tokens.Package("pkg"): {
Version: &v1,
PluginDownloadURL: "github://owner/repo",
Checksums: map[string][]byte{"key": []byte("expected-checksum-value")},
},
},
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return resource.PropertyMap{}, nil
},
},
}
req := d.normalizeProviderRequest(providers.NewProviderRequest(tokens.Package("pkg"), nil, "", nil, nil))
assert.NotNil(t, req)
assert.Equal(t, &v1, req.Version())
assert.Equal(t, "github://owner/repo", req.PluginDownloadURL())
assert.Equal(t, map[string][]byte{"key": []byte("expected-checksum-value")}, req.PluginChecksums())
})
})
t.Run("newRegisterDefaultProviderEvent", func(t *testing.T) {
t.Parallel()
t.Run("error in GetPackageConfig()", func(t *testing.T) {
t.Parallel()
expectedErr := errors.New("expected error")
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, expectedErr
},
},
}
_, _, err := d.newRegisterDefaultProviderEvent(providers.ProviderRequest{})
assert.ErrorIs(t, err, expectedErr)
})
})
t.Run("handleRequest", func(t *testing.T) {
t.Parallel()
t.Run("error in shouldDenyRequest", func(t *testing.T) {
t.Parallel()
expectedErr := errors.New("expected error")
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, expectedErr
},
},
}
_, err := d.handleRequest(providers.ProviderRequest{})
assert.ErrorIs(t, err, expectedErr)
})
t.Run("error in newRegisterDefaultProviderEvent", func(t *testing.T) {
t.Parallel()
expectedErr := errors.New("expected error")
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
if pkg == "pulumi" {
// Enables shouldDenyRequest(req) to succeed as it always calls using
// "pulumi".
return nil, nil
}
return nil, expectedErr
},
},
}
_, err := d.handleRequest(providers.ProviderRequest{})
assert.ErrorIs(t, err, expectedErr)
})
t.Run("error due to cancel before registration", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
d := &defaultProviders{
cancel: cancel,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
}
_, err := d.handleRequest(providers.ProviderRequest{})
assert.ErrorIs(t, err, context.Canceled)
})
t.Run("error cancel after registration, but before registration result", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
providerRegChan := make(chan *registerResourceEvent, 1)
d := &defaultProviders{
cancel: cancel,
providerRegChan: providerRegChan,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
}
go func() {
// Cancel after reading the registration.
<-providerRegChan
cancel <- true
}()
_, err := d.handleRequest(providers.ProviderRequest{})
assert.ErrorIs(t, err, context.Canceled)
})
})
t.Run("shouldDenyRequest", func(t *testing.T) {
t.Parallel()
t.Run("GetPackageConfigErr", func(t *testing.T) {
t.Parallel()
expectedErr := errors.New("expected error")
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, expectedErr
},
},
}
_, err := d.shouldDenyRequest(providers.ProviderRequest{})
assert.ErrorIs(t, err, expectedErr)
})
t.Run("disable-default-providers", func(t *testing.T) {
t.Parallel()
t.Run("invalid value", func(t *testing.T) {
t.Parallel()
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return resource.PropertyMap{
"disable-default-providers": resource.NewNumberProperty(100),
}, nil
},
},
}
_, err := d.shouldDenyRequest(providers.ProviderRequest{})
assert.ErrorContains(t, err, "Unexpected encoding of pulumi:disable-default-providers")
})
t.Run("empty value", func(t *testing.T) {
t.Parallel()
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return resource.PropertyMap{
"disable-default-providers": resource.NewStringProperty(""),
}, nil
},
},
}
res, err := d.shouldDenyRequest(providers.ProviderRequest{})
assert.NoError(t, err)
assert.False(t, res)
})
t.Run("invalid list", func(t *testing.T) {
t.Run("bad json", func(t *testing.T) {
t.Parallel()
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return resource.PropertyMap{
"disable-default-providers": resource.NewStringProperty("[[["),
}, nil
},
},
}
res, err := d.shouldDenyRequest(providers.ProviderRequest{})
assert.ErrorContains(t, err, "Failed to parse [[[")
assert.True(t, res)
})
t.Run("mixed list values", func(t *testing.T) {
t.Parallel()
d := &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return resource.PropertyMap{
"disable-default-providers": resource.NewStringProperty(`["foo", 2, 3]`),
}, nil
},
},
}
res, err := d.shouldDenyRequest(providers.ProviderRequest{})
assert.ErrorContains(t, err, "must be a string")
assert.True(t, res)
})
})
})
})
t.Run("Cancel", func(t *testing.T) {
t.Parallel()
t.Run("serve respects cancel", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
d := &defaultProviders{
cancel: cancel,
}
d.serve()
})
t.Run("getDefaultProviderRef respects cancel", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
d := &defaultProviders{
cancel: cancel,
}
_, err := d.getDefaultProviderRef(providers.ProviderRequest{})
assert.ErrorIs(t, err, context.Canceled)
})
})
}
func TestGetProviderReference(t *testing.T) {
t.Parallel()
t.Run("bad-reference", func(t *testing.T) {
t.Parallel()
_, err := getProviderReference(nil, providers.ProviderRequest{}, "bad-reference")
assert.ErrorContains(t, err, "could not parse provider reference")
})
t.Run("provider-reference-error", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
_, err := getProviderReference(&defaultProviders{
cancel: cancel,
}, providers.ProviderRequest{}, "")
assert.ErrorIs(t, err, context.Canceled)
})
}
func TestGetProviderFromSource(t *testing.T) {
t.Parallel()
t.Run("bad reference", func(t *testing.T) {
t.Parallel()
_, err := getProviderFromSource(nil, nil, providers.ProviderRequest{}, "bad-reference", "")
assert.ErrorContains(t, err, "getProviderFromSource")
})
}
func TestParseProviderRequest(t *testing.T) {
t.Parallel()
t.Run("bad version", func(t *testing.T) {
t.Parallel()
_, err := parseProviderRequest("", "bad-version", "", nil, nil)
assert.ErrorContains(t, err, "No Major.Minor.Patch elements found")
})
}
func TestInvoke(t *testing.T) {
t.Parallel()
t.Run("bad version", func(t *testing.T) {
t.Parallel()
rm := &resmon{}
_, err := rm.Invoke(context.Background(), &pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
Version: "bad-version",
})
assert.ErrorContains(t, err, "No Major.Minor.Patch elements found")
})
t.Run("error in invoke", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
expectedErr := errors.New("expected error")
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
InvokeF: func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error) {
called = true
return plugin.InvokeResponse{}, expectedErr
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
_, err = mon.Invoke(context.Background(), &pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
})
assert.ErrorContains(t, err, "returned an error")
// Ensure the channel is read from.
wg.Wait()
assert.True(t, called)
})
t.Run("error in invoke", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
InvokeF: func(context.Context, plugin.InvokeRequest) (plugin.InvokeResponse, error) {
called = true
return plugin.InvokeResponse{
Failures: []plugin.CheckFailure{
{
Property: "some-property",
Reason: "expect failure",
},
},
}, nil
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
res, err := mon.Invoke(context.Background(), &pulumirpc.ResourceInvokeRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
})
assert.NoError(t, err)
assert.Equal(t, "some-property", res.Failures[0].Property)
assert.Equal(t, "expect failure", res.Failures[0].Reason)
// Ensure the channel is read from.
wg.Wait()
assert.True(t, called)
})
}
func TestCall(t *testing.T) {
t.Parallel()
t.Run("bad version", func(t *testing.T) {
t.Parallel()
rm := &resmon{}
_, err := rm.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: "pkgA:index:func",
Version: "bad-version",
})
assert.ErrorContains(t, err, "No Major.Minor.Patch elements found")
})
t.Run("error in call", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
var called bool
expectedErr := errors.New("expected error")
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
CallF: func(context.Context, plugin.CallRequest, *deploytest.ResourceMonitor) (plugin.CallResponse, error) {
called = true
return plugin.CallResponse{}, expectedErr
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
abortChan := make(chan bool)
cancel := make(chan bool)
mon.abortChan = abortChan
mon.cancel = cancel
wg := &sync.WaitGroup{}
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
go func() {
// the resource monitor should send a true value to the abort channel to indicate that the
// iterator should shut down.
val, ok := <-abortChan
assert.True(t, ok)
assert.True(t, val)
close(cancel)
}()
_, err = mon.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
})
assert.ErrorContains(t, err, "returned an error")
// Ensure the channel is read from.
wg.Wait()
assert.True(t, called)
})
t.Run("handles args and arg dependencies", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
wg := &sync.WaitGroup{}
defer wg.Wait()
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
var called bool
expectedErr := errors.New("expected error")
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
CallF: func(
_ context.Context,
req plugin.CallRequest,
_ *deploytest.ResourceMonitor,
) (plugin.CallResponse, error) {
assert.Equal(t,
resource.PropertyMap{
"test": resource.NewStringProperty("test-value"),
},
req.Args)
require.Equal(t, 1, len(req.Options.ArgDependencies))
assert.ElementsMatch(t,
[]resource.URN{
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"urn:pulumi:stack::project::type::dep3",
},
req.Options.ArgDependencies["test"])
called = true
return plugin.CallResponse{}, expectedErr
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
abortChan := make(chan bool)
cancel := make(chan bool)
mon.abortChan = abortChan
mon.cancel = cancel
go func() {
// the resource monitor should send a true value to the abort channel to indicate that the
// iterator should shut down.
val, ok := <-abortChan
assert.True(t, ok)
assert.True(t, val)
close(cancel)
}()
args, err := plugin.MarshalProperties(resource.PropertyMap{
"test": resource.NewStringProperty("test-value"),
}, plugin.MarshalOptions{})
require.NoError(t, err)
_, err = mon.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
Args: args,
ArgDependencies: map[string]*pulumirpc.ResourceCallRequest_ArgumentDependencies{
"test": {
Urns: []string{
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"urn:pulumi:stack::project::type::dep3",
},
},
},
})
assert.ErrorContains(t, err, "returned an error")
// Ensure the channel is read from.
assert.True(t, called)
})
t.Run("catch invalid arg dependencies", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
wg := &sync.WaitGroup{}
defer wg.Wait()
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
CallF: func(context.Context, plugin.CallRequest, *deploytest.ResourceMonitor) (plugin.CallResponse, error) {
assert.Fail(t, "Call should not be called")
return plugin.CallResponse{}, nil
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
args, err := plugin.MarshalProperties(resource.PropertyMap{
"test": resource.NewStringProperty("test-value"),
}, plugin.MarshalOptions{})
require.NoError(t, err)
_, err = mon.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
Args: args,
ArgDependencies: map[string]*pulumirpc.ResourceCallRequest_ArgumentDependencies{
"test": {
Urns: []string{
"invalid urn",
},
},
},
})
assert.ErrorContains(t, err, "invalid dependency")
})
t.Run("catch invalid arg dependencies", func(t *testing.T) {
t.Parallel()
plugctx, err := plugin.NewContext(
&deploytest.NoopSink{}, &deploytest.NoopSink{},
deploytest.NewPluginHostF(nil, nil, nil)(),
nil, "", nil, false, nil)
require.NoError(t, err)
providerRegChan := make(chan *registerResourceEvent, 1)
wg := &sync.WaitGroup{}
defer wg.Wait()
wg.Add(1)
// Needed so defaultProviders.handleRequest() doesn't hang.
go func() {
evt := <-providerRegChan
evt.done <- &RegisterResult{
State: &resource.State{
ID: "b2562429-e255-4b8f-904b-2bd239301ff2",
URN: "urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
},
}
wg.Done()
}()
mon, err := newResourceMonitor(&evalSource{
runinfo: &EvalRunInfo{
ProjectRoot: "/",
Pwd: "/",
Program: ".",
Proj: &workspace.Project{Name: "proj"},
Target: &Target{
Name: tokens.MustParseStackName("stack"),
},
},
plugctx: plugctx,
}, &providerSourceMock{
Provider: &deploytest.Provider{
CallF: func(context.Context, plugin.CallRequest, *deploytest.ResourceMonitor) (plugin.CallResponse, error) {
return plugin.CallResponse{
Return: resource.PropertyMap{
"result": resource.NewNumberProperty(100),
},
ReturnDependencies: map[resource.PropertyKey][]resource.URN{
"prop": {
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"urn:pulumi:stack::project::type::dep3",
},
},
Failures: []plugin.CheckFailure{
{
Property: "some-prop",
Reason: "expected failure",
},
},
}, nil
},
},
}, providerRegChan, nil, nil, nil, nil, opentracing.SpanFromContext(context.Background()))
require.NoError(t, err)
args, err := plugin.MarshalProperties(resource.PropertyMap{
"test": resource.NewStringProperty("test-value"),
}, plugin.MarshalOptions{})
require.NoError(t, err)
res, err := mon.Call(context.Background(), &pulumirpc.ResourceCallRequest{
Tok: "pkgA:index:func",
Version: "1.0.0",
Args: args,
})
assert.NoError(t, err)
assert.Equal(t,
map[string]interface{}{
"result": float64(100),
}, res.Return.AsMap())
assert.Equal(t,
[]string{
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"urn:pulumi:stack::project::type::dep3",
}, res.ReturnDependencies["prop"].Urns)
assert.Equal(t, &pulumirpc.CheckFailure{
Property: "some-prop",
Reason: "expected failure",
}, res.Failures[0])
})
}
func TestReadResource(t *testing.T) {
t.Parallel()
t.Run("bad parent", func(t *testing.T) {
t.Parallel()
rm := &resmon{}
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "foo:bar:some-type",
Parent: "invalid-parent",
})
assert.ErrorContains(t, err, "invalid parent URN")
})
t.Run("handles error from parseProviderRequest", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
rm := &resmon{
defaultProviders: &defaultProviders{
cancel: cancel,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "foo:bar:some-type",
Version: "1.0.0",
})
assert.ErrorIs(t, err, context.Canceled)
})
t.Run("handles invalid dependencies", func(t *testing.T) {
t.Parallel()
rm := &resmon{
defaultProviders: &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "pulumi:providers:fake-provider",
Version: "1.0.0",
Dependencies: []string{
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"invalidURN",
},
})
assert.ErrorContains(t, err, "invalid URN")
})
t.Run("handles invalid dependencies", func(t *testing.T) {
t.Parallel()
rm := &resmon{
defaultProviders: &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "pulumi:providers:fake-provider",
Version: "1.0.0",
Dependencies: []string{
"urn:pulumi:stack::project::type::dep1",
"urn:pulumi:stack::project::type::dep2",
"invalidURN",
},
})
assert.ErrorContains(t, err, "invalid URN")
})
t.Run("handles additional secret outputs", func(t *testing.T) {
t.Parallel()
regReadChan := make(chan *readResourceEvent, 1)
rm := &resmon{
regReadChan: regReadChan,
defaultProviders: &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
evt := <-regReadChan
assert.Equal(t, []resource.PropertyKey{"foo"}, evt.additionalSecretOutputs)
evt.done <- &ReadResult{
State: &resource.State{},
}
wg.Done()
}()
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "pulumi:providers:fake-provider",
Version: "1.0.0",
AdditionalSecretOutputs: []string{"foo"},
})
assert.NoError(t, err)
wg.Wait()
})
t.Run("resource monitor shut down while sending resource registration", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
rm := &resmon{
cancel: cancel,
defaultProviders: &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
cancel <- true
wg.Done()
}()
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "pulumi:providers:fake-provider",
Version: "1.0.0",
})
assert.ErrorContains(t, err, "resource monitor shut down while sending resource registration")
wg.Wait()
})
t.Run("resource monitor shut down while waiting on step's done channel", func(t *testing.T) {
t.Parallel()
// requests := make(chan
cancel := make(chan bool, 1)
regReadChan := make(chan *readResourceEvent, 1)
rm := &resmon{
regReadChan: regReadChan,
cancel: cancel,
defaultProviders: &defaultProviders{
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
<-regReadChan
cancel <- true
wg.Done()
}()
_, err := rm.ReadResource(context.Background(), &pulumirpc.ReadResourceRequest{
Type: "pulumi:providers:fake-provider",
Version: "1.0.0",
})
assert.ErrorContains(t, err, "resource monitor shut down while waiting on step's done channel")
wg.Wait()
})
}
func TestRegisterResource(t *testing.T) {
t.Parallel()
t.Run("gracefully handle cancellation", func(t *testing.T) {
t.Parallel()
t.Run("resource monitor shut down while sending resource registration", func(t *testing.T) {
t.Parallel()
cancel := make(chan bool, 1)
cancel <- true
rm := &resmon{
cancel: cancel,
}
_, err := rm.RegisterResource(context.Background(), &pulumirpc.RegisterResourceRequest{})
assert.ErrorContains(t, err, "resource monitor shut down while sending resource registration")
})
t.Run("resource monitor shut down while waiting on step's done channel", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
cancel := make(chan bool, 1)
go func() {
<-regChan
cancel <- true
}()
rm := &resmon{
regChan: regChan,
cancel: cancel,
}
_, err := rm.RegisterResource(context.Background(), &pulumirpc.RegisterResourceRequest{})
assert.ErrorContains(t, err, "resource monitor shut down while waiting on step's done channel")
})
t.Run("resource monitor shut down while waiting on step's done channel", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
cancel := make(chan bool, 1)
go func() {
<-regChan
cancel <- true
}()
rm := &resmon{
regChan: regChan,
cancel: cancel,
}
_, err := rm.RegisterResource(context.Background(), &pulumirpc.RegisterResourceRequest{})
assert.ErrorContains(t, err, "resource monitor shut down while waiting on step's done channel")
})
})
t.Run("remote handles improper version", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{}
req := &pulumirpc.RegisterResourceRequest{
Type: "foo:bar:some-type",
Version: "improper-version",
Remote: true,
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "No Major.Minor.Patch elements found")
})
t.Run("custom handles improper version", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{}
req := &pulumirpc.RegisterResourceRequest{
Type: "foo:bar:some-type",
Version: "improper-version",
Custom: true,
}
require.False(t, providers.IsProviderType(tokens.Type(req.GetType())))
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "No Major.Minor.Patch elements found")
})
t.Run("custom provider handles improper version", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{}
req := &pulumirpc.RegisterResourceRequest{
Type: "pulumi:providers:some-type",
Version: "improper-version",
Custom: true,
}
require.True(t, providers.IsProviderType(tokens.Type(req.GetType())))
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "passed invalid version")
})
t.Run("invalid alias URN", func(t *testing.T) {
t.Parallel()
rm := &resmon{}
req := &pulumirpc.RegisterResourceRequest{
Type: "pulumi:providers:some-type",
AliasURNs: []string{
"invalid-urn",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "invalid alias URN")
})
t.Run("invalid dependency on property", func(t *testing.T) {
t.Parallel()
rm := &resmon{
defaultProviders: &defaultProviders{
defaultProviderInfo: map[tokens.Package]workspace.PluginSpec{},
},
}
req := &pulumirpc.RegisterResourceRequest{
Type: "pulumi:providers:some-type",
Version: "1.0.0",
PropertyDependencies: map[string]*pulumirpc.RegisterResourceRequest_PropertyDependencies{
"invalid-urn": {
Urns: []string{"bad-urn"},
},
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "invalid dependency on property")
})
t.Run("remote resource", func(t *testing.T) {
t.Parallel()
t.Run("invalid provider in providers", func(t *testing.T) {
t.Parallel()
requests := make(chan defaultProviderRequest, 1)
go func() {
evt := <-requests
ref, err := providers.NewReference(
"urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
"b2562429-e255-4b8f-904b-2bd239301ff2")
require.NoError(t, err)
evt.response <- defaultProviderResponse{
ref: ref,
}
}()
rm := &resmon{
defaultProviders: &defaultProviders{
requests: requests,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
}
req := &pulumirpc.RegisterResourceRequest{
Version: "1.0.0",
Type: "pulumi:providers:some-type",
Remote: true,
Providers: map[string]string{
"name": "not-an-urn::id",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "could not parse provider reference")
})
t.Run("catch denied default provider", func(t *testing.T) {
t.Parallel()
requests := make(chan defaultProviderRequest, 1)
go func() {
evt := <-requests
ref, err := providers.NewReference(
"urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
"denydefaultprovider")
require.NoError(t, err)
evt.response <- defaultProviderResponse{
ref: ref,
}
}()
rm := &resmon{
defaultProviders: &defaultProviders{
requests: requests,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
providers: &providerSourceMock{
Provider: &deploytest.Provider{},
},
}
req := &pulumirpc.RegisterResourceRequest{
Version: "1.0.0",
Type: "pulumi:providers:some-type",
Remote: true,
Providers: map[string]string{
"missing": "urn:pulumi:stack::project::pulumi:providers:aws::prov-1::uuid",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err,
"Default provider for 'pulumi' disabled. 'pulumi:providers:some-type' must use an explicit provider.")
})
t.Run("unknown provider", func(t *testing.T) {
t.Parallel()
requests := make(chan defaultProviderRequest, 1)
go func() {
evt := <-requests
ref, err := providers.NewReference(
"urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
"b2562429-e255-4b8f-904b-2bd239301ff2")
require.NoError(t, err)
evt.response <- defaultProviderResponse{
ref: ref,
}
}()
rm := &resmon{
defaultProviders: &defaultProviders{
requests: requests,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
providers: &providerSourceMock{},
}
req := &pulumirpc.RegisterResourceRequest{
Version: "1.0.0",
Type: "pulumi:providers:some-type",
Remote: true,
Providers: map[string]string{
"missing": "urn:pulumi:stack::project::pulumi:providers:aws::prov-1::uuid",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "unknown provider")
})
})
t.Run("output dependencies", func(t *testing.T) {
t.Parallel()
requests := make(chan defaultProviderRequest, 1)
go func() {
evt := <-requests
ref, err := providers.NewReference(
"urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
"b2562429-e255-4b8f-904b-2bd239301ff2")
require.NoError(t, err)
evt.response <- defaultProviderResponse{
ref: ref,
}
}()
rm := &resmon{
defaultProviders: &defaultProviders{
requests: requests,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
providers: &providerSourceMock{
Provider: &deploytest.Provider{
DialMonitorF: func(
ctx context.Context, endpoint string,
) (*deploytest.ResourceMonitor, error) {
return nil, nil
},
ConstructF: func(
context.Context,
plugin.ConstructRequest,
*deploytest.ResourceMonitor,
) (plugin.ConstructResponse, error) {
return plugin.ConstructResponse{
OutputDependencies: map[resource.PropertyKey][]resource.URN{
"expected-key-1": {
"untrusted-urn-1",
},
"expected-key-2": {
"untrusted-urn-1",
"untrusted-urn-2",
},
},
}, nil
},
},
},
}
req := &pulumirpc.RegisterResourceRequest{
Version: "1.0.0",
Type: "pulumi:providers:some-type",
Remote: true,
}
res, err := rm.RegisterResource(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, []string{
"untrusted-urn-1",
}, res.PropertyDependencies["expected-key-1"].Urns)
assert.Equal(t, []string{
"untrusted-urn-1",
"untrusted-urn-2",
}, res.PropertyDependencies["expected-key-2"].Urns)
})
t.Run("not remote resource", func(t *testing.T) {
t.Parallel()
t.Run("additional secret keys", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{
regChan: regChan,
componentProviders: map[resource.URN]map[string]string{
"urn:pulumi:stack::project::type::foo": {
"urn:pulumi:stack::project::type::prov1": "",
"urn:pulumi:stack::project::type::prov2": "expected-value",
},
},
}
req := &pulumirpc.RegisterResourceRequest{
Provider: "urn:pulumi:stack::project::type::bar",
Parent: "urn:pulumi:stack::project::type::foo",
AdditionalSecretOutputs: []string{
"a",
"b",
"c",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t,
[]string{"a", "b", "c"},
req.AdditionalSecretOutputs)
})
t.Run("handle invalid custom timeouts", func(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{
regChan: regChan,
componentProviders: map[resource.URN]map[string]string{},
}
req := &pulumirpc.RegisterResourceRequest{
CustomTimeouts: &pulumirpc.RegisterResourceRequest_CustomTimeouts{
Create: "invalid",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "unable to parse customTimeout Value")
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{
regChan: regChan,
componentProviders: map[resource.URN]map[string]string{},
}
req := &pulumirpc.RegisterResourceRequest{
CustomTimeouts: &pulumirpc.RegisterResourceRequest_CustomTimeouts{
Delete: "invalid",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "unable to parse customTimeout Value")
})
t.Run("Update", func(t *testing.T) {
t.Parallel()
regChan := make(chan *registerResourceEvent, 1)
go func() {
evt := <-regChan
evt.done <- &RegisterResult{
State: &resource.State{},
}
}()
rm := &resmon{
regChan: regChan,
componentProviders: map[resource.URN]map[string]string{},
}
req := &pulumirpc.RegisterResourceRequest{
CustomTimeouts: &pulumirpc.RegisterResourceRequest_CustomTimeouts{
Update: "invalid",
},
}
_, err := rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "unable to parse customTimeout Value")
})
})
})
}
func TestValidationFailures(t *testing.T) {
t.Parallel()
s, _ := status.Newf(codes.InvalidArgument, "bad request").WithDetails(
&pulumirpc.InputPropertiesError{
Errors: []*pulumirpc.InputPropertiesError_PropertyError{
{
Reason: "missing",
PropertyPath: "testproperty",
},
{
Reason: "nested property error",
PropertyPath: "nested[0]",
},
},
},
)
badRequestError := s.Err()
cases := []struct {
name string
err error
expectedStderr string
}{
{
name: "regular error",
err: errors.New("test error"),
expectedStderr: "error: pulumi:providers:some-type resource 'some-name' has a problem: test error\n",
},
{
name: "bad request",
err: badRequestError,
expectedStderr: "error: pulumi:providers:some-type resource 'some-name' has a problem: bad request\n" +
"\t\t- property testproperty with value '{testvalue}' has a problem: missing\n" +
"\t\t- property nested[0] with value '{nestedvalue}' has a problem: nested property error\n",
},
}
for _, c := range cases {
cancel := make(chan bool)
abortChan := make(chan bool)
go func() {
// the resource monitor should send a true value to the abort channel to indicate that the
// iterator should shut down.
val, ok := <-abortChan
assert.True(t, ok)
assert.True(t, val)
close(cancel)
}()
requests := make(chan defaultProviderRequest, 1)
go func() {
evt := <-requests
ref, err := providers.NewReference(
"urn:pulumi:stack::project::pulumi:providers:aws::default_5_42_0",
"b2562429-e255-4b8f-904b-2bd239301ff2")
require.NoError(t, err)
evt.response <- defaultProviderResponse{
ref: ref,
}
}()
var stdout, stderr bytes.Buffer
rm := &resmon{
diagnostics: diagtest.MockSink(&stdout, &stderr),
cancel: cancel,
abortChan: abortChan,
defaultProviders: &defaultProviders{
requests: requests,
config: &configSourceMock{
GetPackageConfigF: func(pkg tokens.Package) (resource.PropertyMap, error) {
return nil, nil
},
},
},
providers: &providerSourceMock{
Provider: &deploytest.Provider{
DialMonitorF: func(
ctx context.Context, endpoint string,
) (*deploytest.ResourceMonitor, error) {
return nil, nil
},
ConstructF: func(ctx context.Context, req plugin.ConstructRequest, monitor *deploytest.ResourceMonitor,
) (plugin.ConstructResult, error) {
return plugin.ConstructResult{}, c.err
},
},
},
}
props := resource.PropertyMap{
"testproperty": resource.NewPropertyValue("testvalue"),
"nested": resource.NewArrayProperty(
[]resource.PropertyValue{resource.NewPropertyValue("nestedvalue")},
),
}
marshalledProps, err := plugin.MarshalProperties(props, plugin.MarshalOptions{})
assert.NoError(t, err)
req := &pulumirpc.RegisterResourceRequest{
Version: "1.0.0",
Type: "pulumi:providers:some-type",
Name: "some-name",
Remote: true,
Object: marshalledProps,
}
_, err = rm.RegisterResource(context.Background(), req)
assert.ErrorContains(t, err, "resource monitor shut down")
assert.Equal(t, c.expectedStderr, stderr.String())
assert.Equal(t, "", stdout.String())
}
}
func TestDowngradeOutputValues(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input resource.PropertyMap
expected resource.PropertyMap
}{
{
"plain",
resource.PropertyMap{
"foo": resource.NewStringProperty("hello"),
"bar": resource.NewNumberProperty(42),
},
resource.PropertyMap{
"foo": resource.NewStringProperty("hello"),
"bar": resource.NewNumberProperty(42),
},
},
{
"secret",
resource.PropertyMap{
"foo": resource.MakeSecret(resource.NewStringProperty("hello")),
},
resource.PropertyMap{
"foo": resource.MakeSecret(resource.NewStringProperty("hello")),
},
},
{
"output",
resource.PropertyMap{
"foo": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty("hello"),
Known: true,
}),
},
resource.PropertyMap{
"foo": resource.NewStringProperty("hello"),
},
},
{
"secret output",
resource.PropertyMap{
"foo": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty("hello"),
Known: true,
Secret: true,
}),
},
resource.PropertyMap{
"foo": resource.MakeSecret(resource.NewStringProperty("hello")),
},
},
{
"unknown output",
resource.PropertyMap{
"foo": resource.NewOutputProperty(resource.Output{}),
},
resource.PropertyMap{
"foo": resource.MakeComputed(resource.NewStringProperty("")),
},
},
{
"unknown resource reference",
resource.PropertyMap{
"foo": resource.NewResourceReferenceProperty(resource.ResourceReference{
URN: "urn:pulumi:stack::project::package:module:resource::name",
ID: resource.NewOutputProperty(resource.Output{}),
}),
},
resource.PropertyMap{
"foo": resource.NewResourceReferenceProperty(resource.ResourceReference{
URN: "urn:pulumi:stack::project::package:module:resource::name",
ID: resource.MakeComputed(resource.NewStringProperty("")),
}),
},
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := downgradeOutputValues(tt.input)
assert.Equal(t, tt.expected, actual)
})
}
}