From 20af051cafd4b6b3d469aa3cd65bf5c05d4b3b1c Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sat, 9 Jun 2018 16:59:53 -0700 Subject: [PATCH] Implement unknown outputs This commit implements unknown outputs in the same style as our Node.js language provider. That is to say, during previews, it's possible that certain outputs will not have known values. In those cases, we want to flow sufficient information through the resolution of values, so that we may skip applies. We also return this fact from the direct accessors. --- sdk/go/pulumi/context.go | 12 +- sdk/go/pulumi/properties.go | 321 +++++++++++++++++-------------- sdk/go/pulumi/properties_test.go | 90 ++++++--- sdk/go/pulumi/rpc.go | 23 ++- sdk/go/pulumi/rpc_test.go | 2 +- 5 files changed, 264 insertions(+), 184 deletions(-) diff --git a/sdk/go/pulumi/context.go b/sdk/go/pulumi/context.go index e5e3a105c..1eb0f9a93 100644 --- a/sdk/go/pulumi/context.go +++ b/sdk/go/pulumi/context.go @@ -162,14 +162,14 @@ func (ctx *Context) RegisterResource( urn, resolveURN, rejectURN := NewOutput(nil) var id *Output - var resolveID func(interface{}) + var resolveID func(interface{}, bool) var rejectID func(error) if custom { id, resolveID, rejectID = NewOutput(nil) } state := make(map[string]*Output) - resolveState := make(map[string]func(interface{})) + resolveState := make(map[string]func(interface{}, bool)) rejectState := make(map[string]func(error)) for _, key := range keys { state[key], resolveState[key], rejectState[key] = NewOutput(nil) @@ -208,16 +208,18 @@ func (ctx *Context) RegisterResource( } } else { glog.V(9).Infof("RegisterResource(%s, %s): success: %s %s %d", t, name, resp.Urn, resp.Id, len(outprops)) - resolveURN(URN(resp.Urn)) + resolveURN(URN(resp.Urn), true) if resolveID != nil { - resolveID(ID(resp.Id)) + resolveID(ID(resp.Id), true) } for _, key := range keys { out, err := unmarshalOutput(outprops[key]) if err != nil { rejectState[key](err) } else { - resolveState[key](out) + // During previews, it's possible that nils will be returned due to unknown values. + known := !ctx.DryRun() || out != nil + resolveState[key](out, known) } } } diff --git a/sdk/go/pulumi/properties.go b/sdk/go/pulumi/properties.go index 13e4cf551..688e4ee28 100644 --- a/sdk/go/pulumi/properties.go +++ b/sdk/go/pulumi/properties.go @@ -34,12 +34,13 @@ type Output struct { type valueOrError struct { value interface{} // a value, if the output resolved to a value. err error // an error, if the producer yielded an error instead of a value. + known bool // true if this value is known, versus just being a placeholder during previews. } // NewOutput returns an output value that can be used to rendezvous with the production of a value or error. The // function returns the output itself, plus two functions: one for resolving a value, and another for rejecting with an // error; exactly one function must be called. This acts like a promise. -func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { +func NewOutput(deps []Resource) (*Output, func(interface{}, bool), func(error)) { out := &Output{ sync: make(chan *valueOrError, 1), deps: deps, @@ -49,19 +50,19 @@ func NewOutput(deps []Resource) (*Output, func(interface{}), func(error)) { // resolve will resolve the output. It is not exported, because we want to control the capabilities tightly, such // that anybody who happens to have an Output is not allowed to resolve it; only those who created it can. -func (out *Output) resolve(v interface{}) { +func (out *Output) resolve(v interface{}, known bool) { // If v is another output, chain this rather than resolving to an output directly. - if other, isOut := v.(*Output); isOut { + if other, isOut := v.(*Output); known && isOut { go func() { - real, err := other.Value() + real, otherKnown, err := other.Value() if err != nil { out.reject(err) } else { - out.resolve(real) + out.resolve(real, otherKnown) } }() } else { - out.sync <- &valueOrError{value: v} + out.sync <- &valueOrError{value: v, known: known} } } @@ -78,27 +79,33 @@ func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Outp result, resolve, reject := NewOutput(out.Deps()) go func() { for { - v, err := out.Value() + v, known, err := out.Value() if err != nil { reject(err) break } else { - // If we have a value, run the applier to transform it. - u, err := applier(v) - if err != nil { - reject(err) - break - } else { - // Now that we've transformed the value, it's possible we have another output. If so, pluck it - // out and go around to await it until we hit a real value. Note that we are not capturing the - // resources of this inner output, intentionally, as the output returned should be related to - // this output already. - if newout, ok := v.(*Output); ok { - out = newout - } else { - resolve(u) + if known { + // If we have a known value, run the applier to transform it. + u, err := applier(v) + if err != nil { + reject(err) break + } else { + // Now that we've transformed the value, it's possible we have another output. If so, pluck it + // out and go around to await it until we hit a real value. Note that we are not capturing the + // resources of this inner output, intentionally, as the output returned should be related to + // this output already. + if newout, ok := v.(*Output); ok { + out = newout + } else { + resolve(u, true) + break + } } + } else { + // If the value isn't known, skip the apply function. + resolve(nil, false) + break } } } @@ -107,12 +114,10 @@ func (out *Output) Apply(applier func(v interface{}) (interface{}, error)) *Outp } // Deps returns the dependencies for this output property. -func (out *Output) Deps() []Resource { - return out.deps -} +func (out *Output) Deps() []Resource { return out.deps } // Value retrieves the underlying value for this output property. -func (out *Output) Value() (interface{}, error) { +func (out *Output) Value() (interface{}, bool, error) { // If neither error nor value are available, first await the channel. Only one Goroutine will make it through this // and is responsible for closing the channel, to signal to other awaiters that it's safe to read the values. if out.voe == nil { @@ -121,184 +126,184 @@ func (out *Output) Value() (interface{}, error) { close(out.sync) } } - return out.voe.value, out.voe.err + return out.voe.value, out.voe.known, out.voe.err } // Archive retrives the underlying value for this output property as an archive. -func (out *Output) Archive() (asset.Archive, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Archive() (asset.Archive, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return v.(asset.Archive), nil + return v.(asset.Archive), true, nil } // Array retrives the underlying value for this output property as an array. -func (out *Output) Array() ([]interface{}, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Array() ([]interface{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return cast.ToSlice(v), nil + return cast.ToSlice(v), true, nil } // Asset retrives the underlying value for this output property as an asset. -func (out *Output) Asset() (asset.Asset, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Asset() (asset.Asset, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return v.(asset.Asset), nil + return v.(asset.Asset), true, nil } // Bool retrives the underlying value for this output property as a bool. -func (out *Output) Bool() (bool, error) { - v, err := out.Value() - if err != nil { - return false, err +func (out *Output) Bool() (bool, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return false, known, err } - return cast.ToBool(v), nil + return cast.ToBool(v), true, nil } // Map retrives the underlying value for this output property as a map. -func (out *Output) Map() (map[string]interface{}, error) { - v, err := out.Value() - if err != nil { - return nil, err +func (out *Output) Map() (map[string]interface{}, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return nil, known, err } - return cast.ToStringMap(v), nil + return cast.ToStringMap(v), true, nil } // Float32 retrives the underlying value for this output property as a float32. -func (out *Output) Float32() (float32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Float32() (float32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToFloat32(v), nil + return cast.ToFloat32(v), true, nil } // Float64 retrives the underlying value for this output property as a float64. -func (out *Output) Float64() (float64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Float64() (float64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToFloat64(v), nil + return cast.ToFloat64(v), true, nil } // ID retrives the underlying value for this output property as an ID. -func (out *Output) ID() (ID, error) { - v, err := out.Value() - if err != nil { - return "", err +func (out *Output) ID() (ID, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err } - return ID(cast.ToString(v)), nil + return ID(cast.ToString(v)), true, nil } // Int retrives the underlying value for this output property as a int. -func (out *Output) Int() (int, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int() (int, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt(v), nil + return cast.ToInt(v), true, nil } // Int8 retrives the underlying value for this output property as a int8. -func (out *Output) Int8() (int8, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int8() (int8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt8(v), nil + return cast.ToInt8(v), true, nil } // Int16 retrives the underlying value for this output property as a int16. -func (out *Output) Int16() (int16, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int16() (int16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt16(v), nil + return cast.ToInt16(v), true, nil } // Int32 retrives the underlying value for this output property as a int32. -func (out *Output) Int32() (int32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int32() (int32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt32(v), nil + return cast.ToInt32(v), true, nil } // Int64 retrives the underlying value for this output property as a int64. -func (out *Output) Int64() (int64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Int64() (int64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToInt64(v), nil + return cast.ToInt64(v), true, nil } // String retrives the underlying value for this output property as a string. -func (out *Output) String() (string, error) { - v, err := out.Value() - if err != nil { - return "", err +func (out *Output) String() (string, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return "", known, err } - return cast.ToString(v), nil + return cast.ToString(v), true, nil } // Uint retrives the underlying value for this output property as a uint. -func (out *Output) Uint() (uint, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint() (uint, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint(v), nil + return cast.ToUint(v), true, nil } // Uint8 retrives the underlying value for this output property as a uint8. -func (out *Output) Uint8() (uint8, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint8() (uint8, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint8(v), nil + return cast.ToUint8(v), true, nil } // Uint16 retrives the underlying value for this output property as a uint16. -func (out *Output) Uint16() (uint16, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint16() (uint16, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint16(v), nil + return cast.ToUint16(v), true, nil } // Uint32 retrives the underlying value for this output property as a uint32. -func (out *Output) Uint32() (uint32, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint32() (uint32, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint32(v), nil + return cast.ToUint32(v), true, nil } // Uint64 retrives the underlying value for this output property as a uint64. -func (out *Output) Uint64() (uint64, error) { - v, err := out.Value() - if err != nil { - return 0, err +func (out *Output) Uint64() (uint64, bool, error) { + v, known, err := out.Value() + if err != nil || !known { + return 0, known, err } - return cast.ToUint64(v), nil + return cast.ToUint64(v), true, nil } // URN retrives the underlying value for this output property as a URN. func (out *Output) URN() (URN, error) { - v, err := out.Value() - if err != nil { + v, known, err := out.Value() + if err != nil || !known { return "", err } return URN(cast.ToString(v)), nil @@ -311,7 +316,9 @@ type Outputs map[string]*Output type ArchiveOutput Output // Value returns the underlying archive value. -func (out *ArchiveOutput) Value() (asset.Archive, error) { return (*Output)(out).Archive() } +func (out *ArchiveOutput) Value() (asset.Archive, bool, error) { + return (*Output)(out).Archive() +} // Apply applies a transformation to the archive value when it is available. func (out *ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error)) *Output { @@ -324,7 +331,9 @@ func (out *ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error) type ArrayOutput Output // Value returns the underlying array value. -func (out *ArrayOutput) Value() ([]interface{}, error) { return (*Output)(out).Array() } +func (out *ArrayOutput) Value() ([]interface{}, bool, error) { + return (*Output)(out).Array() +} // Apply applies a transformation to the array value when it is available. func (out *ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) *Output { @@ -337,7 +346,9 @@ func (out *ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) type AssetOutput Output // Value returns the underlying asset value. -func (out *AssetOutput) Value() (asset.Asset, error) { return (*Output)(out).Asset() } +func (out *AssetOutput) Value() (asset.Asset, bool, error) { + return (*Output)(out).Asset() +} // Apply applies a transformation to the asset value when it is available. func (out *AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) *Output { @@ -350,7 +361,9 @@ func (out *AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) *O type BoolOutput Output // Value returns the underlying bool value. -func (out *BoolOutput) Value() (bool, error) { return (*Output)(out).Bool() } +func (out *BoolOutput) Value() (bool, bool, error) { + return (*Output)(out).Bool() +} // Apply applies a transformation to the bool value when it is available. func (out *BoolOutput) Apply(applier func(bool) (interface{}, error)) *Output { @@ -363,7 +376,9 @@ func (out *BoolOutput) Apply(applier func(bool) (interface{}, error)) *Output { type Float32Output Output // Value returns the underlying number value. -func (out *Float32Output) Value() (float32, error) { return (*Output)(out).Float32() } +func (out *Float32Output) Value() (float32, bool, error) { + return (*Output)(out).Float32() +} // Apply applies a transformation to the float32 value when it is available. func (out *Float32Output) Apply(applier func(float32) (interface{}, error)) *Output { @@ -376,7 +391,9 @@ func (out *Float32Output) Apply(applier func(float32) (interface{}, error)) *Out type Float64Output Output // Value returns the underlying number value. -func (out *Float64Output) Value() (float64, error) { return (*Output)(out).Float64() } +func (out *Float64Output) Value() (float64, bool, error) { + return (*Output)(out).Float64() +} // Apply applies a transformation to the float64 value when it is available. func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Output { @@ -389,7 +406,9 @@ func (out *Float64Output) Apply(applier func(float64) (interface{}, error)) *Out type IntOutput Output // Value returns the underlying number value. -func (out *IntOutput) Value() (int, error) { return (*Output)(out).Int() } +func (out *IntOutput) Value() (int, bool, error) { + return (*Output)(out).Int() +} // Apply applies a transformation to the int value when it is available. func (out *IntOutput) Apply(applier func(int) (interface{}, error)) *Output { @@ -402,7 +421,9 @@ func (out *IntOutput) Apply(applier func(int) (interface{}, error)) *Output { type Int8Output Output // Value returns the underlying number value. -func (out *Int8Output) Value() (int8, error) { return (*Output)(out).Int8() } +func (out *Int8Output) Value() (int8, bool, error) { + return (*Output)(out).Int8() +} // Apply applies a transformation to the int8 value when it is available. func (out *Int8Output) Apply(applier func(int8) (interface{}, error)) *Output { @@ -415,7 +436,9 @@ func (out *Int8Output) Apply(applier func(int8) (interface{}, error)) *Output { type Int16Output Output // Value returns the underlying number value. -func (out *Int16Output) Value() (int16, error) { return (*Output)(out).Int16() } +func (out *Int16Output) Value() (int16, bool, error) { + return (*Output)(out).Int16() +} // Apply applies a transformation to the int16 value when it is available. func (out *Int16Output) Apply(applier func(int16) (interface{}, error)) *Output { @@ -428,7 +451,9 @@ func (out *Int16Output) Apply(applier func(int16) (interface{}, error)) *Output type Int32Output Output // Value returns the underlying number value. -func (out *Int32Output) Value() (int32, error) { return (*Output)(out).Int32() } +func (out *Int32Output) Value() (int32, bool, error) { + return (*Output)(out).Int32() +} // Apply applies a transformation to the int32 value when it is available. func (out *Int32Output) Apply(applier func(int32) (interface{}, error)) *Output { @@ -441,7 +466,7 @@ func (out *Int32Output) Apply(applier func(int32) (interface{}, error)) *Output type Int64Output Output // Value returns the underlying number value. -func (out *Int64Output) Value() (int64, error) { return (*Output)(out).Int64() } +func (out *Int64Output) Value() (int64, bool, error) { return (*Output)(out).Int64() } // Apply applies a transformation to the int64 value when it is available. func (out *Int64Output) Apply(applier func(int64) (interface{}, error)) *Output { @@ -454,7 +479,9 @@ func (out *Int64Output) Apply(applier func(int64) (interface{}, error)) *Output type MapOutput Output // Value returns the underlying number value. -func (out *MapOutput) Value() (map[string]interface{}, error) { return (*Output)(out).Map() } +func (out *MapOutput) Value() (map[string]interface{}, bool, error) { + return (*Output)(out).Map() +} // Apply applies a transformation to the number value when it is available. func (out *MapOutput) Apply(applier func(map[string]interface{}) (interface{}, error)) *Output { @@ -467,7 +494,9 @@ func (out *MapOutput) Apply(applier func(map[string]interface{}) (interface{}, e type StringOutput Output // Value returns the underlying number value. -func (out *StringOutput) Value() (string, error) { return (*Output)(out).String() } +func (out *StringOutput) Value() (string, bool, error) { + return (*Output)(out).String() +} // Apply applies a transformation to the number value when it is available. func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Output { @@ -480,7 +509,9 @@ func (out *StringOutput) Apply(applier func(string) (interface{}, error)) *Outpu type UintOutput Output // Value returns the underlying number value. -func (out *UintOutput) Value() (uint, error) { return (*Output)(out).Uint() } +func (out *UintOutput) Value() (uint, bool, error) { + return (*Output)(out).Uint() +} // Apply applies a transformation to the uint value when it is available. func (out *UintOutput) Apply(applier func(uint) (interface{}, error)) *Output { @@ -493,7 +524,9 @@ func (out *UintOutput) Apply(applier func(uint) (interface{}, error)) *Output { type Uint8Output Output // Value returns the underlying number value. -func (out *Uint8Output) Value() (uint8, error) { return (*Output)(out).Uint8() } +func (out *Uint8Output) Value() (uint8, bool, error) { + return (*Output)(out).Uint8() +} // Apply applies a transformation to the uint8 value when it is available. func (out *Uint8Output) Apply(applier func(uint8) (interface{}, error)) *Output { @@ -506,7 +539,9 @@ func (out *Uint8Output) Apply(applier func(uint8) (interface{}, error)) *Output type Uint16Output Output // Value returns the underlying number value. -func (out *Uint16Output) Value() (uint16, error) { return (*Output)(out).Uint16() } +func (out *Uint16Output) Value() (uint16, bool, error) { + return (*Output)(out).Uint16() +} // Apply applies a transformation to the uint16 value when it is available. func (out *Uint16Output) Apply(applier func(uint16) (interface{}, error)) *Output { @@ -519,7 +554,9 @@ func (out *Uint16Output) Apply(applier func(uint16) (interface{}, error)) *Outpu type Uint32Output Output // Value returns the underlying number value. -func (out *Uint32Output) Value() (uint32, error) { return (*Output)(out).Uint32() } +func (out *Uint32Output) Value() (uint32, bool, error) { + return (*Output)(out).Uint32() +} // Apply applies a transformation to the uint32 value when it is available. func (out *Uint32Output) Apply(applier func(uint32) (interface{}, error)) *Output { @@ -532,7 +569,9 @@ func (out *Uint32Output) Apply(applier func(uint32) (interface{}, error)) *Outpu type Uint64Output Output // Value returns the underlying number value. -func (out *Uint64Output) Value() (uint64, error) { return (*Output)(out).Uint64() } +func (out *Uint64Output) Value() (uint64, bool, error) { + return (*Output)(out).Uint64() +} // Apply applies a transformation to the uint64 value when it is available. func (out *Uint64Output) Apply(applier func(uint64) (interface{}, error)) *Output { diff --git a/sdk/go/pulumi/properties_test.go b/sdk/go/pulumi/properties_test.go index f0222c25e..a2af8b46f 100644 --- a/sdk/go/pulumi/properties_test.go +++ b/sdk/go/pulumi/properties_test.go @@ -26,10 +26,11 @@ func TestBasicOutputs(t *testing.T) { { out, resolve, _ := NewOutput(nil) go func() { - resolve(42) + resolve(42, true) }() - v, err := out.Value() + v, known, err := out.Value() assert.Nil(t, err) + assert.True(t, known) assert.NotNil(t, v) assert.Equal(t, 42, v.(int)) } @@ -38,7 +39,7 @@ func TestBasicOutputs(t *testing.T) { go func() { reject(errors.New("boom")) }() - v, err := out.Value() + v, _, err := out.Value() assert.NotNil(t, err) assert.Nil(t, v) } @@ -47,11 +48,12 @@ func TestBasicOutputs(t *testing.T) { func TestArrayOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve([]interface{}{nil, 0, "x"}) + resolve([]interface{}{nil, 0, "x"}, true) }() { - v, err := out.Array() + 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]) @@ -61,7 +63,7 @@ func TestArrayOutputs(t *testing.T) { } { arr := (*ArrayOutput)(out) - v, err := arr.Value() + v, _, err := arr.Value() assert.Nil(t, err) assert.NotNil(t, v) if assert.Equal(t, 3, len(v)) { @@ -75,17 +77,19 @@ func TestArrayOutputs(t *testing.T) { func TestBoolOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve(true) + resolve(true, true) }() { - v, err := out.Bool() + v, known, err := out.Bool() assert.Nil(t, err) + assert.True(t, known) assert.True(t, v) } { b := (*BoolOutput)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.True(t, v) } } @@ -97,11 +101,12 @@ func TestMapOutputs(t *testing.T) { "x": 1, "y": false, "z": "abc", - }) + }, true) }() { - v, err := out.Map() + 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"]) @@ -109,8 +114,9 @@ func TestMapOutputs(t *testing.T) { } { b := (*MapOutput)(out) - v, err := b.Value() + 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"]) @@ -121,17 +127,19 @@ func TestMapOutputs(t *testing.T) { func TestNumberOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve(42.345) + resolve(42.345, true) }() { - v, err := out.Float64() + v, known, err := out.Float64() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, 42.345, v) } { b := (*Float64Output)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, 42.345, v) } } @@ -139,17 +147,19 @@ func TestNumberOutputs(t *testing.T) { func TestStringOutputs(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { - resolve("a stringy output") + resolve("a stringy output", true) }() { - v, err := out.String() + v, known, err := out.String() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, "a stringy output", v) } { b := (*StringOutput)(out) - v, err := b.Value() + v, known, err := b.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, "a stringy output", v) } } @@ -160,11 +170,12 @@ func TestResolveOutputToOutput(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { other, resolveOther, _ := NewOutput(nil) - resolve(other) - go func() { resolveOther(99) }() + resolve(other, true) + go func() { resolveOther(99, true) }() }() - v, err := out.Value() + v, known, err := out.Value() assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, v, 99) } // Similarly, test that resolving an output to a rejected output yields an error. @@ -172,10 +183,10 @@ func TestResolveOutputToOutput(t *testing.T) { out, resolve, _ := NewOutput(nil) go func() { other, _, rejectOther := NewOutput(nil) - resolve(other) + resolve(other, true) go func() { rejectOther(errors.New("boom")) }() }() - v, err := out.Value() + v, _, err := out.Value() assert.NotNil(t, err) assert.Nil(t, v) } @@ -185,18 +196,34 @@ func TestOutputApply(t *testing.T) { // Test that resolved outputs lead to applies being run. { out, resolve, _ := NewOutput(nil) - go func() { resolve(42) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { ranApp = true return v + 1, nil }) - v, err := app.Value() + v, known, err := app.Value() 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. + { + out, resolve, _ := NewOutput(nil) + go func() { resolve(42, false) }() + var ranApp bool + b := (*IntOutput)(out) + app := b.Apply(func(v int) (interface{}, error) { + ranApp = true + return v + 1, nil + }) + _, known, err := app.Value() + 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) @@ -207,7 +234,7 @@ func TestOutputApply(t *testing.T) { ranApp = true return v + 1, nil }) - v, err := app.Value() + v, _, err := app.Value() assert.False(t, ranApp) assert.NotNil(t, err) assert.Nil(t, v) @@ -215,24 +242,25 @@ func TestOutputApply(t *testing.T) { // 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) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { other, resolveOther, _ := NewOutput(nil) - go func() { resolveOther(v + 1) }() + go func() { resolveOther(v+1, true) }() ranApp = true return other, nil }) - v, err := app.Value() + v, known, err := app.Value() assert.True(t, ranApp) assert.Nil(t, err) + assert.True(t, known) assert.Equal(t, v, 43) } // 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) }() + go func() { resolve(42, true) }() var ranApp bool b := (*IntOutput)(out) app := b.Apply(func(v int) (interface{}, error) { @@ -241,7 +269,7 @@ func TestOutputApply(t *testing.T) { ranApp = true return other, nil }) - v, err := app.Value() + v, _, err := app.Value() assert.True(t, ranApp) assert.NotNil(t, err) assert.Nil(t, v) diff --git a/sdk/go/pulumi/rpc.go b/sdk/go/pulumi/rpc.go index 4f9c65470..498bacaac 100644 --- a/sdk/go/pulumi/rpc.go +++ b/sdk/go/pulumi/rpc.go @@ -107,16 +107,22 @@ func marshalInput(v interface{}) (interface{}, []Resource, error) { }, nil, nil case *Output: // Await the value and return its raw value. - ov, err := t.Value() + ov, known, err := t.Value() if err != nil { return nil, nil, err } - // TODO: unknownValue - e, d, err := marshalInput(ov) - if err != nil { - return nil, nil, err + + if known { + // If the value is known, marshal it. + e, d, merr := marshalInput(ov) + if merr != nil { + return nil, nil, merr + } + return e, append(t.Deps(), d...), nil + } else { + // Otherwise, simply return the unknown value sentinel. + return rpcTokenUnknownValue, t.Deps(), nil } - return e, append(t.Deps(), d...), err case CustomResource: // Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.a e, d, err := marshalInput(t.ID()) @@ -196,6 +202,11 @@ func unmarshalOutputs(outs *structpb.Struct) (map[string]interface{}, error) { // unmarshalOutput unmarshals a single output variable into its runtime representation. For the most part, this just // returns the raw value. In a small number of cases, we need to change a type. func unmarshalOutput(v interface{}) (interface{}, error) { + // Check for nils and unknowns. + if v == nil || v == rpcTokenUnknownValue { + return nil, nil + } + // In the case of assets and archives, turn these into real asset and archive structures. if m, ok := v.(map[string]interface{}); ok { if m[rpcTokenSpecialSigKey] == rpcTokenSpecialAssetSig { diff --git a/sdk/go/pulumi/rpc_test.go b/sdk/go/pulumi/rpc_test.go index 3e3d487c4..11e83769d 100644 --- a/sdk/go/pulumi/rpc_test.go +++ b/sdk/go/pulumi/rpc_test.go @@ -26,7 +26,7 @@ import ( func TestMarshalRoundtrip(t *testing.T) { // Create interesting inputs. out, resolve, _ := NewOutput(nil) - resolve("outputty") + resolve("outputty", true) input := map[string]interface{}{ "s": "a string", "a": true,