a6caef973a
This change recognizes assets and archives as 1st class resource property values. This is necessary to support them in the new bridge work, and lays the foundation for fixing pulumi/lumi#153. I also took the opportunity to clean up some old cruft in the resource properties area.
438 lines
15 KiB
Go
438 lines
15 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/pulumi/lumi/pkg/compiler/ast"
|
|
"github.com/pulumi/lumi/pkg/compiler/symbols"
|
|
"github.com/pulumi/lumi/pkg/compiler/types"
|
|
"github.com/pulumi/lumi/pkg/compiler/types/predef"
|
|
"github.com/pulumi/lumi/pkg/diag"
|
|
"github.com/pulumi/lumi/pkg/eval/rt"
|
|
"github.com/pulumi/lumi/pkg/pack"
|
|
"github.com/pulumi/lumi/pkg/resource"
|
|
"github.com/pulumi/lumi/pkg/resource/plugin"
|
|
"github.com/pulumi/lumi/pkg/tokens"
|
|
"github.com/pulumi/lumi/pkg/util/cmdutil"
|
|
"github.com/pulumi/lumi/pkg/util/contract"
|
|
)
|
|
|
|
// TestNullPlan creates a plan with no operations.
|
|
func TestNullPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), nil)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("null")}
|
|
prev := NewSnapshot(targ.Name, nil, nil)
|
|
plan := NewPlan(ctx, targ, prev, NullSource, nil)
|
|
iter, err := plan.Iterate()
|
|
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)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("errs")}
|
|
prev := NewSnapshot(targ.Name, nil, nil)
|
|
plan := NewPlan(ctx, targ, prev, &errorSource{err: errors.New("ITERATE"), duringIterate: true}, nil)
|
|
iter, err := plan.Iterate()
|
|
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)
|
|
assert.Nil(t, err)
|
|
targ := &Target{Name: tokens.QName("errs")}
|
|
prev := NewSnapshot(targ.Name, nil, nil)
|
|
plan := NewPlan(ctx, targ, prev, &errorSource{err: errors.New("NEXT"), duringIterate: false}, nil)
|
|
iter, err := plan.Iterate()
|
|
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) Info() interface{} {
|
|
return nil
|
|
}
|
|
|
|
func (src *errorSource) Iterate() (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) Produce(res *resource.Object) {
|
|
// nothing to do.
|
|
}
|
|
|
|
func (iter *errorSourceIterator) Next() (*SourceAllocation, *SourceQuery, error) {
|
|
return nil, nil, iter.src.err
|
|
}
|
|
|
|
// TestBasicCRUDPlan creates a plan with numerous C(R)UD operations.
|
|
func TestBasicCRUDPlan(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Setup a fake namespace/target combination.
|
|
targ := &Target{Name: tokens.QName("crud")}
|
|
ns := targ.Name
|
|
base := fakeResourceBase()
|
|
pkg, mod, newResourceType := fakeTestResources(tokens.PackageName("testcrud"), tokens.ModuleName("index"))
|
|
|
|
// Create a context that the snapshots and plan will use.
|
|
ctx, err := plugin.NewContext(cmdutil.Diag(), &testProviderHost{
|
|
provider: func(propkg tokens.Package) (plugin.Provider, error) {
|
|
if propkg != pkg.Tok {
|
|
return nil, errors.Errorf("Unexpected request to load package %v; expected just %v", propkg, pkg)
|
|
}
|
|
return &testProvider{
|
|
check: func(t tokens.Type, props resource.PropertyMap) ([]plugin.CheckFailure, error) {
|
|
return nil, nil // accept all changes.
|
|
},
|
|
name: func(_ tokens.Type, props resource.PropertyMap) (tokens.QName, error) {
|
|
name, has := props["name"]
|
|
assert.True(t, has)
|
|
assert.True(t, name.IsString())
|
|
return tokens.QName(name.StringValue()), nil
|
|
},
|
|
inspectChange: func(t tokens.Type, id resource.ID, olds resource.PropertyMap,
|
|
news resource.PropertyMap) ([]resource.PropertyKey, resource.PropertyMap, error) {
|
|
return nil, nil, nil // accept all changes.
|
|
},
|
|
// we don't actually execute the plan, so there's no need to implement the other functions.
|
|
}, nil
|
|
},
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
// Some shared tokens and names.
|
|
typA := newResourceType(tokens.TypeName("A"), base)
|
|
namA := tokens.QName("res-a")
|
|
urnA := resource.NewURN(ns, mod.Tok, typA.Tok, namA)
|
|
typB := newResourceType(tokens.TypeName("B"), base)
|
|
namB := tokens.QName("res-b")
|
|
urnB := resource.NewURN(ns, mod.Tok, typB.Tok, namB)
|
|
typC := newResourceType(tokens.TypeName("C"), base)
|
|
namC := tokens.QName("res-c")
|
|
urnC := resource.NewURN(ns, mod.Tok, typC.Tok, namC)
|
|
typD := newResourceType(tokens.TypeName("D"), base)
|
|
namD := tokens.QName("res-d")
|
|
urnD := resource.NewURN(ns, mod.Tok, typD.Tok, namD)
|
|
|
|
// Create the old resources snapshot.
|
|
oldResB := resource.NewState(typB.Tok, urnB, resource.ID("b-b-b"), resource.PropertyMap{
|
|
"name": resource.NewStringProperty(namB.String()),
|
|
"bf1": resource.NewStringProperty("b-value"),
|
|
"bf2": resource.NewNumberProperty(42),
|
|
}, nil)
|
|
oldResC := resource.NewState(typC.Tok, urnC, resource.ID("c-c-c"), resource.PropertyMap{
|
|
"name": resource.NewStringProperty(namC.String()),
|
|
"cf1": resource.NewStringProperty("c-value"),
|
|
"cf2": resource.NewNumberProperty(83),
|
|
}, resource.PropertyMap{
|
|
"outta1": resource.NewStringProperty("populated during skip/step"),
|
|
"outta234": resource.NewNumberProperty(99881122),
|
|
})
|
|
oldResD := resource.NewState(typD.Tok, urnD, resource.ID("d-d-d"), resource.PropertyMap{
|
|
"name": resource.NewStringProperty(namD.String()),
|
|
"df1": resource.NewStringProperty("d-value"),
|
|
"df2": resource.NewNumberProperty(167),
|
|
}, nil)
|
|
oldsnap := NewSnapshot(ns, []*resource.State{oldResB, oldResC, oldResD}, nil)
|
|
|
|
// Create the new resource objects a priori.
|
|
// - A is created:
|
|
newObjA := rt.NewObject(typA, nil, nil, nil)
|
|
newObjA.Properties().InitAddr(rt.PropertyKey("name"), rt.NewStringObject(namA.String()), false, nil, nil)
|
|
newObjA.Properties().InitAddr(rt.PropertyKey("af1"), rt.NewStringObject("a-value"), false, nil, nil)
|
|
newObjA.Properties().InitAddr(rt.PropertyKey("af2"), rt.NewNumberObject(42), false, nil, nil)
|
|
newResA := resource.NewObject(newObjA)
|
|
newResAProps := newResA.CopyProperties()
|
|
// - B is updated:
|
|
newObjB := rt.NewObject(typB, nil, nil, nil)
|
|
newObjB.Properties().InitAddr(rt.PropertyKey("name"), rt.NewStringObject(namB.String()), false, nil, nil)
|
|
newObjB.Properties().InitAddr(rt.PropertyKey("bf1"), rt.NewStringObject("b-value"), false, nil, nil)
|
|
// delete the bf2 field, and add bf3.
|
|
newObjB.Properties().InitAddr(rt.PropertyKey("bf3"), rt.True, false, nil, nil)
|
|
newResB := resource.NewObject(newObjB)
|
|
newResBProps := newResB.CopyProperties()
|
|
// - C has no changes:
|
|
newObjC := rt.NewObject(typC, nil, nil, nil)
|
|
newObjC.Properties().InitAddr(rt.PropertyKey("name"), rt.NewStringObject(namC.String()), false, nil, nil)
|
|
newObjC.Properties().InitAddr(rt.PropertyKey("cf1"), rt.NewStringObject("c-value"), false, nil, nil)
|
|
newObjC.Properties().InitAddr(rt.PropertyKey("cf2"), rt.NewNumberObject(83), false, nil, nil)
|
|
newObjC.Properties().InitAddr(rt.PropertyKey("outta234"),
|
|
rt.NewComputedObject(types.Dynamic, false, []*rt.Object{newObjC}), false, nil, nil)
|
|
newResC := resource.NewObject(newObjC)
|
|
newResCProps := newResC.CopyProperties()
|
|
// - No D; it is deleted.
|
|
|
|
// Use a fixed source that just returns the above predefined objects during planning.
|
|
new := NewFixedSource(mod.Tok, []*resource.Object{newResA, newResB, newResC})
|
|
|
|
// Next up, create a plan from the new and old, and validate its shape.
|
|
plan := NewPlan(ctx, targ, oldsnap, new, nil)
|
|
|
|
// 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.Iterate()
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, iter)
|
|
for {
|
|
step, err := iter.Next()
|
|
assert.Nil(t, err)
|
|
if step == nil {
|
|
break
|
|
}
|
|
|
|
err = step.Pre()
|
|
assert.Nil(t, err)
|
|
|
|
var urn resource.URN
|
|
var realID bool
|
|
var expectOuts resource.PropertyMap
|
|
var obj *resource.Object
|
|
switch s := step.(type) {
|
|
case *CreateStep: // A is created
|
|
old := s.Old()
|
|
new := s.New()
|
|
assert.Nil(t, old)
|
|
assert.NotNil(t, new)
|
|
assert.Equal(t, newResAProps, s.Inputs())
|
|
obj, urn, realID = new, urnA, false
|
|
case *UpdateStep: // B is updated
|
|
old := s.Old()
|
|
new := s.New()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnB, old.URN())
|
|
assert.Equal(t, oldResB, old)
|
|
assert.NotNil(t, new)
|
|
assert.Equal(t, newResBProps, s.Inputs())
|
|
obj, urn, realID = new, urnB, true
|
|
case *SameStep: // C is the same
|
|
old := s.Old()
|
|
new := s.New()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnC, old.URN())
|
|
assert.Equal(t, oldResC, old)
|
|
assert.NotNil(t, new)
|
|
assert.Equal(t, newResCProps, s.Inputs())
|
|
obj, urn, realID, expectOuts = new, urnC, true, oldResC.Outputs()
|
|
case *DeleteStep: // D is deleted
|
|
old := s.Old()
|
|
new := s.New()
|
|
assert.NotNil(t, old)
|
|
assert.Equal(t, urnD, old.URN())
|
|
assert.Equal(t, oldResD, old)
|
|
assert.Nil(t, new)
|
|
default:
|
|
t.FailNow() // unexpected step kind.
|
|
}
|
|
|
|
if obj != nil {
|
|
// Ensure the ID and URN aren't assigned until we step.
|
|
assert.False(t, obj.HasID())
|
|
assert.False(t, obj.HasURN())
|
|
if expectOuts != nil {
|
|
for k := range expectOuts {
|
|
outprop := obj.Obj().Properties().GetAddr(rt.PropertyKey(k))
|
|
if outprop != nil {
|
|
outobj := outprop.Obj()
|
|
assert.NotNil(t, outobj)
|
|
assert.True(t, outobj.IsNull() || outobj.IsComputed())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
err = step.Skip()
|
|
assert.Nil(t, err)
|
|
|
|
op := step.Op()
|
|
if obj != nil {
|
|
// Ensure the ID and URN are populated correctly.
|
|
if realID {
|
|
assert.True(t, obj.HasID(), "Expected op %v to populate a real ID (%v)", op, urn)
|
|
}
|
|
assert.True(t, obj.HasURN(), "Expected op %v to populate a URN (%v)", op, urn)
|
|
assert.Equal(t, urn, obj.URN())
|
|
if expectOuts != nil {
|
|
for k := range expectOuts {
|
|
outprop := obj.Obj().Properties().GetAddr(rt.PropertyKey(k))
|
|
assert.NotNil(t, outprop)
|
|
outobj := outprop.Obj()
|
|
assert.NotNil(t, outobj)
|
|
assert.False(t, outobj.IsNull())
|
|
}
|
|
}
|
|
}
|
|
|
|
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[OpGet])
|
|
assert.Equal(t, 0, seen[OpQuery])
|
|
|
|
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])
|
|
}
|
|
|
|
// fakeResourceBase news up a resource type so that it looks like a predefined resource type.
|
|
func fakeResourceBase() symbols.Type {
|
|
_, _, fact := fakeTestResources(predef.LumiStdlib.Name(), predef.LumiStdlibResourceClass.Module().Name())
|
|
return fact(predef.LumiStdlibResourceClass.Name(), nil)
|
|
}
|
|
|
|
// fakeTestResources creates a fake package, module, and factory for resource types.
|
|
func fakeTestResources(pkgnm tokens.PackageName,
|
|
modnm tokens.ModuleName) (*symbols.Package, *symbols.Module, func(tokens.TypeName, symbols.Type) *symbols.Class) {
|
|
pkg := symbols.NewPackageSym(&pack.Package{
|
|
Name: pkgnm,
|
|
})
|
|
mod := symbols.NewModuleSym(&ast.Module{
|
|
DefinitionNode: ast.DefinitionNode{
|
|
Name: &ast.Identifier{Ident: tokens.Name(modnm)},
|
|
},
|
|
}, pkg)
|
|
pkg.Modules[mod.Tok.Name()] = mod
|
|
return pkg, mod, func(tynm tokens.TypeName, base symbols.Type) *symbols.Class {
|
|
cls := symbols.NewClassSym(&ast.Class{
|
|
ModuleMemberNode: ast.ModuleMemberNode{
|
|
DefinitionNode: ast.DefinitionNode{
|
|
Name: &ast.Identifier{Ident: tokens.Name(tynm)},
|
|
},
|
|
},
|
|
}, mod, base, nil)
|
|
mod.Members[cls.MemberName()] = cls
|
|
return cls
|
|
}
|
|
}
|
|
|
|
type testProviderHost struct {
|
|
analyzer func(nm tokens.QName) (plugin.Analyzer, error)
|
|
provider func(pkg tokens.Package) (plugin.Provider, 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.Message(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) (plugin.Provider, error) {
|
|
return host.provider(pkg)
|
|
}
|
|
|
|
type testProvider struct {
|
|
pkg tokens.Package
|
|
check func(tokens.Type, resource.PropertyMap) ([]plugin.CheckFailure, error)
|
|
name func(tokens.Type, resource.PropertyMap) (tokens.QName, error)
|
|
create func(tokens.Type, resource.PropertyMap) (resource.ID, resource.Status, error)
|
|
get func(tokens.Type, resource.ID) (resource.PropertyMap, error)
|
|
inspectChange func(tokens.Type, resource.ID,
|
|
resource.PropertyMap, resource.PropertyMap) ([]resource.PropertyKey, resource.PropertyMap, error)
|
|
update func(tokens.Type, resource.ID,
|
|
resource.PropertyMap, resource.PropertyMap) (resource.Status, error)
|
|
delete func(tokens.Type, resource.ID) (resource.Status, error)
|
|
}
|
|
|
|
func (prov *testProvider) Close() error {
|
|
return nil
|
|
}
|
|
func (prov *testProvider) Pkg() tokens.Package {
|
|
return prov.pkg
|
|
}
|
|
func (prov *testProvider) Check(t tokens.Type, props resource.PropertyMap) ([]plugin.CheckFailure, error) {
|
|
return prov.check(t, props)
|
|
}
|
|
func (prov *testProvider) Name(t tokens.Type, props resource.PropertyMap) (tokens.QName, error) {
|
|
return prov.name(t, props)
|
|
}
|
|
func (prov *testProvider) Create(t tokens.Type, props resource.PropertyMap) (resource.ID, resource.Status, error) {
|
|
return prov.create(t, props)
|
|
}
|
|
func (prov *testProvider) Get(t tokens.Type, id resource.ID) (resource.PropertyMap, error) {
|
|
return prov.get(t, id)
|
|
}
|
|
func (prov *testProvider) InspectChange(t tokens.Type, id resource.ID,
|
|
olds resource.PropertyMap, news resource.PropertyMap) ([]resource.PropertyKey, resource.PropertyMap, error) {
|
|
return prov.inspectChange(t, id, olds, news)
|
|
}
|
|
func (prov *testProvider) Update(t tokens.Type, id resource.ID,
|
|
olds resource.PropertyMap, news resource.PropertyMap) (resource.Status, error) {
|
|
return prov.update(t, id, olds, news)
|
|
}
|
|
func (prov *testProvider) Delete(t tokens.Type, id resource.ID) (resource.Status, error) {
|
|
return prov.delete(t, id)
|
|
}
|