Support remote components in Go (#5558)

This commit is contained in:
Justin Van Patten 2020-10-29 15:13:17 -07:00 committed by GitHub
parent 5cef84f036
commit 855f14c053
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1961 additions and 1677 deletions

View file

@ -2,7 +2,9 @@ CHANGELOG
=========
## HEAD (Unreleased)
_(none)_
- Add internal scaffolding for using cross-language components from Go.
[#5558](https://github.com/pulumi/pulumi/pull/5558)
## 2.12.1 (2020-10-23)

View file

@ -343,7 +343,7 @@ func (ctx *Context) ReadResource(
var state *structpb.Struct
var err error
defer func() {
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state)
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state, nil)
ctx.endRPC(err)
}()
@ -410,6 +410,12 @@ func (ctx *Context) ReadResource(
//
func (ctx *Context) RegisterResource(
t, name string, props Input, resource Resource, opts ...ResourceOption) error {
return ctx.registerResource(t, name, props, resource, false /*remote*/, opts...)
}
func (ctx *Context) registerResource(
t, name string, props Input, resource Resource, remote bool, opts ...ResourceOption) error {
if t == "" {
return errors.New("resource type argument cannot be empty")
} else if name == "" {
@ -469,9 +475,10 @@ func (ctx *Context) RegisterResource(
var urn, resID string
var inputs *resourceInputs
var state *structpb.Struct
deps := make(map[string][]Resource)
var err error
defer func() {
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state)
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state, deps)
ctx.endRPC(err)
}()
@ -500,6 +507,7 @@ func (ctx *Context) RegisterResource(
AcceptSecrets: true,
AdditionalSecretOutputs: inputs.additionalSecretOutputs,
Version: inputs.version,
Remote: remote,
})
if err != nil {
logging.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err)
@ -509,6 +517,13 @@ func (ctx *Context) RegisterResource(
if resp != nil {
urn, resID = resp.Urn, resp.Id
state = resp.Object
for key, propertyDependencies := range resp.GetPropertyDependencies() {
var resources []Resource
for _, urn := range propertyDependencies.GetUrns() {
resources = append(resources, &ResourceState{urn: URNInput(URN(urn)).ToURNOutput()})
}
deps[key] = resources
}
}
}()
@ -518,7 +533,13 @@ func (ctx *Context) RegisterResource(
func (ctx *Context) RegisterComponentResource(
t, name string, resource ComponentResource, opts ...ResourceOption) error {
return ctx.RegisterResource(t, name, nil, resource, opts...)
return ctx.RegisterResource(t, name, nil /*props*/, resource, opts...)
}
func (ctx *Context) RegisterRemoteComponentResource(
t, name string, props Input, resource ComponentResource, opts ...ResourceOption) error {
return ctx.registerResource(t, name, props, resource, true /*remote*/, opts...)
}
// resourceState contains the results of a resource registration operation.
@ -720,7 +741,7 @@ func makeResourceState(t, name string, resourceV Resource, providers map[string]
// resolve resolves the resource outputs using the given error and/or values.
func (state *resourceState) resolve(dryrun bool, err error, inputs *resourceInputs, urn, id string,
result *structpb.Struct) {
result *structpb.Struct, deps map[string][]Resource) {
var inprops resource.PropertyMap
if inputs != nil {
@ -784,7 +805,7 @@ func (state *resourceState) resolve(dryrun bool, err error, inputs *resourceInpu
if err != nil {
output.reject(err)
} else {
output.resolve(dest.Interface(), known, secret)
output.resolve(dest.Interface(), known, secret, deps[k])
}
}
}

View file

@ -15,17 +15,19 @@ func testPrintf(t *testing.T, ins ...interface{}) {
// Fprintf
buf := &bytes.Buffer{}
out := Output(Fprintf(buf, f, ins...))
_, known, secret, err := await(out)
_, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.Nil(t, err)
assert.Equal(t, expected, buf.String())
// Sprintf
out = Sprintf(f, ins...)
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.False(t, secret)
assert.True(t, known)
assert.Nil(t, deps)
assert.Nil(t, err)
assert.Equal(t, expected, v)
}

View file

@ -211,7 +211,7 @@ func marshalInputAndDetermineSecret(v interface{},
}
// Await the output.
ov, known, outputSecret, err := output.await(context.TODO())
ov, known, outputSecret, outputDeps, err := output.await(context.TODO())
if err != nil {
return resource.PropertyValue{}, nil, false, err
}
@ -219,10 +219,10 @@ func marshalInputAndDetermineSecret(v interface{},
// If the value is unknown, return the appropriate sentinel.
if !known {
return resource.MakeComputed(resource.NewStringProperty("")), output.dependencies(), secret, nil
return resource.MakeComputed(resource.NewStringProperty("")), outputDeps, secret, nil
}
v, deps = ov, output.dependencies()
v, deps = ov, outputDeps
}
}

View file

@ -70,7 +70,7 @@ func TestMarshalRoundtrip(t *testing.T) {
out, resolve, _ := NewOutput()
resolve("outputty")
out2 := newOutputState(reflect.TypeOf(""))
out2.fulfill(nil, false, false, nil)
out2.fulfill(nil, false, false, nil, nil)
inputs := testInputs{
S: String("a string"),
A: Bool(true),
@ -296,7 +296,7 @@ func TestResourceState(t *testing.T) {
resolved,
plugin.MarshalOptions{KeepUnknowns: true})
assert.NoError(t, err)
state.resolve(false, nil, nil, "foo", "bar", s)
state.resolve(false, nil, nil, "foo", "bar", s, nil)
input := &testResourceInputs{
URN: theResource.URN(),
@ -419,7 +419,7 @@ func TestMarshalRoundtripNestedSecret(t *testing.T) {
out, resolve, _ := NewOutput()
resolve("outputty")
out2 := newOutputState(reflect.TypeOf(""))
out2.fulfill(nil, false, true, nil)
out2.fulfill(nil, false, true, nil, nil)
inputs := testInputs{
S: String("a string"),
A: Bool(true),
@ -506,7 +506,7 @@ func (UntypedArgs) ElementType() reflect.Type {
func TestMapInputMarhsalling(t *testing.T) {
var theResource simpleResource
out := newOutput(reflect.TypeOf((*StringOutput)(nil)).Elem(), &theResource)
out.resolve("outputty", true, false)
out.resolve("outputty", true, false, nil)
inputs1 := Map(map[string]Input{
"prop": out,

View file

@ -101,22 +101,25 @@ func TestRegisterResource(t *testing.T) {
}, &res)
assert.NoError(t, err)
id, known, secret, err := await(res.ID())
id, known, secret, deps, err := await(res.ID())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.Equal(t, ID("someID"), id)
urn, known, secret, err := await(res.URN())
urn, known, secret, deps, err := await(res.URN())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.NotEqual(t, "", urn)
foo, known, secret, err := await(res.Foo)
foo, known, secret, deps, err := await(res.Foo)
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.Equal(t, "qux", foo)
// Test map marshaling.
@ -129,22 +132,25 @@ func TestRegisterResource(t *testing.T) {
}, &res2)
assert.NoError(t, err)
id, known, secret, err = await(res2.ID())
id, known, secret, deps, err = await(res2.ID())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res2}, deps)
assert.Equal(t, ID("someID"), id)
urn, known, secret, err = await(res2.URN())
urn, known, secret, deps, err = await(res2.URN())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res2}, deps)
assert.NotEqual(t, "", urn)
outputs, known, secret, err := await(res2.Outputs)
outputs, known, secret, deps, err := await(res2.Outputs)
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res2}, deps)
assert.Equal(t, map[string]interface{}{"foo": "qux"}, outputs)
return nil
@ -176,22 +182,25 @@ func TestReadResource(t *testing.T) {
}, &res)
assert.NoError(t, err)
id, known, secret, err := await(res.ID())
id, known, secret, deps, err := await(res.ID())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.Equal(t, ID("someID"), id)
urn, known, secret, err := await(res.URN())
urn, known, secret, deps, err := await(res.URN())
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.NotEqual(t, "", urn)
foo, known, secret, err := await(res.Foo)
foo, known, secret, deps, err := await(res.Foo)
assert.NoError(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Equal(t, []Resource{&res}, deps)
assert.Equal(t, "qux", foo)
return nil

View file

@ -36,21 +36,21 @@ func TestStackReference(t *testing.T) {
resName = "stack"
ref0, err := NewStackReference(ctx, resName, nil)
assert.NoError(t, err)
_, _, _, err = await(ref0.ID())
_, _, _, _, err = await(ref0.ID())
assert.NoError(t, err)
resName = "stack2"
ref1, err := NewStackReference(ctx, resName, &StackReferenceArgs{Name: String("stack")})
assert.NoError(t, err)
outs0, _, _, err := await(ref0.Outputs)
outs0, _, _, _, err := await(ref0.Outputs)
assert.NoError(t, err)
assert.Equal(t, outputs, outs0)
zed0, _, _, err := await(ref0.GetOutput(String("zed")))
zed0, _, _, _, err := await(ref0.GetOutput(String("zed")))
assert.NoError(t, err)
assert.Equal(t, outputs["zed"], zed0)
outs1, _, _, err := await(ref1.Outputs)
outs1, _, _, _, err := await(ref1.Outputs)
assert.NoError(t, err)
assert.Equal(t, outputs, outs1)
zed1, _, _, err := await(ref1.GetOutput(String("zed")))
zed1, _, _, _, err := await(ref1.GetOutput(String("zed")))
assert.NoError(t, err)
assert.Equal(t, outputs["zed"], zed1)
return nil

View file

@ -30,13 +30,13 @@ func TestOutputApply(t *testing.T) {
// Test that resolved outputs lead to applies being run.
{
out := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
v, known, _, err := await(app)
v, known, _, _, err := await(app)
assert.True(t, ranApp)
assert.Nil(t, err)
assert.True(t, known)
@ -45,13 +45,13 @@ func TestOutputApply(t *testing.T) {
// Test that resolved, but unknown outputs, skip the running of applies.
{
out := newIntOutput()
go func() { out.resolve(42, false, false) }()
go func() { out.resolve(42, false, false, nil) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
_, known, _, err := await(app)
_, known, _, _, err := await(app)
assert.False(t, ranApp)
assert.Nil(t, err)
assert.False(t, known)
@ -65,7 +65,7 @@ func TestOutputApply(t *testing.T) {
ranApp = true
return v + 1, nil
})
v, _, _, err := await(app)
v, _, _, _, err := await(app)
assert.False(t, ranApp)
assert.NotNil(t, err)
assert.Nil(t, v)
@ -73,7 +73,7 @@ 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 := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
other, resolveOther, _ := NewOutput()
@ -81,7 +81,7 @@ func TestOutputApply(t *testing.T) {
ranApp = true
return other, nil
})
v, known, _, err := await(app)
v, known, _, _, err := await(app)
assert.True(t, ranApp)
assert.Nil(t, err)
assert.True(t, known)
@ -93,7 +93,7 @@ func TestOutputApply(t *testing.T) {
ranApp = true
return other, nil
})
v, known, _, err = await(app)
v, known, _, _, err = await(app)
assert.True(t, ranApp)
assert.Nil(t, err)
assert.True(t, known)
@ -102,7 +102,7 @@ func TestOutputApply(t *testing.T) {
// Test that an an apply that reject an output returns the rejection of that output, not the output itself.
{
out := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
other, _, rejectOther := NewOutput()
@ -110,7 +110,7 @@ func TestOutputApply(t *testing.T) {
ranApp = true
return other, nil
})
v, _, _, err := await(app)
v, _, _, _, err := await(app)
assert.True(t, ranApp)
assert.NotNil(t, err)
assert.Nil(t, v)
@ -121,7 +121,7 @@ func TestOutputApply(t *testing.T) {
ranApp = true
return other, nil
})
v, _, _, err = await(app)
v, _, _, _, err = await(app)
assert.True(t, ranApp)
assert.NotNil(t, err)
assert.Nil(t, v)
@ -129,17 +129,17 @@ func TestOutputApply(t *testing.T) {
// Test builtin applies.
{
out := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
{{range .Builtins}}
t.Run("Apply{{.Name}}", func(t *testing.T) {
o2 := out.Apply{{.Name}}(func(v int) {{.ElementType}} { return *new({{.ElementType}}) })
_, known, _, err := await(o2)
_, known, _, _, err := await(o2)
assert.True(t, known)
assert.NoError(t, err)
o2 = out.Apply{{.Name}}WithContext(context.Background(), func(_ context.Context, v int) {{.ElementType}} { return *new({{.ElementType}}) })
_, known, _, err = await(o2)
_, known, _, _, err = await(o2)
assert.True(t, known)
assert.NoError(t, err)
})
@ -148,7 +148,7 @@ func TestOutputApply(t *testing.T) {
// Test that applies return appropriate concrete implementations of Output based on the callback type
{
out := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
{{range .Builtins}}
t.Run("ApplyT::{{.Name}}Output", func(t *testing.T) {
@ -165,10 +165,10 @@ func TestOutputApply(t *testing.T) {
}
out := newIntOutput()
go func() { out.resolve(42, true, false) }()
go func() { out.resolve(42, true, false, nil) }()
out2 := StringOutput{newOutputState(reflect.TypeOf(""))}
go func() { out2.resolve("hello", true, false) }()
go func() { out2.resolve("hello", true, false, nil) }()
res := out.
ApplyT(func(v int) myStructType {
@ -230,7 +230,7 @@ func TestOutputApply(t *testing.T) {
_, ok := res.(StringArrayOutput)
assert.True(t, ok)
v, known, _, err := await(res)
v, known, _, _, err := await(res)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, []string{"qux", "zed"}, v)
@ -238,7 +238,7 @@ func TestOutputApply(t *testing.T) {
_, ok = res2.(StringArrayOutput)
assert.True(t, ok)
v, known, _, err = await(res2)
v, known, _, _, err = await(res2)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, []string{"foo", "bar"}, v)
@ -246,7 +246,7 @@ func TestOutputApply(t *testing.T) {
_, ok = res3.(StringOutput)
assert.True(t, ok)
v, known, _, err = await(res3)
v, known, _, _, err = await(res3)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, "foo,bar,qux,zed", v)
@ -254,12 +254,12 @@ func TestOutputApply(t *testing.T) {
_, ok = res4.(AnyOutput)
assert.True(t, ok)
v, known, _, err = await(res4)
v, known, _, _, err = await(res4)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, &myStructType{foo: 42, bar: "hello"}, v)
v, known, _, err = await(res5)
v, known, _, _, err = await(res5)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, "foo,bar,qux,zed;42;hello", v)
@ -273,7 +273,7 @@ func TestToOutput{{.Name}}(t *testing.T) {
_, ok := out.({{.Name}}Input)
assert.True(t, ok)
_, known, _, err := await(out)
_, known, _, _, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
@ -281,7 +281,7 @@ func TestToOutput{{.Name}}(t *testing.T) {
_, ok = out.({{.Name}}Input)
assert.True(t, ok)
_, known, _, err = await(out)
_, known, _, _, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
}
@ -294,25 +294,25 @@ func TestTo{{.Name}}Output(t *testing.T) {
out := in.To{{.Name}}Output()
_, known, _, err := await(out)
_, known, _, _, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
out = out.To{{.Name}}Output()
_, known, _, err = await(out)
_, known, _, _, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
out = in.To{{.Name}}OutputWithContext(context.Background())
_, known, _, err = await(out)
_, known, _, _, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
out = out.To{{.Name}}OutputWithContext(context.Background())
_, known, _, err = await(out)
_, known, _, _, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
}
@ -322,56 +322,56 @@ func TestTo{{.Name}}Output(t *testing.T) {
func TestBuiltinConversions(t *testing.T) {
archiveIn := NewFileArchive("foo.zip")
assetOrArchiveOut := archiveIn.ToAssetOrArchiveOutput()
archiveV, known, _, err := await(assetOrArchiveOut)
archiveV, known, _, _, err := await(assetOrArchiveOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, archiveIn, archiveV)
archiveOut := archiveIn.ToArchiveOutput()
assetOrArchiveOut = archiveOut.ToAssetOrArchiveOutput()
archiveV, known, _, err = await(assetOrArchiveOut)
archiveV, known, _, _, err = await(assetOrArchiveOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, archiveIn, archiveV)
assetIn := NewFileAsset("foo.zip")
assetOrArchiveOut = assetIn.ToAssetOrArchiveOutput()
assetV, known, _, err := await(assetOrArchiveOut)
assetV, known, _, _, err := await(assetOrArchiveOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, assetIn, assetV)
assetOut := assetIn.ToAssetOutput()
assetOrArchiveOut = assetOut.ToAssetOrArchiveOutput()
assetV, known, _, err = await(assetOrArchiveOut)
assetV, known, _, _, err = await(assetOrArchiveOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, assetIn, assetV)
idIn := ID("foo")
stringOut := idIn.ToStringOutput()
stringV, known, _, err := await(stringOut)
stringV, known, _, _, err := await(stringOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, string(idIn), stringV)
idOut := idIn.ToIDOutput()
stringOut = idOut.ToStringOutput()
stringV, known, _, err = await(stringOut)
stringV, known, _, _, err = await(stringOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, string(idIn), stringV)
urnIn := URN("foo")
stringOut = urnIn.ToStringOutput()
stringV, known, _, err = await(stringOut)
stringV, known, _, _, err = await(stringOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, string(urnIn), stringV)
urnOut := urnIn.ToURNOutput()
stringOut = urnOut.ToStringOutput()
stringV, known, _, err = await(stringOut)
stringV, known, _, _, err = await(stringOut)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, string(urnIn), stringV)
@ -383,11 +383,11 @@ func TestBuiltinConversions(t *testing.T) {
func Test{{.Name}}Elem(t *testing.T) {
out := ({{.Example}}).To{{.Name}}Output()
av, known, _, err := await(out)
av, known, _, _, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
iv, known, _, err := await(out.Elem())
iv, known, _, _, err := await(out.Elem())
assert.True(t, known)
assert.NoError(t, err)
@ -403,11 +403,11 @@ func Test{{.Name}}Elem(t *testing.T) {
func Test{{.Name}}Index(t *testing.T) {
out := ({{.Example}}).To{{.Name}}Output()
av, known, _, err := await(out)
av, known, _, _, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
iv, known, _, err := await(out.Index(Int(0)))
iv, known, _, _, err := await(out.Index(Int(0)))
assert.True(t, known)
assert.NoError(t, err)
@ -422,11 +422,11 @@ func Test{{.Name}}Index(t *testing.T) {
func Test{{.Name}}Index(t *testing.T) {
out := ({{.Example}}).To{{.Name}}Output()
av, known, _, err := await(out)
av, known, _, _, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
iv, known, _, err := await(out.MapIndex(String("baz")))
iv, known, _, _, err := await(out.MapIndex(String("baz")))
assert.True(t, known)
assert.NoError(t, err)

View file

@ -39,12 +39,12 @@ type Output interface {
getState() *OutputState
dependencies() []Resource
fulfillValue(value reflect.Value, known, secret bool, err error)
resolveValue(value reflect.Value, known, secret bool)
fulfill(value interface{}, known, secret bool, err error)
resolve(value interface{}, known, secret bool)
fulfillValue(value reflect.Value, known, secret bool, deps []Resource, err error)
resolveValue(value reflect.Value, known, secret bool, deps []Resource)
fulfill(value interface{}, known, secret bool, deps []Resource, err error)
resolve(value interface{}, known, secret bool, deps []Resource)
reject(err error)
await(ctx context.Context) (interface{}, bool, bool, error)
await(ctx context.Context) (interface{}, bool, bool, []Resource, error)
isSecret() bool
}
@ -99,11 +99,11 @@ func (o *OutputState) dependencies() []Resource {
return o.deps
}
func (o *OutputState) fulfill(value interface{}, known, secret bool, err error) {
o.fulfillValue(reflect.ValueOf(value), known, secret, err)
func (o *OutputState) fulfill(value interface{}, known, secret bool, deps []Resource, err error) {
o.fulfillValue(reflect.ValueOf(value), known, secret, deps, err)
}
func (o *OutputState) fulfillValue(value reflect.Value, known, secret bool, err error) {
func (o *OutputState) fulfillValue(value reflect.Value, known, secret bool, deps []Resource, err error) {
if o == nil {
return
}
@ -125,39 +125,57 @@ func (o *OutputState) fulfillValue(value reflect.Value, known, secret bool, err
reflect.ValueOf(&o.value).Elem().Set(value)
}
o.state, o.known, o.secret = outputResolved, known, secret
// If needed, merge the up-front provided dependencies with fulfilled dependencies, pruning duplicates.
if len(deps) == 0 {
// We didn't get any new dependencies, so no need to merge.
return
}
depSet := make(map[Resource]struct{})
for _, d := range o.deps {
depSet[d] = struct{}{}
}
for _, d := range deps {
depSet[d] = struct{}{}
}
mergedDeps := make([]Resource, 0, len(depSet))
for d := range depSet {
mergedDeps = append(mergedDeps, d)
}
o.deps = mergedDeps
}
}
func (o *OutputState) resolve(value interface{}, known, secret bool) {
o.fulfill(value, known, secret, nil)
func (o *OutputState) resolve(value interface{}, known, secret bool, deps []Resource) {
o.fulfill(value, known, secret, deps, nil)
}
func (o *OutputState) resolveValue(value reflect.Value, known, secret bool) {
o.fulfillValue(value, known, secret, nil)
func (o *OutputState) resolveValue(value reflect.Value, known, secret bool, deps []Resource) {
o.fulfillValue(value, known, secret, deps, nil)
}
func (o *OutputState) reject(err error) {
o.fulfill(nil, true, false, err)
o.fulfill(nil, true, false, nil, err)
}
func (o *OutputState) await(ctx context.Context) (interface{}, bool, bool, error) {
func (o *OutputState) await(ctx context.Context) (interface{}, bool, bool, []Resource, error) {
for {
if o == nil {
// If the state is nil, treat its value as resolved and unknown.
return nil, false, false, nil
return nil, false, false, nil, nil
}
o.mutex.Lock()
for o.state == outputPending {
if ctx.Err() != nil {
return nil, true, false, ctx.Err()
return nil, true, false, nil, ctx.Err()
}
o.cond.Wait()
}
o.mutex.Unlock()
if !o.known || o.err != nil {
return nil, o.known, o.secret, o.err
return nil, o.known, o.secret, o.deps, o.err
}
// If the result is an Output, await it in turn.
@ -166,7 +184,7 @@ func (o *OutputState) await(ctx context.Context) (interface{}, bool, bool, error
// the element type of the outer output. We should reconsider this.
ov, ok := o.value.(Output)
if !ok {
return o.value, true, o.secret, nil
return o.value, true, o.secret, o.deps, nil
}
o = ov.getState()
}
@ -223,7 +241,7 @@ func NewOutput() (Output, func(interface{}), func(error)) {
out := newOutputState(anyType)
resolve := func(v interface{}) {
out.resolve(v, true, false)
out.resolve(v, true, false, nil)
}
reject := func(err error) {
out.reject(err)
@ -378,9 +396,9 @@ func (o *OutputState) ApplyTWithContext(ctx context.Context, applier interface{}
result := newOutput(resultType, o.dependencies()...)
go func() {
v, known, secret, err := o.await(ctx)
v, known, secret, deps, err := o.await(ctx)
if err != nil || !known {
result.fulfill(nil, known, secret, err)
result.fulfill(nil, known, secret, deps, err)
return
}
@ -396,7 +414,7 @@ func (o *OutputState) ApplyTWithContext(ctx context.Context, applier interface{}
}
// Fulfill the result.
result.fulfillValue(results[0], true, secret, nil)
result.fulfillValue(results[0], true, secret, deps, nil)
}()
return result
}
@ -530,11 +548,11 @@ func callToOutputMethod(ctx context.Context, input reflect.Value, resolvedType r
return toOutputMethod.Call([]reflect.Value{reflect.ValueOf(ctx)})[0].Interface().(Output), true
}
func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, error) {
func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, []Resource, error) {
contract.Assert(v.IsValid())
if !resolved.CanSet() {
return true, false, nil
return true, false, nil, nil
}
// If the value is an Input with of a different element type, turn it into an Output of the appropriate type and
@ -544,7 +562,7 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
input, isNonNil := v.Interface().(Input)
if !isNonNil {
// A nil input is already fully-resolved.
return true, false, nil
return true, false, nil, nil
}
valueType = input.ElementType()
@ -570,9 +588,9 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
// If the input is an Output, await its value. The returned value is fully resolved.
if output, ok := input.(Output); ok {
e, known, secret, err := output.await(ctx)
e, known, secret, deps, err := output.await(ctx)
if err != nil || !known {
return known, secret, err
return known, secret, deps, err
}
if !assignInput {
val := reflect.ValueOf(e)
@ -583,13 +601,13 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
} else {
resolved.Set(reflect.ValueOf(input))
}
return true, secret, nil
return true, secret, deps, nil
}
// Check for types that are already fully-resolved.
if v, ok := getResolvedValue(input); ok {
resolved.Set(v)
return true, false, nil
return true, false, nil, nil
}
v, isInput = reflect.ValueOf(input), true
@ -626,7 +644,7 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
resolved = reflect.New(valueType).Elem()
}
known, secret, err := true, false, error(nil)
known, secret, deps, err := true, false, make([]Resource, 0), error(nil)
switch v.Kind() {
case reflect.Interface:
if !v.IsNil() {
@ -643,9 +661,10 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
numFields := typ.NumField()
for i := 0; i < numFields; i++ {
_, field := getMappedField(resolved, i)
fknown, fsecret, ferr := awaitInputs(ctx, v.Field(i), field)
fknown, fsecret, fdeps, ferr := awaitInputs(ctx, v.Field(i), field)
known = known && fknown
secret = secret || fsecret
deps = append(deps, fdeps...)
if err == nil {
err = ferr
}
@ -653,9 +672,10 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
case reflect.Array:
l := v.Len()
for i := 0; i < l; i++ {
eknown, esecret, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
eknown, esecret, edeps, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
known = known && eknown
secret = secret || esecret
deps = append(deps, edeps...)
if err == nil {
err = eerr
}
@ -664,9 +684,10 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
l := v.Len()
resolved.Set(reflect.MakeSlice(resolved.Type(), l, l))
for i := 0; i < l; i++ {
eknown, esecret, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
eknown, esecret, edeps, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
known = known && eknown
secret = secret || esecret
deps = append(deps, edeps...)
if err == nil {
err = eerr
}
@ -677,13 +698,13 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
iter := v.MapRange()
for iter.Next() {
kv := reflect.New(resolvedKeyType).Elem()
kknown, ksecret, kerr := awaitInputs(ctx, iter.Key(), kv)
kknown, ksecret, kdeps, kerr := awaitInputs(ctx, iter.Key(), kv)
if err == nil {
err = kerr
}
vv := reflect.New(resolvedValueType).Elem()
vknown, vsecret, verr := awaitInputs(ctx, iter.Value(), vv)
vknown, vsecret, vdeps, verr := awaitInputs(ctx, iter.Value(), vv)
if err == nil {
err = verr
}
@ -694,6 +715,7 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
known = known && kknown && vknown
secret = secret || ksecret || vsecret
deps = append(append(deps, kdeps...), vdeps...)
}
default:
if isInput {
@ -701,7 +723,7 @@ func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, bool, er
}
resolved.Set(v)
}
return known, secret, err
return known, secret, deps, err
}
// ToOutput returns an Output that will resolve when all Inputs contained in the given value have resolved.
@ -729,20 +751,20 @@ func toOutputWithContext(ctx context.Context, v interface{}, forceSecret bool) O
result := newOutput(resultType, gatherDependencies(v)...)
go func() {
if v == nil {
result.fulfill(nil, true, false, nil)
result.fulfill(nil, true, false, nil, nil)
return
}
element := reflect.New(resolvedType).Elem()
known, secret, err := awaitInputs(ctx, reflect.ValueOf(v), element)
known, secret, deps, err := awaitInputs(ctx, reflect.ValueOf(v), element)
secret = secret || forceSecret
if err != nil || !known {
result.fulfill(nil, known, secret, err)
result.fulfill(nil, known, secret, deps, err)
return
}
result.resolveValue(element, true, secret)
result.resolveValue(element, true, secret, deps)
}()
return result
}
@ -822,12 +844,12 @@ func AnyWithContext(ctx context.Context, v interface{}) AnyOutput {
out := newOutput(anyOutputType, gatherDependencies(v)...)
go func() {
if v == nil {
out.fulfill(nil, true, false, nil)
out.fulfill(nil, true, false, nil, nil)
return
}
var result interface{}
known, secret, err := awaitInputs(ctx, reflect.ValueOf(v), reflect.ValueOf(&result).Elem())
out.fulfill(result, known, secret, err)
known, secret, deps, err := awaitInputs(ctx, reflect.ValueOf(v), reflect.ValueOf(&result).Elem())
out.fulfill(result, known, secret, deps, err)
}()
return out.(AnyOutput)
}
@ -855,7 +877,7 @@ func (o IDOutput) ToStringPtrOutputWithContext(ctx context.Context) StringPtrOut
}
func (o IDOutput) awaitID(ctx context.Context) (ID, bool, bool, error) {
id, known, secret, err := o.await(ctx)
id, known, secret, _, err := o.await(ctx)
if !known || err != nil {
return "", known, false, err
}
@ -879,7 +901,7 @@ func (o URNOutput) ToStringPtrOutputWithContext(ctx context.Context) StringPtrOu
}
func (o URNOutput) awaitURN(ctx context.Context) (URN, bool, bool, error) {
id, known, secret, err := o.await(ctx)
id, known, secret, _, err := o.await(ctx)
if !known || err != nil {
return "", known, secret, err
}

File diff suppressed because it is too large Load diff

View file

@ -25,12 +25,12 @@ import (
"github.com/stretchr/testify/assert"
)
func await(out Output) (interface{}, bool, bool, error) {
func await(out Output) (interface{}, bool, bool, []Resource, error) {
return out.await(context.Background())
}
func assertApplied(t *testing.T, out Output) {
_, known, _, err := await(out)
_, known, _, _, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
}
@ -46,10 +46,11 @@ func TestBasicOutputs(t *testing.T) {
go func() {
resolve(42)
}()
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.Nil(t, err)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NotNil(t, v)
assert.Equal(t, 42, v.(int))
}
@ -58,7 +59,7 @@ func TestBasicOutputs(t *testing.T) {
go func() {
reject(errors.New("boom"))
}()
v, _, _, err := await(out)
v, _, _, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
@ -67,7 +68,7 @@ func TestBasicOutputs(t *testing.T) {
func TestArrayOutputs(t *testing.T) {
out := ArrayOutput{newOutputState(reflect.TypeOf([]interface{}{}))}
go func() {
out.resolve([]interface{}{nil, 0, "x"}, true, false)
out.resolve([]interface{}{nil, 0, "x"}, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(arr []interface{}) (interface{}, error) {
@ -85,7 +86,7 @@ func TestArrayOutputs(t *testing.T) {
func TestBoolOutputs(t *testing.T) {
out := BoolOutput{newOutputState(reflect.TypeOf(false))}
go func() {
out.resolve(true, true, false)
out.resolve(true, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v bool) (interface{}, error) {
@ -102,7 +103,7 @@ func TestMapOutputs(t *testing.T) {
"x": 1,
"y": false,
"z": "abc",
}, true, false)
}, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v map[string]interface{}) (interface{}, error) {
@ -118,7 +119,7 @@ func TestMapOutputs(t *testing.T) {
func TestNumberOutputs(t *testing.T) {
out := Float64Output{newOutputState(reflect.TypeOf(float64(0)))}
go func() {
out.resolve(42.345, true, false)
out.resolve(42.345, true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v float64) (interface{}, error) {
@ -131,7 +132,7 @@ func TestNumberOutputs(t *testing.T) {
func TestStringOutputs(t *testing.T) {
out := StringOutput{newOutputState(reflect.TypeOf(""))}
go func() {
out.resolve("a stringy output", true, false)
out.resolve("a stringy output", true, false, nil)
}()
{
assertApplied(t, out.ApplyT(func(v string) (interface{}, error) {
@ -163,7 +164,7 @@ func TestResolveOutputToOutput(t *testing.T) {
resolve(other)
go func() { rejectOther(errors.New("boom")) }()
}()
v, _, _, err := await(out)
v, _, _, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
@ -175,9 +176,10 @@ func TestToOutputStruct(t *testing.T) {
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
@ -185,9 +187,10 @@ func TestToOutputStruct(t *testing.T) {
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, err = await(out)
v, known, secret, deps, err = await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
@ -196,9 +199,10 @@ func TestToOutputStruct(t *testing.T) {
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, err = await(out)
v, known, secret, deps, err = await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
}
@ -236,9 +240,10 @@ func TestToOutputConvert(t *testing.T) {
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 1}, v)
}
@ -259,9 +264,10 @@ func TestToOutputAny(t *testing.T) {
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
argsV := v.(*args)
@ -284,6 +290,73 @@ func TestToOutputAny(t *testing.T) {
assert.Equal(t, true, bo.value)
}
func TestToOutputAnyDeps(t *testing.T) {
type args struct {
S StringInput
I IntInput
A Input
R Resource
}
stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
stringOut := StringOutput{newOutputState(reflect.TypeOf(""), stringDep1)}
go func() {
stringOut.resolve("a stringy output", true, false, []Resource{stringDep2})
}()
intDep1, intDep2 := &ResourceState{}, &ResourceState{}
intOut := IntOutput{newOutputState(reflect.TypeOf(0), intDep1)}
go func() {
intOut.resolve(42, true, false, []Resource{intDep2})
}()
boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
boolOut := BoolOutput{newOutputState(reflect.TypeOf(true), boolDep1)}
go func() {
boolOut.resolve(true, true, false, []Resource{boolDep2})
}()
res := &ResourceState{}
out := ToOutput(&args{
S: stringOut,
I: intOut,
A: Map{"world": boolOut},
R: res,
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, intDep1, intDep2, boolDep1, boolDep2, res}, deps)
assert.NoError(t, err)
argsV := v.(*args)
so, ok := argsV.S.(StringOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), so.state)
assert.Equal(t, "a stringy output", so.value)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2}, so.deps)
io, ok := argsV.I.(IntOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), io.state)
assert.Equal(t, 42, io.value)
assert.ElementsMatch(t, []Resource{intDep1, intDep2}, io.deps)
ai, ok := argsV.A.(Map)
assert.True(t, ok)
bo, ok := ai["world"].(BoolOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), bo.getState().state)
assert.Equal(t, true, bo.value)
assert.ElementsMatch(t, []Resource{boolDep1, boolDep2}, bo.deps)
}
type args struct {
S string
I int
@ -310,9 +383,10 @@ func TestToOutputInputAny(t *testing.T) {
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, secret, err := await(out)
v, known, secret, deps, err := await(out)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, &args{
@ -407,41 +481,76 @@ func TestSecretApply(t *testing.T) {
func TestNil(t *testing.T) {
ao := Any(nil)
v, known, secret, err := await(ao)
v, known, secret, deps, err := await(ao)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
o := ToOutput(nil)
v, known, secret, err = await(o)
v, known, secret, deps, err = await(o)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
o = ToOutput(ao)
v, known, secret, err = await(o)
v, known, secret, deps, err = await(o)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
ao = ToOutput("").ApplyT(func(v string) interface{} {
return nil
}).(AnyOutput)
v, known, secret, err = await(ao)
v, known, secret, deps, err = await(ao)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, nil, v)
bo := ao.ApplyBool(func(x interface{}) bool {
return x == nil
})
v, known, secret, err = await(bo)
v, known, secret, deps, err = await(bo)
assert.True(t, known)
assert.False(t, secret)
assert.Nil(t, deps)
assert.NoError(t, err)
assert.Equal(t, true, v)
}
// Test that dependencies flow through all/apply.
func TestDeps(t *testing.T) {
stringDep1, stringDep2 := &ResourceState{}, &ResourceState{}
stringOut := StringOutput{newOutputState(reflect.TypeOf(""), stringDep1)}
assert.ElementsMatch(t, []Resource{stringDep1}, stringOut.deps)
go func() {
stringOut.resolve("hello", true, false, []Resource{stringDep2})
}()
boolDep1, boolDep2 := &ResourceState{}, &ResourceState{}
boolOut := BoolOutput{newOutputState(reflect.TypeOf(true), boolDep1)}
assert.ElementsMatch(t, []Resource{boolDep1}, boolOut.deps)
go func() {
boolOut.resolve(true, true, false, []Resource{boolDep2})
}()
a := All(stringOut, boolOut).ApplyT(func(args []interface{}) (string, error) {
s := args[0].(string)
b := args[1].(bool)
return fmt.Sprintf("%s: %v", s, b), nil
})
v, known, secret, deps, err := await(a)
assert.Equal(t, "hello: true", v)
assert.True(t, known)
assert.False(t, secret)
assert.ElementsMatch(t, []Resource{stringDep1, stringDep2, boolDep1, boolDep2}, deps)
assert.NoError(t, err)
}

View file

@ -0,0 +1,3 @@
name: construct_component_go
description: A program that constructs remote component resources.
runtime: go

View file

@ -0,0 +1,58 @@
// Copyright 2016-2020, Pulumi Corporation. All rights reserved.
package main
import (
"reflect"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)
type componentArgs struct {
Echo interface{} `pulumi:"echo"`
}
type ComponentArgs struct {
Echo pulumi.Input
}
func (ComponentArgs) ElementType() reflect.Type {
return reflect.TypeOf((*componentArgs)(nil)).Elem()
}
type Component struct {
pulumi.ResourceState
Echo pulumi.AnyOutput `pulumi:"echo"`
ChildID pulumi.StringOutput `pulumi:"childId"`
}
func NewComponent(
ctx *pulumi.Context, name string, args *ComponentArgs, opts ...pulumi.ResourceOption) (*Component, error) {
var resource Component
err := ctx.RegisterRemoteComponentResource("testcomponent:index:Component", name, args, &resource, opts...)
if err != nil {
return nil, err
}
return &resource, nil
}
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
componentA, err := NewComponent(ctx, "a", &ComponentArgs{Echo: pulumi.Int(42)})
if err != nil {
return err
}
_, err = NewComponent(ctx, "b", &ComponentArgs{Echo: componentA.Echo})
if err != nil {
return err
}
_, err = NewComponent(ctx, "C", &ComponentArgs{Echo: componentA.ChildID})
if err != nil {
return err
}
return nil
})
}

View file

@ -10,6 +10,8 @@ import (
"testing"
"github.com/pulumi/pulumi/pkg/v2/testing/integration"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/stretchr/testify/assert"
)
// TestEmptyGo simply tests that we can build and run an empty Go project.
@ -116,3 +118,59 @@ func TestLargeResourceGo(t *testing.T) {
Dir: filepath.Join("large_resource", "go"),
})
}
// Test remote component construction in Go.
func TestConstructGo(t *testing.T) {
pathEnv, err := testComponentPathEnv()
if err != nil {
t.Fatalf("failed to build test component PATH: %v", err)
}
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
const testYarnLinkPulumiEnv = "PULUMI_TEST_YARN_LINK_PULUMI=true"
var opts *integration.ProgramTestOptions
opts = &integration.ProgramTestOptions{
Env: []string{pathEnv, testYarnLinkPulumiEnv},
Dir: filepath.Join("construct_component", "go"),
Dependencies: []string{
"github.com/pulumi/pulumi/sdk/v2",
},
Quick: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.Type)
assert.Equal(t, "", string(stackRes.Parent))
// Check that dependencies flow correctly between the originating program and the remote component
// plugin.
urns := make(map[string]resource.URN)
for _, res := range stackInfo.Deployment.Resources[1:] {
assert.NotNil(t, res)
urns[string(res.URN.Name())] = res.URN
switch res.URN.Name() {
case "child-a":
for _, deps := range res.PropertyDependencies {
assert.Empty(t, deps)
}
case "child-b":
assert.Equal(t, []resource.URN{urns["a"]}, res.PropertyDependencies["echo"])
case "child-c":
assert.ElementsMatch(t, []resource.URN{urns["child-a"], urns["a"]},
res.PropertyDependencies["echo"])
}
}
}
},
}
integration.ProgramTest(t, opts)
}