This change adds a GetRequiredPlugins RPC method to the language host, enabling us to query it for its list of plugin requirements. This is language-specific because it requires looking at the set of dependencies (e.g., package.json files). It also adds a call up front during any update/preview operation to compute the set of plugins and require that they are present. These plugins are populated in the cache and will be used for all subsequent plugin-related operations during the engine's activity. We now cache the language plugins, so that we may load them eagerly too, which we never did previously due to the fact that we needed to pass the monitor address at load time. This was a bit bizarre anyhow, since it's really the Run RPC function that needs this information. So, to enable caching and eager loading -- which we need in order to invoke GetRequiredPlugins -- the "phone home" monitor RPC address is passed at Run time. In a subsequent change, we will switch to faulting in the plugins that are missing -- rather than erroring -- in addition to supporting the `pulumi plugin install` CLI command.
437 lines
14 KiB
Go
437 lines
14 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/pulumi/pulumi/pkg/diag"
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
|
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
"github.com/pulumi/pulumi/pkg/workspace"
|
|
)
|
|
|
|
// TestNullPlan creates a plan with no operations.
|
|
func TestNullPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, "", nil)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("null")}
|
|
prev := NewSnapshot(targ.Name, Manifest{}, nil)
|
|
plan := NewPlan(ctx, targ, prev, NullSource, nil, false)
|
|
iter, err := plan.Start(Options{})
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, iter)
|
|
next, err := iter.Next()
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, next)
|
|
err = ctx.Close()
|
|
assert.Nil(t, err)
|
|
}
|
|
|
|
// TestErrorPlan creates a plan that immediately fails with an unhandled error.
|
|
func TestErrorPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// First trigger an error from Iterate:
|
|
{
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, "", nil)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("errs")}
|
|
prev := NewSnapshot(targ.Name, Manifest{}, nil)
|
|
plan := NewPlan(ctx, targ, prev, &errorSource{err: errors.New("ITERATE"), duringIterate: true}, nil, false)
|
|
iter, err := plan.Start(Options{})
|
|
assert.Nil(t, iter)
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, "ITERATE", err.Error())
|
|
err = ctx.Close()
|
|
assert.Nil(t, err)
|
|
}
|
|
|
|
// Next trigger an error from Next:
|
|
{
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, "", nil)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("errs")}
|
|
prev := NewSnapshot(targ.Name, Manifest{}, nil)
|
|
plan := NewPlan(ctx, targ, prev, &errorSource{err: errors.New("NEXT"), duringIterate: false}, nil, false)
|
|
iter, err := plan.Start(Options{})
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, iter)
|
|
next, err := iter.Next()
|
|
assert.Nil(t, next)
|
|
assert.NotNil(t, err)
|
|
assert.Equal(t, "NEXT", err.Error())
|
|
err = ctx.Close()
|
|
assert.Nil(t, err)
|
|
}
|
|
}
|
|
|
|
// An errorSource returns an error from either iterate or next, depending on the flag.
|
|
type errorSource struct {
|
|
err error // the error to return.
|
|
duringIterate bool // if true, the error happens in Iterate; else, Next.
|
|
}
|
|
|
|
func (src *errorSource) Close() error {
|
|
return nil // nothing to do.
|
|
}
|
|
|
|
func (src *errorSource) Project() tokens.PackageName {
|
|
return ""
|
|
}
|
|
|
|
func (src *errorSource) Info() interface{} {
|
|
return nil
|
|
}
|
|
|
|
func (src *errorSource) Iterate(opts Options) (SourceIterator, error) {
|
|
if src.duringIterate {
|
|
return nil, src.err
|
|
}
|
|
return &errorSourceIterator{src: src}, nil
|
|
}
|
|
|
|
type errorSourceIterator struct {
|
|
src *errorSource
|
|
}
|
|
|
|
func (iter *errorSourceIterator) Close() error {
|
|
return nil // nothing to do.
|
|
}
|
|
|
|
func (iter *errorSourceIterator) Next() (SourceEvent, error) {
|
|
return nil, iter.src.err
|
|
}
|
|
|
|
// TestBasicCRUDPlan creates a plan with numerous C(R)UD operations.
|
|
func TestBasicCRUDPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a context that the snapshots and plan will use.
|
|
pkg := tokens.Package("testcrud")
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), &testProviderHost{
|
|
provider: func(propkg tokens.Package, version *semver.Version) (plugin.Provider, error) {
|
|
if propkg != pkg {
|
|
return nil, errors.Errorf("Unexpected request to load package %v; expected just %v", propkg, pkg)
|
|
}
|
|
return &testProvider{
|
|
check: func(urn resource.URN,
|
|
olds, news resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
|
|
return news, nil, nil // accept all changes.
|
|
},
|
|
diff: func(urn resource.URN, id resource.ID, olds resource.PropertyMap,
|
|
news resource.PropertyMap) (plugin.DiffResult, error) {
|
|
return plugin.DiffResult{}, nil // accept all changes.
|
|
},
|
|
// we don't actually execute the plan, so there's no need to implement the other functions.
|
|
}, nil
|
|
},
|
|
}, "", nil)
|
|
assert.Nil(t, err)
|
|
|
|
// Setup a fake namespace/target combination.
|
|
targ := &Target{Name: tokens.QName("crud")}
|
|
ns := targ.Name
|
|
mod := tokens.Module(pkg + ":index")
|
|
pkgname := pkg.Name()
|
|
parentType := tokens.Type("")
|
|
|
|
// Some shared tokens and names.
|
|
typA := tokens.Type(mod + ":A")
|
|
namA := tokens.QName("res-a")
|
|
urnA := resource.NewURN(ns, pkgname, parentType, typA, namA)
|
|
typB := tokens.Type(mod + ":B")
|
|
namB := tokens.QName("res-b")
|
|
urnB := resource.NewURN(ns, pkgname, parentType, typB, namB)
|
|
typC := tokens.Type(mod + ":C")
|
|
namC := tokens.QName("res-c")
|
|
urnC := resource.NewURN(ns, pkgname, parentType, typC, namC)
|
|
typD := tokens.Type(mod + ":D")
|
|
namD := tokens.QName("res-d")
|
|
urnD := resource.NewURN(ns, pkgname, parentType, typD, namD)
|
|
|
|
// Create the old resources snapshot.
|
|
oldResB := resource.NewState(typB, urnB, true, false, resource.ID("b-b-b"),
|
|
resource.PropertyMap{
|
|
"bf1": resource.NewStringProperty("b-value"),
|
|
"bf2": resource.NewNumberProperty(42),
|
|
},
|
|
nil,
|
|
"",
|
|
false,
|
|
)
|
|
oldResC := resource.NewState(typC, urnC, true, false, resource.ID("c-c-c"),
|
|
resource.PropertyMap{
|
|
"cf1": resource.NewStringProperty("c-value"),
|
|
"cf2": resource.NewNumberProperty(83),
|
|
},
|
|
resource.PropertyMap{
|
|
"outta1": resource.NewStringProperty("populated during skip/step"),
|
|
"outta234": resource.NewNumberProperty(99881122),
|
|
},
|
|
"",
|
|
false,
|
|
)
|
|
oldResD := resource.NewState(typD, urnD, true, false, resource.ID("d-d-d"),
|
|
resource.PropertyMap{
|
|
"df1": resource.NewStringProperty("d-value"),
|
|
"df2": resource.NewNumberProperty(167),
|
|
},
|
|
nil,
|
|
"",
|
|
false,
|
|
)
|
|
oldsnap := NewSnapshot(ns, Manifest{}, []*resource.State{oldResB, oldResC, oldResD})
|
|
|
|
// Create the new resource objects a priori.
|
|
// - A is created:
|
|
newResA := resource.NewGoal(typA, namA, true, resource.PropertyMap{
|
|
"af1": resource.NewStringProperty("a-value"),
|
|
"af2": resource.NewNumberProperty(42),
|
|
}, "", false)
|
|
newStateA := &testRegEvent{goal: newResA}
|
|
// - B is updated:
|
|
newResB := resource.NewGoal(typB, namB, true, resource.PropertyMap{
|
|
"bf1": resource.NewStringProperty("b-value"),
|
|
// delete the bf2 field, and add bf3.
|
|
"bf3": resource.NewBoolProperty(true),
|
|
}, "", false)
|
|
newStateB := &testRegEvent{goal: newResB}
|
|
// - C has no changes:
|
|
newResC := resource.NewGoal(typC, namC, true, resource.PropertyMap{
|
|
"cf1": resource.NewStringProperty("c-value"),
|
|
"cf2": resource.NewNumberProperty(83),
|
|
}, "", false)
|
|
newStateC := &testRegEvent{goal: newResC}
|
|
// - No D; it is deleted.
|
|
|
|
// Use a fixed source that just returns the above predefined objects during planning.
|
|
source := NewFixedSource(pkgname, []SourceEvent{newStateA, newStateB, newStateC})
|
|
|
|
// Next up, create a plan from the new and old, and validate its shape.
|
|
plan := NewPlan(ctx, targ, oldsnap, source, nil, false)
|
|
|
|
// Next, validate the steps and ensure that we see all of the expected ones. Note that there aren't any
|
|
// dependencies between the steps, so we must validate it in a way that's insensitive of order.
|
|
seen := make(map[StepOp]int)
|
|
iter, err := plan.Start(Options{})
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, iter)
|
|
for {
|
|
step, err := iter.Next()
|
|
assert.Nil(t, err)
|
|
if step == nil {
|
|
break
|
|
}
|
|
|
|
var state *resource.State
|
|
var urn resource.URN
|
|
var realID bool
|
|
var expectOuts resource.PropertyMap
|
|
switch s := step.(type) {
|
|
case *CreateStep: // A is created
|
|
old := s.Old()
|
|
assert.Nil(t, old)
|
|
new := s.New()
|
|
assert.NotNil(t, new)
|
|
assert.Equal(t, urnA, new.URN)
|
|
assert.Equal(t, newResA.Properties, new.Inputs)
|
|
state = new
|
|
urn, realID = urnA, false
|
|
case *UpdateStep: // B is updated
|
|
old := s.Old()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnB, old.URN)
|
|
assert.Equal(t, oldResB, old)
|
|
new := s.New()
|
|
assert.NotNil(t, new)
|
|
assert.Equal(t, urnB, new.URN)
|
|
assert.Equal(t, newResB.Properties, new.Inputs)
|
|
state = new
|
|
urn, realID = urnB, true
|
|
case *SameStep: // C is the same
|
|
old := s.Old()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnC, old.URN)
|
|
assert.Equal(t, oldResC, old)
|
|
new := s.New()
|
|
assert.NotNil(t, s.New())
|
|
assert.Equal(t, urnC, new.URN)
|
|
assert.Equal(t, newResC.Properties, new.Inputs)
|
|
state = new
|
|
urn, realID, expectOuts = urnC, true, oldResC.Outputs
|
|
case *DeleteStep: // D is deleted
|
|
old := s.Old()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnD, old.URN)
|
|
assert.Equal(t, oldResD, old)
|
|
new := s.New()
|
|
assert.Nil(t, new)
|
|
default:
|
|
t.FailNow() // unexpected step kind.
|
|
}
|
|
|
|
_, err = step.Apply(true)
|
|
assert.Nil(t, err)
|
|
|
|
op := step.Op()
|
|
if state != nil {
|
|
// Ensure the URN, ID, and properties are populated correctly.
|
|
assert.Equal(t, urn, state.URN,
|
|
"Expected op %v to populate a URN equal to %v", op, urn)
|
|
|
|
if realID {
|
|
assert.NotEqual(t, resource.ID(""), state.ID,
|
|
"Expected op %v to populate a real ID (%v)", op, urn)
|
|
} else {
|
|
assert.Equal(t, resource.ID(""), state.ID,
|
|
"Expected op %v to leave ID blank (%v); got %v", op, urn, state.ID)
|
|
}
|
|
|
|
if expectOuts != nil {
|
|
props := state.All()
|
|
for k := range expectOuts {
|
|
assert.Equal(t, expectOuts[k], props[k])
|
|
}
|
|
}
|
|
}
|
|
|
|
seen[op]++ // track the # of these we've seen so we can validate.
|
|
}
|
|
assert.Equal(t, 1, seen[OpCreate])
|
|
assert.Equal(t, 1, seen[OpUpdate])
|
|
assert.Equal(t, 1, seen[OpDelete])
|
|
assert.Equal(t, 0, seen[OpReplace])
|
|
assert.Equal(t, 0, seen[OpDeleteReplaced])
|
|
|
|
assert.Equal(t, 1, len(iter.Creates()))
|
|
assert.True(t, iter.Creates()[urnA])
|
|
assert.Equal(t, 1, len(iter.Updates()))
|
|
assert.True(t, iter.Updates()[urnB])
|
|
assert.Equal(t, 0, len(iter.Replaces()))
|
|
assert.Equal(t, 1, len(iter.Sames()))
|
|
assert.True(t, iter.Sames()[urnC])
|
|
assert.Equal(t, 1, len(iter.Deletes()))
|
|
assert.True(t, iter.Deletes()[urnD])
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type testProviderHost struct {
|
|
analyzer func(nm tokens.QName) (plugin.Analyzer, error)
|
|
provider func(pkg tokens.Package, version *semver.Version) (plugin.Provider, error)
|
|
langhost func(runtime string) (plugin.LanguageRuntime, error)
|
|
}
|
|
|
|
func (host *testProviderHost) Close() error {
|
|
return nil
|
|
}
|
|
func (host *testProviderHost) ServerAddr() string {
|
|
contract.Failf("Host RPC address not available")
|
|
return ""
|
|
}
|
|
func (host *testProviderHost) Log(sev diag.Severity, msg string) {
|
|
cmdutil.Diag().Logf(sev, diag.RawMessage(msg))
|
|
}
|
|
func (host *testProviderHost) ReadLocation(tok tokens.Token) (resource.PropertyValue, error) {
|
|
return resource.PropertyValue{}, errors.New("Invalid location")
|
|
}
|
|
func (host *testProviderHost) ReadLocations(tok tokens.Token) (resource.PropertyMap, error) {
|
|
return nil, errors.New("Invalid location")
|
|
}
|
|
func (host *testProviderHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) {
|
|
return host.analyzer(nm)
|
|
}
|
|
func (host *testProviderHost) Provider(pkg tokens.Package, version *semver.Version) (plugin.Provider, error) {
|
|
return host.provider(pkg, version)
|
|
}
|
|
func (host *testProviderHost) LanguageRuntime(runtime string) (plugin.LanguageRuntime, error) {
|
|
return host.langhost(runtime)
|
|
}
|
|
func (host *testProviderHost) ListPlugins() []workspace.PluginInfo {
|
|
return nil
|
|
}
|
|
func (host *testProviderHost) EnsurePlugins(info plugin.ProgInfo) error {
|
|
return nil
|
|
}
|
|
func (host *testProviderHost) GetRequiredPlugins(info plugin.ProgInfo) ([]workspace.PluginInfo, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type testProvider struct {
|
|
pkg tokens.Package
|
|
config func(map[tokens.ModuleMember]string) error
|
|
check func(resource.URN,
|
|
resource.PropertyMap, resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error)
|
|
create func(resource.URN, resource.PropertyMap) (resource.ID, resource.PropertyMap, resource.Status, error)
|
|
diff func(resource.URN, resource.ID, resource.PropertyMap, resource.PropertyMap) (plugin.DiffResult, error)
|
|
update func(resource.URN, resource.ID,
|
|
resource.PropertyMap, resource.PropertyMap) (resource.PropertyMap, resource.Status, error)
|
|
delete func(resource.URN, resource.ID, resource.PropertyMap) (resource.Status, error)
|
|
invoke func(tokens.ModuleMember, resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error)
|
|
}
|
|
|
|
func (prov *testProvider) Close() error {
|
|
return nil
|
|
}
|
|
func (prov *testProvider) Pkg() tokens.Package {
|
|
return prov.pkg
|
|
}
|
|
func (prov *testProvider) Configure(vars map[tokens.ModuleMember]string) error {
|
|
return prov.config(vars)
|
|
}
|
|
func (prov *testProvider) Check(urn resource.URN,
|
|
olds, news resource.PropertyMap, _ bool) (resource.PropertyMap, []plugin.CheckFailure, error) {
|
|
return prov.check(urn, olds, news)
|
|
}
|
|
func (prov *testProvider) Create(urn resource.URN, props resource.PropertyMap) (resource.ID,
|
|
resource.PropertyMap, resource.Status, error) {
|
|
return prov.create(urn, props)
|
|
}
|
|
func (prov *testProvider) Diff(urn resource.URN, id resource.ID,
|
|
olds resource.PropertyMap, news resource.PropertyMap, _ bool) (plugin.DiffResult, error) {
|
|
return prov.diff(urn, id, olds, news)
|
|
}
|
|
func (prov *testProvider) Update(urn resource.URN, id resource.ID,
|
|
olds resource.PropertyMap, news resource.PropertyMap) (resource.PropertyMap, resource.Status, error) {
|
|
return prov.update(urn, id, olds, news)
|
|
}
|
|
func (prov *testProvider) Delete(urn resource.URN,
|
|
id resource.ID, props resource.PropertyMap) (resource.Status, error) {
|
|
return prov.delete(urn, id, props)
|
|
}
|
|
func (prov *testProvider) Invoke(tok tokens.ModuleMember,
|
|
args resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
|
|
return prov.invoke(tok, args)
|
|
}
|
|
func (prov *testProvider) GetPluginInfo() (workspace.PluginInfo, error) {
|
|
return workspace.PluginInfo{
|
|
Name: "testProvider",
|
|
}, nil
|
|
}
|