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:
Pat Gavlin 2019-11-12 14:20:06 -08:00 committed by GitHub
parent d81ac16132
commit a7f61a59b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 834 additions and 767 deletions

View file

@ -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.

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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) {

View file

@ -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
}