Reimplement Output for Go. (#3496)
- Use a mutex + condition variable instead of a channel for synchronizaiton in order to allow multiple calls to resolve/reject - Properly handle outputs that are resolved to other outputs, especially if those outputs are not of exactly type Output - Remove the Value() methods that allowed prompt access to output values - Add variants of `Apply` that take a context parameter - Ensure that resource outputs properly incorporate their resource as a dependency - Make `Output` a plain struct. Uninitialized outputs will be treated as resolved and unknown. This makes conversions between output types more ergonomic. Contributes to #3492.
This commit is contained in:
parent
d81ac16132
commit
a7f61a59b0
|
@ -14,6 +14,9 @@ CHANGELOG
|
|||
calculated by the provider.
|
||||
[#3327](https://github.com/pulumi/pulumi/pull/3327)
|
||||
|
||||
- Refactor the Output API in the Go SDK.
|
||||
[#3496](https://github.com/pulumi/pulumi/pull/3496)
|
||||
|
||||
## 1.5.1 (2019-11-06)
|
||||
|
||||
- Include the .NET language provider in the Windows SDK.
|
||||
|
|
|
@ -46,6 +46,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/util/result"
|
||||
"github.com/pulumi/pulumi/pkg/util/rpcutil/rpcerror"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
"github.com/pulumi/pulumi/sdk/go/pulumi"
|
||||
|
||||
combinations "github.com/mxschmitt/golang-combinations"
|
||||
)
|
||||
|
@ -4895,3 +4896,53 @@ func TestPreviewInputPropagation(t *testing.T) {
|
|||
_, res = TestOp(Update).Run(project, p.GetTarget(snap), p.Options, preview, p.BackendClient, nil)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func TestSingleResourceDefaultProviderGolangLifecycle(t *testing.T) {
|
||||
loaders := []*deploytest.ProviderLoader{
|
||||
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
||||
return &deploytest.Provider{
|
||||
CreateF: func(urn resource.URN,
|
||||
news resource.PropertyMap, timeout float64) (resource.ID, resource.PropertyMap, resource.Status, error) {
|
||||
|
||||
return "created-id", news, resource.StatusOK, nil
|
||||
},
|
||||
ReadF: func(urn resource.URN, id resource.ID,
|
||||
inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) {
|
||||
return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
||||
ctx, err := pulumi.NewContext(context.Background(), pulumi.RunInfo{
|
||||
Project: info.Project,
|
||||
Stack: info.Stack,
|
||||
Parallel: info.Parallel,
|
||||
DryRun: info.DryRun,
|
||||
MonitorAddr: info.MonitorAddress,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
|
||||
res, err := ctx.RegisterResource("pkgA:m:typA", "resA", true, map[string]interface{}{
|
||||
"foo": "bar",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = ctx.RegisterResource("pkgA:m:typA", "resB", true, map[string]interface{}{
|
||||
"baz": res.State["foo"],
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
host := deploytest.NewPluginHost(nil, nil, program, loaders...)
|
||||
|
||||
p := &TestPlan{
|
||||
Options: UpdateOptions{host: host},
|
||||
Steps: MakeBasicLifecycleSteps(t, 4),
|
||||
}
|
||||
p.Run(t, nil)
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
"sync"
|
||||
|
||||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
|
@ -137,7 +137,7 @@ func (ctx *Context) Invoke(tok string, args map[string]interface{}, opts ...Invo
|
|||
|
||||
// Serialize arguments, first by awaiting them, and then marshaling them to the requisite gRPC values.
|
||||
// TODO[pulumi/pulumi#1483]: feels like we should be propagating dependencies to the outputs, instead of ignoring.
|
||||
rpcArgs, _, _, err := marshalInputs(args)
|
||||
rpcArgs, _, _, err := marshalInputs(args, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshaling arguments")
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ func (ctx *Context) ReadResource(
|
|||
}
|
||||
|
||||
// Create resolvers for the resource's outputs.
|
||||
outputs := makeResourceOutputs(true, props)
|
||||
res := makeResourceState(true, props)
|
||||
|
||||
// Kick off the resource read operation. This will happen asynchronously and resolve the above properties.
|
||||
go func() {
|
||||
|
@ -204,7 +204,7 @@ func (ctx *Context) ReadResource(
|
|||
var state *structpb.Struct
|
||||
var err error
|
||||
defer func() {
|
||||
outputs.resolve(ctx.DryRun(), err, props, urn, resID, state)
|
||||
res.resolve(ctx.DryRun(), err, props, urn, resID, state)
|
||||
ctx.endRPC()
|
||||
}()
|
||||
|
||||
|
@ -233,15 +233,7 @@ func (ctx *Context) ReadResource(
|
|||
}
|
||||
}()
|
||||
|
||||
outs := make(map[string]*Output)
|
||||
for k, s := range outputs.state {
|
||||
outs[k] = s.out
|
||||
}
|
||||
return &ResourceState{
|
||||
urn: (*URNOutput)(outputs.urn.out),
|
||||
id: (*IDOutput)(outputs.id.out),
|
||||
State: outs,
|
||||
}, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// RegisterResource creates and registers a new resource object. t is the fully qualified type token and name is
|
||||
|
@ -261,7 +253,7 @@ func (ctx *Context) RegisterResource(
|
|||
}
|
||||
|
||||
// Create resolvers for the resource's outputs.
|
||||
outputs := makeResourceOutputs(custom, props)
|
||||
res := makeResourceState(custom, props)
|
||||
|
||||
// Kick off the resource registration. If we are actually performing a deployment, the resulting properties
|
||||
// will be resolved asynchronously as the RPC operation completes. If we're just planning, values won't resolve.
|
||||
|
@ -271,7 +263,7 @@ func (ctx *Context) RegisterResource(
|
|||
var state *structpb.Struct
|
||||
var err error
|
||||
defer func() {
|
||||
outputs.resolve(ctx.DryRun(), err, props, urn, resID, state)
|
||||
res.resolve(ctx.DryRun(), err, props, urn, resID, state)
|
||||
ctx.endRPC()
|
||||
}()
|
||||
|
||||
|
@ -307,101 +299,89 @@ func (ctx *Context) RegisterResource(
|
|||
}
|
||||
}()
|
||||
|
||||
var id *IDOutput
|
||||
if outputs.id != nil {
|
||||
id = (*IDOutput)(outputs.id.out)
|
||||
}
|
||||
outs := make(map[string]*Output)
|
||||
for k, s := range outputs.state {
|
||||
outs[k] = s.out
|
||||
}
|
||||
return &ResourceState{
|
||||
urn: (*URNOutput)(outputs.urn.out),
|
||||
id: id,
|
||||
State: outs,
|
||||
}, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// resourceOutputs captures the outputs and resolvers for a resource operation.
|
||||
type resourceOutputs struct {
|
||||
urn *resourceOutput
|
||||
id *resourceOutput
|
||||
state map[string]*resourceOutput
|
||||
// ResourceState contains the results of a resource registration operation.
|
||||
type ResourceState struct {
|
||||
// urn will resolve to the resource's URN after registration has completed.
|
||||
urn URNOutput
|
||||
// id will resolve to the resource's ID after registration, provided this is for a custom resource.
|
||||
id IDOutput
|
||||
// State contains the full set of expected output properties and will resolve after completion.
|
||||
State Outputs
|
||||
}
|
||||
|
||||
// makeResourceOutputs creates a set of resolvers that we'll use to finalize state, for URNs, IDs, and output
|
||||
// URN will resolve to the resource's URN after registration has completed.
|
||||
func (state *ResourceState) URN() URNOutput {
|
||||
return state.urn
|
||||
}
|
||||
|
||||
// ID will resolve to the resource's ID after registration, provided this is for a custom resource.
|
||||
func (state *ResourceState) ID() IDOutput {
|
||||
return state.id
|
||||
}
|
||||
|
||||
// makeResourceState creates a set of resolvers that we'll use to finalize state, for URNs, IDs, and output
|
||||
// properties.
|
||||
func makeResourceOutputs(custom bool, props map[string]interface{}) *resourceOutputs {
|
||||
outURN, resolveURN, rejectURN := NewOutput(nil)
|
||||
urn := &resourceOutput{out: outURN, resolve: resolveURN, reject: rejectURN}
|
||||
func makeResourceState(custom bool, props map[string]interface{}) *ResourceState {
|
||||
state := &ResourceState{}
|
||||
|
||||
state.urn = URNOutput(newOutput(state))
|
||||
|
||||
var id *resourceOutput
|
||||
if custom {
|
||||
outID, resolveID, rejectID := NewOutput(nil)
|
||||
id = &resourceOutput{out: outID, resolve: resolveID, reject: rejectID}
|
||||
state.id = IDOutput(newOutput(state))
|
||||
}
|
||||
|
||||
state := make(map[string]*resourceOutput)
|
||||
state.State = make(map[string]Output)
|
||||
for key := range props {
|
||||
outState, resolveState, rejectState := NewOutput(nil)
|
||||
state[key] = &resourceOutput{
|
||||
out: outState,
|
||||
resolve: resolveState,
|
||||
reject: rejectState,
|
||||
}
|
||||
state.State[key] = newOutput(state)
|
||||
}
|
||||
|
||||
return &resourceOutputs{
|
||||
urn: urn,
|
||||
id: id,
|
||||
state: state,
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// resolve resolves the resource outputs using the given error and/or values.
|
||||
func (outputs *resourceOutputs) resolve(dryrun bool, err error, inputs map[string]interface{}, urn, id string,
|
||||
func (state *ResourceState) resolve(dryrun bool, err error, inputs map[string]interface{}, urn, id string,
|
||||
result *structpb.Struct) {
|
||||
|
||||
var outprops map[string]interface{}
|
||||
if err == nil {
|
||||
outprops, err = unmarshalOutputs(result)
|
||||
}
|
||||
if err != nil {
|
||||
// If there was an error, we must reject everything: URN, ID, and state properties.
|
||||
outputs.urn.reject(err)
|
||||
if outputs.id != nil {
|
||||
outputs.id.reject(err)
|
||||
state.urn.s.reject(err)
|
||||
if state.id.s != nil {
|
||||
state.id.s.reject(err)
|
||||
}
|
||||
for _, s := range outputs.state {
|
||||
s.reject(err)
|
||||
}
|
||||
} else {
|
||||
// Resolve the URN and ID.
|
||||
outputs.urn.resolve(URN(urn), true)
|
||||
if outputs.id != nil {
|
||||
if id == "" && dryrun {
|
||||
outputs.id.resolve("", false)
|
||||
} else {
|
||||
outputs.id.resolve(ID(id), true)
|
||||
}
|
||||
for _, o := range state.State {
|
||||
o.s.reject(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// During previews, it's possible that nils will be returned due to unknown values. This function
|
||||
// determines the known-ed-ness of a given value below.
|
||||
isKnown := func(v interface{}) bool {
|
||||
return !dryrun || v != nil
|
||||
}
|
||||
// Resolve the URN and ID.
|
||||
state.urn.s.resolve(URN(urn), true)
|
||||
if state.id.s != nil {
|
||||
known := id != "" || !dryrun
|
||||
state.id.s.resolve(ID(id), known)
|
||||
}
|
||||
|
||||
// Now resolve all output properties.
|
||||
for k, s := range outputs.state {
|
||||
v, has := outprops[k]
|
||||
if !has && !dryrun {
|
||||
// If we did not receive a value for a particular property, resolve it to the corresponding input
|
||||
// if any exists.
|
||||
v = inputs[k]
|
||||
}
|
||||
s.resolve(v, isKnown(v))
|
||||
// During previews, it's possible that nils will be returned due to unknown values. This function
|
||||
// determines the known-ness of a given value below.
|
||||
isKnown := func(v interface{}) bool {
|
||||
return !dryrun || v != nil
|
||||
}
|
||||
|
||||
// Now resolve all output properties.
|
||||
for k, o := range state.State {
|
||||
v, has := outprops[k]
|
||||
if !has && !dryrun {
|
||||
// If we did not receive a value for a particular property, resolve it to the corresponding input
|
||||
// if any exists.
|
||||
v = inputs[k]
|
||||
}
|
||||
o.s.resolve(v, isKnown(v))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -430,7 +410,8 @@ func (ctx *Context) prepareResourceInputs(props map[string]interface{}, opts ...
|
|||
timeouts := ctx.getTimeouts(opts...)
|
||||
|
||||
// Serialize all properties, first by awaiting them, and then marshaling them to the requisite gRPC values.
|
||||
rpcProps, propertyDeps, rpcDeps, err := marshalInputs(props)
|
||||
keepUnknowns := ctx.DryRun()
|
||||
rpcProps, propertyDeps, rpcDeps, err := marshalInputs(props, keepUnknowns)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "marshaling properties")
|
||||
}
|
||||
|
@ -477,12 +458,6 @@ func (ctx *Context) prepareResourceInputs(props map[string]interface{}, opts ...
|
|||
}, nil
|
||||
}
|
||||
|
||||
type resourceOutput struct {
|
||||
out *Output
|
||||
resolve func(interface{}, bool)
|
||||
reject func(error)
|
||||
}
|
||||
|
||||
func (ctx *Context) getTimeouts(opts ...ResourceOpt) *pulumirpc.RegisterResourceRequest_CustomTimeouts {
|
||||
var timeouts pulumirpc.RegisterResourceRequest_CustomTimeouts
|
||||
for _, opt := range opts {
|
||||
|
@ -530,7 +505,7 @@ func (ctx *Context) getOpts(opts ...ResourceOpt) (URN, []URN, bool, string, bool
|
|||
if parent == nil {
|
||||
parentURN = ctx.stackR
|
||||
} else {
|
||||
urn, err := parent.URN().Value()
|
||||
urn, _, err := parent.URN().await(context.TODO())
|
||||
if err != nil {
|
||||
return "", nil, false, "", false, "", err
|
||||
}
|
||||
|
@ -541,7 +516,7 @@ func (ctx *Context) getOpts(opts ...ResourceOpt) (URN, []URN, bool, string, bool
|
|||
if deps != nil {
|
||||
depURNs = make([]URN, len(deps))
|
||||
for i, r := range deps {
|
||||
urn, err := r.URN().Value()
|
||||
urn, _, err := r.URN().await(context.TODO())
|
||||
if err != nil {
|
||||
return "", nil, false, "", false, "", err
|
||||
}
|
||||
|
@ -562,11 +537,11 @@ func (ctx *Context) getOpts(opts ...ResourceOpt) (URN, []URN, bool, string, bool
|
|||
}
|
||||
|
||||
func (ctx *Context) resolveProviderReference(provider ProviderResource) (string, error) {
|
||||
urn, err := provider.URN().Value()
|
||||
urn, _, err := provider.URN().await(context.TODO())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id, known, err := provider.ID().Value()
|
||||
id, known, err := provider.ID().await(context.TODO())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -621,26 +596,6 @@ func (ctx *Context) waitForRPCs() {
|
|||
ctx.rpcs = noMoreRPCs
|
||||
}
|
||||
|
||||
// ResourceState contains the results of a resource registration operation.
|
||||
type ResourceState struct {
|
||||
// urn will resolve to the resource's URN after registration has completed.
|
||||
urn *URNOutput
|
||||
// id will resolve to the resource's ID after registration, provided this is for a custom resource.
|
||||
id *IDOutput
|
||||
// State contains the full set of expected output properties and will resolve after completion.
|
||||
State Outputs
|
||||
}
|
||||
|
||||
// URN will resolve to the resource's URN after registration has completed.
|
||||
func (s *ResourceState) URN() *URNOutput {
|
||||
return s.urn
|
||||
}
|
||||
|
||||
// ID will resolve to the resource's ID after registration, provided this is for a custom resource.
|
||||
func (s *ResourceState) ID() *IDOutput {
|
||||
return s.id
|
||||
}
|
||||
|
||||
var _ Resource = (*ResourceState)(nil)
|
||||
var _ CustomResource = (*ResourceState)(nil)
|
||||
var _ ComponentResource = (*ResourceState)(nil)
|
||||
|
@ -648,7 +603,8 @@ var _ ProviderResource = (*ResourceState)(nil)
|
|||
|
||||
// RegisterResourceOutputs completes the resource registration, attaching an optional set of computed outputs.
|
||||
func (ctx *Context) RegisterResourceOutputs(urn URN, outs map[string]interface{}) error {
|
||||
outsMarshalled, _, _, err := marshalInputs(outs)
|
||||
keepUnknowns := ctx.DryRun()
|
||||
outsMarshalled, _, _, err := marshalInputs(outs, keepUnknowns)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshaling outputs")
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,178 +15,148 @@
|
|||
package pulumi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func assertApplied(t *testing.T, o Output) {
|
||||
_, known, err := o.s.await(context.Background())
|
||||
assert.True(t, known)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestBasicOutputs(t *testing.T) {
|
||||
// Just test basic resolve and reject functionality.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve(42, true)
|
||||
resolve(42)
|
||||
}()
|
||||
v, known, err := out.Value()
|
||||
v, known, err := out.s.await(context.Background())
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 42, v.(int))
|
||||
}
|
||||
{
|
||||
out, _, reject := NewOutput(nil)
|
||||
out, _, reject := NewOutput()
|
||||
go func() {
|
||||
reject(errors.New("boom"))
|
||||
}()
|
||||
v, _, err := out.Value()
|
||||
v, _, err := out.s.await(context.Background())
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayOutputs(t *testing.T) {
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve([]interface{}{nil, 0, "x"}, true)
|
||||
resolve([]interface{}{nil, 0, "x"})
|
||||
}()
|
||||
{
|
||||
v, known, err := out.Array()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.NotNil(t, v)
|
||||
if assert.Equal(t, 3, len(v)) {
|
||||
assert.Equal(t, nil, v[0])
|
||||
assert.Equal(t, 0, v[1])
|
||||
assert.Equal(t, "x", v[2])
|
||||
}
|
||||
}
|
||||
{
|
||||
arr := (*ArrayOutput)(out)
|
||||
v, _, err := arr.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, v)
|
||||
if assert.Equal(t, 3, len(v)) {
|
||||
assert.Equal(t, nil, v[0])
|
||||
assert.Equal(t, 0, v[1])
|
||||
assert.Equal(t, "x", v[2])
|
||||
}
|
||||
arr := ArrayOutput(out)
|
||||
assertApplied(t, arr.Apply(func(arr []interface{}) (interface{}, error) {
|
||||
assert.NotNil(t, arr)
|
||||
if assert.Equal(t, 3, len(arr)) {
|
||||
assert.Equal(t, nil, arr[0])
|
||||
assert.Equal(t, 0, arr[1])
|
||||
assert.Equal(t, "x", arr[2])
|
||||
}
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolOutputs(t *testing.T) {
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve(true, true)
|
||||
resolve(true)
|
||||
}()
|
||||
{
|
||||
v, known, err := out.Bool()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.True(t, v)
|
||||
}
|
||||
{
|
||||
b := (*BoolOutput)(out)
|
||||
v, known, err := b.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.True(t, v)
|
||||
b := BoolOutput(out)
|
||||
assertApplied(t, b.Apply(func(v bool) (interface{}, error) {
|
||||
assert.True(t, v)
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapOutputs(t *testing.T) {
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve(map[string]interface{}{
|
||||
"x": 1,
|
||||
"y": false,
|
||||
"z": "abc",
|
||||
}, true)
|
||||
})
|
||||
}()
|
||||
{
|
||||
v, known, err := out.Map()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 1, v["x"])
|
||||
assert.Equal(t, false, v["y"])
|
||||
assert.Equal(t, "abc", v["z"])
|
||||
}
|
||||
{
|
||||
b := (*MapOutput)(out)
|
||||
v, known, err := b.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 1, v["x"])
|
||||
assert.Equal(t, false, v["y"])
|
||||
assert.Equal(t, "abc", v["z"])
|
||||
b := MapOutput(out)
|
||||
assertApplied(t, b.Apply(func(v map[string]interface{}) (interface{}, error) {
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 1, v["x"])
|
||||
assert.Equal(t, false, v["y"])
|
||||
assert.Equal(t, "abc", v["z"])
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberOutputs(t *testing.T) {
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve(42.345, true)
|
||||
resolve(42.345)
|
||||
}()
|
||||
{
|
||||
v, known, err := out.Float64()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, 42.345, v)
|
||||
}
|
||||
{
|
||||
b := (*Float64Output)(out)
|
||||
v, known, err := b.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, 42.345, v)
|
||||
b := Float64Output(out)
|
||||
assertApplied(t, b.Apply(func(v float64) (interface{}, error) {
|
||||
assert.Equal(t, 42.345, v)
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringOutputs(t *testing.T) {
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
resolve("a stringy output", true)
|
||||
resolve("a stringy output")
|
||||
}()
|
||||
{
|
||||
v, known, err := out.String()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, "a stringy output", v)
|
||||
}
|
||||
{
|
||||
b := (*StringOutput)(out)
|
||||
v, known, err := b.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, "a stringy output", v)
|
||||
b := StringOutput(out)
|
||||
assertApplied(t, b.Apply(func(v string) (interface{}, error) {
|
||||
assert.Equal(t, "a stringy output", v)
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOutputToOutput(t *testing.T) {
|
||||
// Test that resolving an output to an output yields the value, not the output.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
other, resolveOther, _ := NewOutput(nil)
|
||||
resolve(other, true)
|
||||
go func() { resolveOther(99, true) }()
|
||||
other, resolveOther, _ := NewOutput()
|
||||
resolve(other)
|
||||
go func() { resolveOther(99) }()
|
||||
}()
|
||||
v, known, err := out.Value()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, v, 99)
|
||||
assertApplied(t, out.Apply(func(v interface{}) (interface{}, error) {
|
||||
assert.Equal(t, v, 99)
|
||||
return nil, nil
|
||||
}))
|
||||
}
|
||||
// Similarly, test that resolving an output to a rejected output yields an error.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() {
|
||||
other, _, rejectOther := NewOutput(nil)
|
||||
resolve(other, true)
|
||||
other, _, rejectOther := NewOutput()
|
||||
resolve(other)
|
||||
go func() { rejectOther(errors.New("boom")) }()
|
||||
}()
|
||||
v, _, err := out.Value()
|
||||
v, _, err := out.s.await(context.Background())
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
|
@ -195,81 +165,104 @@ func TestResolveOutputToOutput(t *testing.T) {
|
|||
func TestOutputApply(t *testing.T) {
|
||||
// Test that resolved outputs lead to applies being run.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
go func() { resolve(42, true) }()
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() { resolve(42) }()
|
||||
var ranApp bool
|
||||
b := (*IntOutput)(out)
|
||||
b := IntOutput(out)
|
||||
app := b.Apply(func(v int) (interface{}, error) {
|
||||
ranApp = true
|
||||
return v + 1, nil
|
||||
})
|
||||
v, known, err := app.Value()
|
||||
v, known, err := app.s.await(context.Background())
|
||||
assert.True(t, ranApp)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, v, 43)
|
||||
}
|
||||
// Test that resolved, but known outputs, skip the running of applies.
|
||||
// Test that resolved, but unknown outputs, skip the running of applies.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
go func() { resolve(42, false) }()
|
||||
out := newOutput()
|
||||
go func() { out.s.fulfill(42, false, nil) }()
|
||||
var ranApp bool
|
||||
b := (*IntOutput)(out)
|
||||
b := IntOutput(out)
|
||||
app := b.Apply(func(v int) (interface{}, error) {
|
||||
ranApp = true
|
||||
return v + 1, nil
|
||||
})
|
||||
_, known, err := app.Value()
|
||||
_, known, err := app.s.await(context.Background())
|
||||
assert.False(t, ranApp)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, known)
|
||||
}
|
||||
// Test that rejected outputs do not run the apply, and instead flow the error.
|
||||
{
|
||||
out, _, reject := NewOutput(nil)
|
||||
out, _, reject := NewOutput()
|
||||
go func() { reject(errors.New("boom")) }()
|
||||
var ranApp bool
|
||||
b := (*IntOutput)(out)
|
||||
b := IntOutput(out)
|
||||
app := b.Apply(func(v int) (interface{}, error) {
|
||||
ranApp = true
|
||||
return v + 1, nil
|
||||
})
|
||||
v, _, err := app.Value()
|
||||
v, _, err := app.s.await(context.Background())
|
||||
assert.False(t, ranApp)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
// Test that an an apply that returns an output returns the resolution of that output, not the output itself.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
go func() { resolve(42, true) }()
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() { resolve(42) }()
|
||||
var ranApp bool
|
||||
b := (*IntOutput)(out)
|
||||
b := IntOutput(out)
|
||||
app := b.Apply(func(v int) (interface{}, error) {
|
||||
other, resolveOther, _ := NewOutput(nil)
|
||||
go func() { resolveOther(v+1, true) }()
|
||||
other, resolveOther, _ := NewOutput()
|
||||
go func() { resolveOther(v + 1) }()
|
||||
ranApp = true
|
||||
return other, nil
|
||||
})
|
||||
v, known, err := app.Value()
|
||||
v, known, err := app.s.await(context.Background())
|
||||
assert.True(t, ranApp)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, v, 43)
|
||||
|
||||
app = b.Apply(func(v int) (interface{}, error) {
|
||||
other, resolveOther, _ := NewOutput()
|
||||
go func() { resolveOther(v + 2) }()
|
||||
ranApp = true
|
||||
return IntOutput(other), nil
|
||||
})
|
||||
v, known, err = app.s.await(context.Background())
|
||||
assert.True(t, ranApp)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, known)
|
||||
assert.Equal(t, v, 44)
|
||||
}
|
||||
// Test that an an apply that reject an output returns the rejection of that output, not the output itself.
|
||||
{
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
go func() { resolve(42, true) }()
|
||||
out, resolve, _ := NewOutput()
|
||||
go func() { resolve(42) }()
|
||||
var ranApp bool
|
||||
b := (*IntOutput)(out)
|
||||
b := IntOutput(out)
|
||||
app := b.Apply(func(v int) (interface{}, error) {
|
||||
other, _, rejectOther := NewOutput(nil)
|
||||
other, _, rejectOther := NewOutput()
|
||||
go func() { rejectOther(errors.New("boom")) }()
|
||||
ranApp = true
|
||||
return other, nil
|
||||
})
|
||||
v, _, err := app.Value()
|
||||
v, _, err := app.s.await(context.Background())
|
||||
assert.True(t, ranApp)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, v)
|
||||
|
||||
app = b.Apply(func(v int) (interface{}, error) {
|
||||
other, _, rejectOther := NewOutput()
|
||||
go func() { rejectOther(errors.New("boom")) }()
|
||||
ranApp = true
|
||||
return IntOutput(other), nil
|
||||
})
|
||||
v, _, err = app.s.await(context.Background())
|
||||
assert.True(t, ranApp)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, v)
|
||||
|
|
|
@ -24,7 +24,7 @@ type (
|
|||
// Resource represents a cloud resource managed by Pulumi.
|
||||
type Resource interface {
|
||||
// URN is this resource's stable logical URN used to distinctly address it before, during, and after deployments.
|
||||
URN() *URNOutput
|
||||
URN() URNOutput
|
||||
}
|
||||
|
||||
// CustomResource is a cloud resource whose create, read, update, and delete (CRUD) operations are managed by performing
|
||||
|
@ -34,7 +34,7 @@ type CustomResource interface {
|
|||
Resource
|
||||
// ID is the provider-assigned unique identifier for this managed resource. It is set during deployments,
|
||||
// but might be missing ("") during planning phases.
|
||||
ID() *IDOutput
|
||||
ID() IDOutput
|
||||
}
|
||||
|
||||
// ComponentResource is a resource that aggregates one or more other child resources into a higher level abstraction.
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
||||
|
@ -27,7 +28,9 @@ import (
|
|||
)
|
||||
|
||||
// marshalInputs turns resource property inputs into a gRPC struct suitable for marshaling.
|
||||
func marshalInputs(props map[string]interface{}) (*structpb.Struct, map[string][]URN, []URN, error) {
|
||||
func marshalInputs(props map[string]interface{},
|
||||
keepUnknowns bool) (*structpb.Struct, map[string][]URN, []URN, error) {
|
||||
|
||||
var depURNs []URN
|
||||
pmap, pdeps := make(map[string]interface{}), make(map[string][]URN)
|
||||
for key := range props {
|
||||
|
@ -42,7 +45,7 @@ func marshalInputs(props map[string]interface{}) (*structpb.Struct, map[string][
|
|||
// Record all dependencies accumulated from reading this property.
|
||||
deps := make([]URN, 0, len(resourceDeps))
|
||||
for _, dep := range resourceDeps {
|
||||
depURN, err := dep.URN().Value()
|
||||
depURN, _, err := dep.URN().await(context.TODO())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
@ -56,7 +59,7 @@ func marshalInputs(props map[string]interface{}) (*structpb.Struct, map[string][
|
|||
// Marshal all properties for the RPC call.
|
||||
m, err := plugin.MarshalProperties(
|
||||
resource.NewPropertyMapFromMap(pmap),
|
||||
plugin.MarshalOptions{KeepUnknowns: true},
|
||||
plugin.MarshalOptions{KeepUnknowns: keepUnknowns},
|
||||
)
|
||||
return m, pdeps, depURNs, err
|
||||
}
|
||||
|
@ -73,114 +76,135 @@ const (
|
|||
|
||||
// marshalInput marshals an input value, returning its raw serializable value along with any dependencies.
|
||||
func marshalInput(v interface{}) (interface{}, []Resource, error) {
|
||||
// If nil, just return that.
|
||||
if v == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
for {
|
||||
// If v is nil, just return that.
|
||||
if v == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Next, look for some well known types.
|
||||
switch t := v.(type) {
|
||||
case bool, int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64, string:
|
||||
return t, nil, nil
|
||||
case asset.Asset:
|
||||
return map[string]interface{}{
|
||||
rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig,
|
||||
"path": t.Path(),
|
||||
"text": t.Text(),
|
||||
"uri": t.URI(),
|
||||
}, nil, nil
|
||||
case asset.Archive:
|
||||
var assets map[string]interface{}
|
||||
if as := t.Assets(); as != nil {
|
||||
assets = make(map[string]interface{})
|
||||
for k, a := range as {
|
||||
aa, _, err := marshalInput(a)
|
||||
// If this is an Output, recurse.
|
||||
if out, ok := isOutput(v); ok {
|
||||
return marshalInputOutput(out)
|
||||
}
|
||||
|
||||
// Next, look for some well known types.
|
||||
switch v := v.(type) {
|
||||
case asset.Asset:
|
||||
return map[string]interface{}{
|
||||
rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig,
|
||||
"path": v.Path(),
|
||||
"text": v.Text(),
|
||||
"uri": v.URI(),
|
||||
}, nil, nil
|
||||
case asset.Archive:
|
||||
var assets map[string]interface{}
|
||||
if as := v.Assets(); as != nil {
|
||||
assets = make(map[string]interface{})
|
||||
for k, a := range as {
|
||||
aa, _, err := marshalInput(a)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
assets[k] = aa
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig,
|
||||
"assets": assets,
|
||||
"path": v.Path(),
|
||||
"uri": v.URI(),
|
||||
}, nil, nil
|
||||
case CustomResource:
|
||||
// Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.
|
||||
e, d, err := marshalInput(v.ID())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return e, append([]Resource{v}, d...), nil
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Bool:
|
||||
return rv.Bool(), nil, nil
|
||||
case reflect.Int:
|
||||
return int(rv.Int()), nil, nil
|
||||
case reflect.Int8:
|
||||
return int8(rv.Int()), nil, nil
|
||||
case reflect.Int16:
|
||||
return int16(rv.Int()), nil, nil
|
||||
case reflect.Int32:
|
||||
return int32(rv.Int()), nil, nil
|
||||
case reflect.Int64:
|
||||
return rv.Int(), nil, nil
|
||||
case reflect.Uint:
|
||||
return uint(rv.Uint()), nil, nil
|
||||
case reflect.Uint8:
|
||||
return uint8(rv.Uint()), nil, nil
|
||||
case reflect.Uint16:
|
||||
return uint16(rv.Uint()), nil, nil
|
||||
case reflect.Uint32:
|
||||
return uint32(rv.Uint()), nil, nil
|
||||
case reflect.Uint64:
|
||||
return rv.Uint(), nil, nil
|
||||
case reflect.Float32:
|
||||
return float32(rv.Float()), nil, nil
|
||||
case reflect.Float64:
|
||||
return rv.Float(), nil, nil
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
// Dereference non-nil pointers and interfaces.
|
||||
if rv.IsNil() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
rv = rv.Elem()
|
||||
case reflect.Array, reflect.Slice:
|
||||
// If an array or a slice, create a new array by recursing into elements.
|
||||
var arr []interface{}
|
||||
var deps []Resource
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
elem := rv.Index(i)
|
||||
e, d, err := marshalInput(elem.Interface())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
assets[k] = aa
|
||||
arr = append(arr, e)
|
||||
deps = append(deps, d...)
|
||||
}
|
||||
}
|
||||
return arr, deps, nil
|
||||
case reflect.Map:
|
||||
// For maps, only support string-based keys, and recurse into the values.
|
||||
obj := make(map[string]interface{})
|
||||
var deps []Resource
|
||||
for _, key := range rv.MapKeys() {
|
||||
k, ok := key.Interface().(string)
|
||||
if !ok {
|
||||
return nil, nil,
|
||||
errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface()))
|
||||
}
|
||||
value := rv.MapIndex(key)
|
||||
mv, d, err := marshalInput(value.Interface())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig,
|
||||
"assets": assets,
|
||||
"path": t.Path(),
|
||||
"uri": t.URI(),
|
||||
}, nil, nil
|
||||
case Output:
|
||||
return marshalInputOutput(&t)
|
||||
case *Output:
|
||||
return marshalInputOutput(t)
|
||||
case CustomResource:
|
||||
// Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a
|
||||
e, d, err := marshalInput(t.ID())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
obj[k] = mv
|
||||
deps = append(deps, d...)
|
||||
}
|
||||
return obj, deps, nil
|
||||
case reflect.String:
|
||||
return rv.String(), nil, nil
|
||||
default:
|
||||
return nil, nil, errors.Errorf("unrecognized input property type: %v (%T)", v, v)
|
||||
}
|
||||
return e, append([]Resource{t}, d...), nil
|
||||
v = rv.Interface()
|
||||
}
|
||||
|
||||
// Finally, handle the usual primitives (numbers, strings, arrays, maps, ...)
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rk := rv.Type().Kind(); rk {
|
||||
case reflect.Array, reflect.Slice:
|
||||
// If an array or a slice, create a new array by recursing into elements.
|
||||
var arr []interface{}
|
||||
var deps []Resource
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
elem := rv.Index(i)
|
||||
e, d, err := marshalInput(elem.Interface())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
arr = append(arr, e)
|
||||
deps = append(deps, d...)
|
||||
}
|
||||
return arr, deps, nil
|
||||
case reflect.Map:
|
||||
// For maps, only support string-based keys, and recurse into the values.
|
||||
obj := make(map[string]interface{})
|
||||
var deps []Resource
|
||||
for _, key := range rv.MapKeys() {
|
||||
k, ok := key.Interface().(string)
|
||||
if !ok {
|
||||
return nil, nil,
|
||||
errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface()))
|
||||
}
|
||||
value := rv.MapIndex(key)
|
||||
mv, d, err := marshalInput(value.Interface())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
obj[k] = mv
|
||||
deps = append(deps, d...)
|
||||
}
|
||||
return obj, deps, nil
|
||||
case reflect.Ptr:
|
||||
// See if this is an alias for *Output. If so, convert to an *Output, and recurse.
|
||||
ot := reflect.TypeOf(&Output{})
|
||||
if rv.Type().ConvertibleTo(ot) {
|
||||
oo := rv.Convert(ot)
|
||||
return marshalInput(oo.Interface())
|
||||
}
|
||||
|
||||
// For all other pointers, recurse into the underlying value.
|
||||
if rv.IsNil() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return marshalInput(rv.Elem().Interface())
|
||||
case reflect.String:
|
||||
return marshalInput(rv.String())
|
||||
}
|
||||
|
||||
return nil, nil, errors.Errorf("unrecognized input property type: %v (%v)", v, reflect.TypeOf(v))
|
||||
}
|
||||
|
||||
func marshalInputOutput(out *Output) (interface{}, []Resource, error) {
|
||||
func marshalInputOutput(out Output) (interface{}, []Resource, error) {
|
||||
// Await the value and return its raw value.
|
||||
ov, known, err := out.Value()
|
||||
ov, known, err := out.s.await(context.TODO())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -191,11 +215,11 @@ func marshalInputOutput(out *Output) (interface{}, []Resource, error) {
|
|||
if merr != nil {
|
||||
return nil, nil, merr
|
||||
}
|
||||
return e, append(out.Deps(), d...), nil
|
||||
return e, append(out.s.dependencies(), d...), nil
|
||||
}
|
||||
|
||||
// Otherwise, simply return the unknown value sentinel.
|
||||
return rpcTokenUnknownValue, out.Deps(), nil
|
||||
return rpcTokenUnknownValue, out.s.dependencies(), nil
|
||||
}
|
||||
|
||||
// unmarshalOutputs unmarshals all the outputs into a simple map.
|
||||
|
|
|
@ -25,8 +25,11 @@ import (
|
|||
// TestMarshalRoundtrip ensures that marshaling a complex structure to and from its on-the-wire gRPC format succeeds.
|
||||
func TestMarshalRoundtrip(t *testing.T) {
|
||||
// Create interesting inputs.
|
||||
out, resolve, _ := NewOutput(nil)
|
||||
resolve("outputty", true)
|
||||
out, resolve, _ := NewOutput()
|
||||
resolve("outputty")
|
||||
out2 := newOutput()
|
||||
out2.s.fulfill(nil, false, nil)
|
||||
out3 := Output{}
|
||||
input := map[string]interface{}{
|
||||
"s": "a string",
|
||||
"a": true,
|
||||
|
@ -47,10 +50,13 @@ func TestMarshalRoundtrip(t *testing.T) {
|
|||
"y": 999.9,
|
||||
"z": false,
|
||||
},
|
||||
"g": out2,
|
||||
"h": URN("foo"),
|
||||
"i": out3,
|
||||
}
|
||||
|
||||
// Marshal those inputs.
|
||||
m, pdeps, deps, err := marshalInputs(input)
|
||||
m, pdeps, deps, err := marshalInputs(input, true)
|
||||
if !assert.Nil(t, err) {
|
||||
assert.Equal(t, len(input), len(pdeps))
|
||||
assert.Equal(t, 0, len(deps))
|
||||
|
@ -83,9 +89,82 @@ func TestMarshalRoundtrip(t *testing.T) {
|
|||
assert.Equal(t, "y", am["x"])
|
||||
assert.Equal(t, 999.9, am["y"])
|
||||
assert.Equal(t, false, am["z"])
|
||||
assert.Equal(t, rpcTokenUnknownValue, res["g"])
|
||||
assert.Equal(t, "foo", res["h"])
|
||||
assert.Equal(t, rpcTokenUnknownValue, res["i"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal those inputs without unknowns.
|
||||
m, pdeps, deps, err = marshalInputs(input, false)
|
||||
if !assert.Nil(t, err) {
|
||||
assert.Equal(t, len(input), len(pdeps))
|
||||
assert.Equal(t, 0, len(deps))
|
||||
|
||||
// Now just unmarshal and ensure the resulting map matches.
|
||||
res, err := unmarshalOutputs(m)
|
||||
if !assert.Nil(t, err) {
|
||||
if !assert.NotNil(t, res) {
|
||||
assert.Equal(t, "a string", res["s"])
|
||||
assert.Equal(t, true, res["a"])
|
||||
assert.Equal(t, 42, res["b"])
|
||||
assert.Equal(t, "put a lime in the coconut", res["cStringAsset"].(asset.Asset).Text())
|
||||
assert.Equal(t, "foo.txt", res["cFileAsset"].(asset.Asset).Path())
|
||||
assert.Equal(t, "https://pulumi.com/fake/asset.txt", res["cRemoteAsset"].(asset.Asset).URI())
|
||||
ar := res["dAssetArchive"].(asset.Archive).Assets()
|
||||
assert.Equal(t, 2, len(ar))
|
||||
assert.Equal(t, "bar.txt", ar["subAsset"].(asset.Asset).Path())
|
||||
assert.Equal(t, "bar.zip", ar["subrchive"].(asset.Archive).Path())
|
||||
assert.Equal(t, "foo.zip", res["dFileArchive"].(asset.Archive).Path())
|
||||
assert.Equal(t, "https://pulumi.com/fake/archive.zip", res["dRemoteArchive"].(asset.Archive).URI())
|
||||
assert.Equal(t, "outputty", res["e"])
|
||||
aa := res["fArray"].([]interface{})
|
||||
assert.Equal(t, 4, len(aa))
|
||||
assert.Equal(t, 0, aa[0])
|
||||
assert.Equal(t, 1.3, aa[1])
|
||||
assert.Equal(t, "x", aa[2])
|
||||
assert.Equal(t, false, aa[3])
|
||||
am := res["fMap"].(map[string]interface{})
|
||||
assert.Equal(t, 3, len(am))
|
||||
assert.Equal(t, "y", am["x"])
|
||||
assert.Equal(t, 999.9, am["y"])
|
||||
assert.Equal(t, false, am["z"])
|
||||
assert.Equal(t, nil, res["g"])
|
||||
assert.Equal(t, "foo", res["h"])
|
||||
assert.Equal(t, nil, res["i"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceState(t *testing.T) {
|
||||
state := makeResourceState(true, map[string]interface{}{"baz": nil})
|
||||
|
||||
s, _, _, _ := marshalInputs(map[string]interface{}{"baz": "qux"}, true)
|
||||
state.resolve(false, nil, nil, "foo", "bar", s)
|
||||
|
||||
input := map[string]interface{}{
|
||||
"urn": state.urn,
|
||||
"id": state.id,
|
||||
"baz": state.State["baz"],
|
||||
}
|
||||
m, pdeps, deps, err := marshalInputs(input, true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, map[string][]URN{
|
||||
"urn": {"foo"},
|
||||
"id": {"foo"},
|
||||
"baz": {"foo"},
|
||||
}, pdeps)
|
||||
assert.Equal(t, []URN{"foo", "foo", "foo"}, deps)
|
||||
|
||||
res, err := unmarshalOutputs(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"urn": "foo",
|
||||
"id": "bar",
|
||||
"baz": "qux",
|
||||
}, res)
|
||||
}
|
||||
|
||||
func TestUnmarshalUnsupportedSecret(t *testing.T) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
|
@ -77,7 +77,7 @@ func RunWithContext(ctx *Context, body RunFunc) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.stackR, err = reg.URN().Value()
|
||||
ctx.stackR, _, err = reg.URN().await(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue