Redesign the Go SDK resource/input/output system. (#3506)

The redesign is focused around providing better static typings and
improved ease-of-use for the Go SDK. Most of the redesign revolves
around three pivots:
- Strongly-typed inputs, especially for nested types
- Struct-based resource and invoke APIs
- Ease-of-use of Apply

1. Strongly-typed inputs

Input is the type of a generic input value for a Pulumi resource.
This type is used in conjunction with Output to provide polymorphism
over strongly-typed input values.

The intended pattern for nested Pulumi value types is to define an
input interface and a plain, input, and output variant of the value
type that implement the input interface.

For example, given a nested Pulumi value type with the following shape:

```
type Nested struct {
    Foo int
    Bar string
}
```

We would define the following:

```
var nestedType = reflect.TypeOf((*Nested)(nil)).Elem()

type NestedInput interface {
    pulumi.Input

    ToNestedOutput() NestedOutput
    ToNestedOutputWithContext(context.Context) NestedOutput
}

type Nested struct {
    Foo int `pulumi:"foo"`
    Bar string `pulumi:"bar"`
}

type NestedInputValue struct {
    Foo pulumi.IntInput `pulumi:"foo"`
    Bar pulumi.StringInput `pulumi:"bar"`
}

func (NestedInputValue) ElementType() reflect.Type {
    return nestedType
}

func (v NestedInputValue) ToNestedOutput() NestedOutput {
    return pulumi.ToOutput(v).(NestedOutput)
}

func (v NestedInputValue) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
    return pulumi.ToOutputWithContext(ctx, v).(NestedOutput)
}

type NestedOutput struct { *pulumi.OutputState }

func (NestedOutput) ElementType() reflect.Type {
    return nestedType
}

func (o NestedOutput) ToNestedOutput() NestedOutput {
    return o
}

func (o NestedOutput) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
    return o
}

func (o NestedOutput) Foo() pulumi.IntOutput {
    return o.Apply(func (v Nested) int {
        return v.Foo
    }).(pulumi.IntOutput)
}

func (o NestedOutput) Bar() pulumi.StringOutput {
    return o.Apply(func (v Nested) string {
        return v.Bar
    }).(pulumi.StringOutput)
}
```

The SDK provides input and output types for primitives, arrays, and
maps.

2. Struct-based APIs

Instead of providing expected output properties in the input map passed
to {Read,Register}Resource and returning the outputs as a map, the user
now passes a pointer to a struct that implements one of the Resource
interfaces and has appropriately typed and tagged fields that represent
its output properties.

For example, given a custom resource with an int-typed output "foo" and
a string-typed output "bar", we would define the following
CustomResource type:

```
type MyResource struct {
    pulumi.CustomResourceState

    Foo pulumi.IntOutput    `pulumi:"foo"`
    Bar pulumi.StringOutput `pulumi:"bar"`
}
```

And invoke RegisterResource like so:

```
var resource MyResource
err := ctx.RegisterResource(tok, name, props, &resource, opts...)
```

Invoke arguments and results are also provided via structs, but use
plain-old Go types for their fields:

```
type MyInvokeArgs struct {
    Foo int `pulumi:"foo"`
}

type MyInvokeResult struct {
    Bar string `pulumi:"bar"`
}

var result MyInvokeResult
err := ctx.Invoke(tok, MyInvokeArgs{Foo: 42}, &result, opts...)
```

3. Ease-of-use of Apply

All `Apply` methods now accept an interface{} as the callback type.
The provided callback value must have one of the following signatures:

	func (v T) U
	func (v T) (U, error)
	func (ctx context.Context, v T) U
	func (ctx context.Context, v T) (U, error)

T must be assignable from the ElementType of the Output. If U is a type
that has a registered Output type, the result of the Apply will be the
corresponding Output type. Otherwise, the result of the Apply will be
AnyOutput.

Fixes https://github.com/pulumi/pulumi/issues/2149.
Fixes https://github.com/pulumi/pulumi/issues/3488.
Fixes https://github.com/pulumi/pulumi/issues/3487.
Fixes https://github.com/pulumi/pulumi-aws/issues/248.
Fixes https://github.com/pulumi/pulumi/issues/3492.
Fixes https://github.com/pulumi/pulumi/issues/3491.
Fixes https://github.com/pulumi/pulumi/issues/3562.
This commit is contained in:
Pat Gavlin 2020-01-18 10:08:37 -05:00 committed by GitHub
parent faa6d95178
commit f168bdc1c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 10062 additions and 1557 deletions

View file

@ -9,7 +9,6 @@ linters:
- gosec
- govet
- ineffassign
- interfacer
- lll
- megacheck
- misspell

View file

@ -16,6 +16,8 @@ CHANGELOG
- Add `BuildNumber` to CI vars and backend metadata property bag for CI systems that have separate ID and a user-friendly number. [#3766](https://github.com/pulumi/pulumi/pull/3766)
- Breaking changes for the Go SDK. Complete details are in [#3506](https://github.com/pulumi/pulumi/pull/3506).
## 1.8.1 (2019-12-20)
- Fix a panic in `pulumi stack select`. [#3687](https://github.com/pulumi/pulumi/pull/3687)

5
go.sum
View file

@ -64,6 +64,7 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.19.45 h1:jAxmC8qqa7mW531FDgM8Ahbqlb3zmiHgTpJU6fY3vJ0=
github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@ -175,6 +176,7 @@ github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -229,6 +231,7 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@ -239,7 +242,9 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=

View file

@ -5138,6 +5138,30 @@ func TestPreviewInputPropagation(t *testing.T) {
assert.Nil(t, res)
}
type testResource struct {
pulumi.CustomResourceState
Foo pulumi.StringOutput `pulumi:"foo"`
}
type testResourceArgs struct {
Foo string `pulumi:"foo"`
Bar string `pulumi:"bar"`
Baz string `pulumi:"baz"`
Bang string `pulumi:"bang"`
}
type testResourceInputs struct {
Foo pulumi.StringInput
Bar pulumi.StringInput
Baz pulumi.StringInput
Bang pulumi.StringInput
}
func (*testResourceInputs) ElementType() reflect.Type {
return reflect.TypeOf((*testResourceArgs)(nil))
}
func TestSingleResourceDefaultProviderGolangLifecycle(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
@ -5166,14 +5190,18 @@ func TestSingleResourceDefaultProviderGolangLifecycle(t *testing.T) {
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",
})
var resA testResource
err := ctx.RegisterResource("pkgA:m:typA", "resA", &testResourceInputs{
Foo: pulumi.String("bar"),
}, &resA)
assert.NoError(t, err)
_, err = ctx.RegisterResource("pkgA:m:typA", "resB", true, map[string]interface{}{
"baz": res.State["foo"],
})
var resB testResource
err = ctx.RegisterResource("pkgA:m:typA", "resB", &testResourceInputs{
Baz: resA.Foo.ApplyT(func(v string) string {
return v + "bar"
}).(pulumi.StringOutput),
}, &resB)
assert.NoError(t, err)
return nil
@ -5227,10 +5255,8 @@ func TestIgnoreChangesGolangLifecycle(t *testing.T) {
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
opts := pulumi.ResourceOpt{
IgnoreChanges: ignoreChanges,
}
_, err := ctx.RegisterResource("pkgA:m:typA", "resA", true, nil, opts)
var res pulumi.CustomResourceState
err := ctx.RegisterResource("pkgA:m:typA", "resA", nil, &res, pulumi.IgnoreChanges(ignoreChanges))
assert.NoError(t, err)
return nil
@ -5276,9 +5302,9 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
if !olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
ReplaceKeys: []resource.PropertyKey{"foo"},
DeleteBeforeReplace: true,
}, nil
}
@ -5287,8 +5313,8 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
if !olds["foo"].DeepEquals(news["foo"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"foo"}}, nil
}
return plugin.DiffResult{}, nil
},
@ -5296,9 +5322,7 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
}),
}
inputsA := map[string]interface{}{"A": "foo"}
optsA := pulumi.ResourceOpt{}
inputsA := &testResourceInputs{Foo: pulumi.String("foo")}
dbrValue, dbrA := true, (*bool)(nil)
getDbr := func() bool {
@ -5321,13 +5345,13 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
provider, err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "provA", true,
map[string]interface{}{})
var provider pulumi.ProviderResourceState
err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "provA", nil, &provider)
assert.NoError(t, err)
optsA.Provider = provider
optsA.DeleteBeforeReplace = getDbr()
fmt.Println(getDbr())
_, err = ctx.RegisterResource("pkgA:m:typA", "resA", true, inputsA, optsA)
var res pulumi.CustomResourceState
err = ctx.RegisterResource("pkgA:m:typA", "resA", inputsA, &res,
pulumi.Provider(provider), pulumi.DeleteBeforeReplace(getDbr()))
assert.NoError(t, err)
return nil
@ -5340,7 +5364,7 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
snap := p.Run(t, nil)
// Change the value of resA.A. Should create before replace
inputsA["A"] = "bar"
inputsA.Foo = pulumi.String("bar")
p.Steps = []TestStep{{
Op: Update,
@ -5364,7 +5388,7 @@ func TestExplicitDeleteBeforeReplaceGoSDK(t *testing.T) {
// Change the registration of resA such that it requires delete-before-replace and change the value of resA.A.
// replacement should be delete-before-replace.
dbrA, inputsA["A"] = &dbrValue, "baz"
dbrA, inputsA.Foo = &dbrValue, pulumi.String("baz")
p.Steps = []TestStep{{
Op: Update,
@ -5414,8 +5438,8 @@ func TestReadResourceGolangLifecycle(t *testing.T) {
assert.NoError(t, err)
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
opts := pulumi.ResourceOpt{}
_, err := ctx.ReadResource("pkgA:m:typA", "resA", "someId", map[string]interface{}{}, opts)
var res pulumi.CustomResourceState
err := ctx.ReadResource("pkgA:m:typA", "resA", pulumi.ID("someId"), nil, &res)
assert.NoError(t, err)
return nil
@ -5454,6 +5478,11 @@ func TestReadResourceGolangLifecycle(t *testing.T) {
// and Invoke all respect the provider hierarchy
// most specific providers are used first 1. resource.provider, 2. resource.providers, 3. resource.parent.providers
func TestProviderInheritanceGolangLifecycle(t *testing.T) {
type invokeArgs struct {
Bang string `pulumi:"bang"`
Bar string `pulumi:"bar"`
}
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
v := &deploytest.Provider{
@ -5507,109 +5536,100 @@ func TestProviderInheritanceGolangLifecycle(t *testing.T) {
return pulumi.RunWithContext(ctx, func(ctx *pulumi.Context) error {
// register a couple of providers, pass in some props that we can use to indentify it during invoke
providerA, err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "prov1", true,
map[string]interface{}{
"foo": "1",
})
var providerA pulumi.ProviderResourceState
err := ctx.RegisterResource(string(providers.MakeProviderType("pkgA")), "prov1",
&testResourceInputs{
Foo: pulumi.String("1"),
}, &providerA)
assert.NoError(t, err)
providerB, err := ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov2", true,
map[string]interface{}{
"bar": "2",
})
var providerB pulumi.ProviderResourceState
err = ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov2",
&testResourceInputs{
Bar: pulumi.String("2"),
Bang: pulumi.String(""),
}, &providerB)
assert.NoError(t, err)
providerBOverride, err := ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov3", true,
map[string]interface{}{
"bang": "3",
})
var providerBOverride pulumi.ProviderResourceState
err = ctx.RegisterResource(string(providers.MakeProviderType("pkgB")), "prov3",
&testResourceInputs{
Bar: pulumi.String(""),
Bang: pulumi.String("3"),
}, &providerBOverride)
assert.NoError(t, err)
componentProviders := make(map[string]pulumi.ProviderResource)
componentProviders["pkgA"] = providerA
componentProviders["pkgB"] = providerB
// create a component resource that uses provider map
componentResource, err := ctx.RegisterResource("pkgA:m:typA", "resA", true,
map[string]interface{}{}, pulumi.ResourceOpt{
Providers: componentProviders,
})
parentProviders := make(map[string]pulumi.ProviderResource)
parentProviders["pkgA"] = providerA
parentProviders["pkgB"] = providerB
// create a parent resource that uses provider map
var parentResource pulumi.CustomResourceState
err = ctx.RegisterResource("pkgA:m:typA", "resA", nil, &parentResource, pulumi.ProviderMap(parentProviders))
assert.NoError(t, err)
// component uses specified provider from map
componentResultProvider := componentResource.GetProvider("pkgA:m:typA")
assert.Same(t, providerA, componentResultProvider)
// parent uses specified provider from map
parentResultProvider := parentResource.GetProvider("pkgA:m:typA")
assert.Equal(t, providerA, parentResultProvider)
// create a child resource
childResource, err := ctx.RegisterResource("pkgB:m:typB", "resBChild", true,
map[string]interface{}{}, pulumi.ResourceOpt{
Parent: componentResource,
})
var childResource pulumi.CustomResourceState
err = ctx.RegisterResource("pkgB:m:typB", "resBChild", nil, &childResource, pulumi.Parent(parentResource))
assert.NoError(t, err)
// child uses provider value from parent
childResultProvider := childResource.GetProvider("pkgB:m:typB")
assert.Same(t, providerB, childResultProvider)
assert.Equal(t, providerB, childResultProvider)
// create a child with a provider specified
childWithOverride, err := ctx.RegisterResource("pkgB:m:typB", "resBChildOverride", true,
map[string]interface{}{}, pulumi.ResourceOpt{
Parent: componentResource,
Provider: providerBOverride,
})
var childWithOverride pulumi.CustomResourceState
err = ctx.RegisterResource("pkgB:m:typB", "resBChildOverride", nil, &childWithOverride,
pulumi.Parent(parentResource), pulumi.Provider(providerBOverride))
assert.NoError(t, err)
// child uses the specified provider, and not the provider from the parent
childWithOverrideProvider := childWithOverride.GetProvider("pkgB:m:typB")
assert.Same(t, providerBOverride, childWithOverrideProvider)
assert.Equal(t, providerBOverride, childWithOverrideProvider)
// pass in a fake ID
testID := pulumi.ID("testID")
// read a component resource that uses provider map
componentResource, err = ctx.ReadResource("pkgA:m:typA", "readResA", testID,
map[string]interface{}{}, pulumi.ResourceOpt{
Providers: componentProviders,
})
// read a resource that uses provider map
err = ctx.ReadResource("pkgA:m:typA", "readResA", testID, nil, &parentResource, pulumi.ProviderMap(parentProviders))
assert.NoError(t, err)
// component uses specified provider from map
componentResultProvider = componentResource.GetProvider("pkgA:m:typA")
assert.Same(t, providerA, componentResultProvider)
// parent uses specified provider from map
parentResultProvider = parentResource.GetProvider("pkgA:m:typA")
assert.Equal(t, providerA, parentResultProvider)
// read a child resource
childResource, err = ctx.ReadResource("pkgB:m:typB", "readResBChild", testID,
map[string]interface{}{}, pulumi.ResourceOpt{
Parent: componentResource,
})
err = ctx.ReadResource("pkgB:m:typB", "readResBChild", testID, nil, &childResource, pulumi.Parent(parentResource))
assert.NoError(t, err)
// child uses provider value from parent
childResultProvider = childResource.GetProvider("pkgB:m:typB")
assert.Same(t, providerB, childResultProvider)
assert.Equal(t, providerB, childResultProvider)
// read a child with a provider specified
childWithOverride, err = ctx.ReadResource("pkgB:m:typB", "readResBChildOverride", testID,
map[string]interface{}{}, pulumi.ResourceOpt{
Parent: componentResource,
Provider: providerBOverride,
})
err = ctx.ReadResource("pkgB:m:typB", "readResBChildOverride", testID, nil, &childWithOverride,
pulumi.Parent(parentResource), pulumi.Provider(providerBOverride))
assert.NoError(t, err)
// child uses the specified provider, and not the provider from the parent
childWithOverrideProvider = childWithOverride.GetProvider("pkgB:m:typB")
assert.Same(t, providerBOverride, childWithOverrideProvider)
assert.Equal(t, providerBOverride, childWithOverrideProvider)
// invoke with specific provider
_, err = ctx.Invoke("pkgB:do:something", map[string]interface{}{
"bang": "3",
}, pulumi.InvokeOpt{Provider: providerBOverride})
var invokeResult struct{}
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bang: "3",
}, &invokeResult, pulumi.Provider(providerBOverride))
assert.NoError(t, err)
// invoke with parent
_, err = ctx.Invoke("pkgB:do:something", map[string]interface{}{
"bar": "2",
}, pulumi.InvokeOpt{Parent: componentResource})
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bar: "2",
}, &invokeResult, pulumi.Parent(parentResource))
assert.NoError(t, err)
// invoke with parent and provider
_, err = ctx.Invoke("pkgB:do:something", map[string]interface{}{
"bang": "3",
}, pulumi.InvokeOpt{Parent: componentResource, Provider: providerBOverride})
err = ctx.Invoke("pkgB:do:something", invokeArgs{
Bang: "3",
}, &invokeResult, pulumi.Parent(parentResource), pulumi.Provider(providerBOverride))
assert.NoError(t, err)
return nil

View file

@ -1,13 +1,16 @@
PROJECT_NAME := Pulumi Go SDK
LANGHOST_PKG := github.com/pulumi/pulumi/sdk/go/pulumi-language-go
VERSION := $(shell ../../scripts/get-version HEAD)
PROJECT_PKGS := $(shell go list ./pulumi/... ./pulumi-language-go/... | grep -v /vendor/)
PROJECT_PKGS := $(shell go list ./pulumi/... ./pulumi-language-go/... | grep -v /vendor/ | grep -v templates)
TESTPARALLELISM := 10
include ../../build/common.mk
build::
gen::
go generate ./pulumi/...
build:: gen
go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGHOST_PKG}
install_plugin::

View file

@ -12,23 +12,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package asset
package pulumi
import (
"reflect"
"github.com/pulumi/pulumi/pkg/util/contract"
"golang.org/x/net/context"
)
// AssetOrArchive represents either an Asset or an Archive.
type AssetOrArchive interface {
isAssetOrArchive()
}
// Asset represents a file that is managed in conjunction with Pulumi resources. An Asset may be backed by a number
// of sources, including local filesystem paths, in-memory blobs of text, or remote files referenced by a URL.
type Asset interface {
AssetOrArchive
AssetInput
ToAssetOrArchiveOutput() AssetOrArchiveOutput
ToAssetOrArchiveOutputWithContext(ctx context.Context) AssetOrArchiveOutput
// Path returns the filesystem path, for file-based assets.
Path() string
// Text returns an in-memory blob of text, for string-based assets.
Text() string
// URI returns a URI, for remote network-based assets.
URI() string
isAsset()
}
type asset struct {
@ -61,14 +75,26 @@ func (a *asset) Text() string { return a.text }
// URI returns the asset's URL, if this is a remote asset, or an empty string otherwise.
func (a *asset) URI() string { return a.uri }
func (a *asset) isAsset() {}
func (a *asset) isAssetOrArchive() {}
// Archive represents a collection of Assets.
type Archive interface {
AssetOrArchive
ArchiveInput
ToAssetOrArchiveOutput() AssetOrArchiveOutput
ToAssetOrArchiveOutputWithContext(ctx context.Context) AssetOrArchiveOutput
// Assets returns a map of named assets or archives, for collections.
Assets() map[string]interface{}
// Path returns the filesystem path, for file-based archives.
Path() string
// URI returns a URI, for remote network-based archives.
URI() string
isArchive()
}
type archive struct {
@ -108,3 +134,7 @@ func (a *archive) Path() string { return a.path }
// URI returns the archive's URL, if this is a remote archive, or an empty string otherwise.
func (a *archive) URI() string { return a.uri }
func (a *archive) isArchive() {}
func (a *archive) isAssetOrArchive() {}

View file

@ -12,9 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:generate go run generate.go
package pulumi
import (
"reflect"
"sort"
"strings"
"sync"
@ -25,6 +28,9 @@ import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/util/logging"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
)
@ -33,8 +39,8 @@ import (
type Context struct {
ctx context.Context
info RunInfo
stackR URN
exports map[string]interface{}
stack Resource
exports map[string]Input
monitor pulumirpc.ResourceMonitorClient
monitorConn *grpc.ClientConn
engine pulumirpc.EngineClient
@ -42,6 +48,7 @@ type Context struct {
rpcs int // the number of outstanding RPC requests.
rpcsDone *sync.Cond // an event signaling completion of RPCs.
rpcsLock *sync.Mutex // a lock protecting the RPC count and event.
rpcError error // the first error (if any) encountered during an RPC.
}
// NewContext creates a fresh run context out of the given metadata.
@ -69,11 +76,16 @@ func NewContext(ctx context.Context, info RunInfo) (*Context, error) {
engine = pulumirpc.NewEngineClient(engineConn)
}
if info.Mocks != nil {
monitor = &mockMonitor{project: info.Project, stack: info.Stack, mocks: info.Mocks}
engine = &mockEngine{}
}
mutex := &sync.Mutex{}
return &Context{
ctx: ctx,
info: info,
exports: make(map[string]interface{}),
exports: make(map[string]Input),
monitorConn: monitorConn,
monitor: monitor,
engineConn: engineConn,
@ -117,55 +129,70 @@ func (ctx *Context) GetConfig(key string) (string, bool) {
return v, ok
}
// Invoke will invoke a provider's function, identified by its token tok. This function call is synchronous.
func (ctx *Context) Invoke(tok string, args map[string]interface{}, opts ...InvokeOpt) (map[string]interface{}, error) {
// Invoke will invoke a provider's function, identified by its token tok. This function call is synchronous.
//
// args and result must be pointers to struct values fields and appropriately tagged and typed for use with Pulumi.
func (ctx *Context) Invoke(tok string, args interface{}, result interface{}, opts ...InvokeOption) error {
if tok == "" {
return nil, errors.New("invoke token must not be empty")
return errors.New("invoke token must not be empty")
}
// Check for a provider option.
var provider string
for _, opt := range opts {
if opt.Parent != nil && opt.Provider == nil {
// attempt to use parent provider if no other is specified.
v, ok := opt.Parent.(*ResourceState)
if ok {
opt.Provider = v.GetProvider(tok)
}
}
if opt.Provider != nil {
pr, err := ctx.resolveProviderReference(opt.Provider)
if err != nil {
return nil, err
}
provider = pr
break
}
resultV := reflect.ValueOf(result)
if resultV.Kind() != reflect.Ptr || resultV.Elem().Kind() != reflect.Struct {
return errors.New("result must be a pointer to a struct value")
}
// 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, false)
options := &invokeOptions{}
for _, o := range opts {
o.applyInvokeOption(options)
}
var providerRef string
if provider := mergeProviders(tok, options.Parent, options.Provider, nil)[getPackage(tok)]; provider != nil {
pr, err := ctx.resolveProviderReference(provider)
if err != nil {
return err
}
providerRef = pr
}
// Serialize arguments. Outputs will not be awaited: instead, an error will be returned if any Outputs are present.
if args == nil {
args = struct{}{}
}
resolvedArgs, _, err := marshalInput(args, anyType, false)
if err != nil {
return nil, errors.Wrap(err, "marshaling arguments")
return errors.Wrap(err, "marshaling arguments")
}
resolvedArgsMap := resource.PropertyMap{}
if resolvedArgs.IsObject() {
resolvedArgsMap = resolvedArgs.ObjectValue()
}
rpcArgs, err := plugin.MarshalProperties(
resolvedArgsMap,
plugin.MarshalOptions{KeepUnknowns: false})
if err != nil {
return errors.Wrap(err, "marshaling arguments")
}
// Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown.
if err = ctx.beginRPC(); err != nil {
return nil, err
return err
}
defer ctx.endRPC()
defer ctx.endRPC(err)
// Now, invoke the RPC to the provider synchronously.
logging.V(9).Infof("Invoke(%s, #args=%d): RPC call being made synchronously", tok, len(args))
logging.V(9).Infof("Invoke(%s, #args=%d): RPC call being made synchronously", tok, len(resolvedArgsMap))
resp, err := ctx.monitor.Invoke(ctx.ctx, &pulumirpc.InvokeRequest{
Tok: tok,
Args: rpcArgs,
Provider: provider,
Provider: providerRef,
})
if err != nil {
logging.V(9).Infof("Invoke(%s, ...): error: %v", tok, err)
return nil, err
return err
}
// If there were any failures from the provider, return them.
@ -176,50 +203,101 @@ func (ctx *Context) Invoke(tok string, args map[string]interface{}, opts ...Invo
ferr = multierror.Append(ferr,
errors.Errorf("%s invoke failed: %s (%s)", tok, failure.Reason, failure.Property))
}
return nil, ferr
return ferr
}
// Otherwsie, simply unmarshal the output properties and return the result.
outs, err := unmarshalOutputs(resp.Return)
logging.V(9).Infof("Invoke(%s, ...): success: w/ %d outs (err=%v)", tok, len(outs), err)
return outs, err
outProps, err := plugin.UnmarshalProperties(resp.Return, plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return err
}
if err = unmarshalOutput(resource.NewObjectProperty(outProps), resultV.Elem()); err != nil {
return err
}
logging.V(9).Infof("Invoke(%s, ...): success: w/ %d outs (err=%v)", tok, len(outProps), err)
return nil
}
// ReadResource reads an existing custom resource's state from the resource monitor. Note that resources read in this
// way will not be part of the resulting stack's state, as they are presumed to belong to another.
// ReadResource reads an existing custom resource's state from the resource monitor. t is the fully qualified type
// token and name is the "name" part to use in creating a stable and globally unique URN for the object. id is the ID
// of the resource to read, and props contains any state necessary to perform the read (typically props will be nil).
// opts contains optional settings that govern the way the resource is managed.
//
// The value passed to resource must be a pointer to a struct. The fields of this struct that correspond to output
// properties of the resource must have types that are assignable from Output, and must have a `pulumi` tag that
// records the name of the corresponding output property. The struct must embed the CustomResourceState type.
//
// For example, given a custom resource with an int-typed output "foo" and a string-typed output "bar", one would
// define the following CustomResource type:
//
// type MyResource struct {
// pulumi.CustomResourceState
//
// Foo pulumi.IntOutput `pulumi:"foo"`
// Bar pulumi.StringOutput `pulumi:"bar"`
// }
//
// And invoke ReadResource like so:
//
// var resource MyResource
// err := ctx.ReadResource(tok, name, id, nil, &resource, opts...)
//
func (ctx *Context) ReadResource(
t, name string, id ID, props map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) {
t, name string, id IDInput, props Input, resource CustomResource, opts ...ResourceOption) error {
if t == "" {
return nil, errors.New("resource type argument cannot be empty")
return errors.New("resource type argument cannot be empty")
} else if name == "" {
return nil, errors.New("resource name argument (for URN creation) cannot be empty")
} else if id == "" {
return nil, errors.New("resource ID is required for lookup and cannot be empty")
return errors.New("resource name argument (for URN creation) cannot be empty")
} else if id == nil {
return errors.New("resource ID is required for lookup and cannot be empty")
}
if props != nil {
propsType := reflect.TypeOf(props)
if propsType.Kind() == reflect.Ptr {
propsType = propsType.Elem()
}
if propsType.Kind() != reflect.Struct {
return errors.New("props must be a struct or a pointer to a struct")
}
}
options := &resourceOptions{}
for _, o := range opts {
o.applyResourceOption(options)
}
if options.Parent == nil {
options.Parent = ctx.stack
}
// Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown.
if err := ctx.beginRPC(); err != nil {
return nil, err
return err
}
// Create resolvers for the resource's outputs.
res := makeResourceState(true, props)
res.providers = mergeProviders(t, opts...)
res := makeResourceState(t, resource, mergeProviders(t, options.Parent, options.Provider, options.Providers))
// Kick off the resource read operation. This will happen asynchronously and resolve the above properties.
go func() {
// No matter the outcome, make sure all promises are resolved and that we've signaled completion of this RPC.
var urn, resID string
var inputs *resourceInputs
var state *structpb.Struct
var err error
defer func() {
res.resolve(ctx.DryRun(), err, props, urn, resID, state)
ctx.endRPC()
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state)
ctx.endRPC(err)
}()
idToRead, known, err := id.ToIDOutput().awaitID(context.TODO())
if !known || err != nil {
return
}
// Prepare the inputs for an impending operation.
inputs, err := ctx.prepareResourceInputs(props, t, res.providers, opts...)
inputs, err = ctx.prepareResourceInputs(props, t, res.providers, options)
if err != nil {
return
}
@ -231,57 +309,101 @@ func (ctx *Context) ReadResource(
Parent: inputs.parent,
Properties: inputs.rpcProps,
Provider: inputs.provider,
Id: string(id),
Id: string(idToRead),
})
if err != nil {
logging.V(9).Infof("RegisterResource(%s, %s): error: %v", t, name, err)
logging.V(9).Infof("ReadResource(%s, %s): error: %v", t, name, err)
} else {
logging.V(9).Infof("RegisterResource(%s, %s): success: %s %s ...", t, name, resp.Urn, id)
logging.V(9).Infof("ReadResource(%s, %s): success: %s %s ...", t, name, resp.Urn, id)
}
if resp != nil {
urn, resID = resp.Urn, string(id)
urn, resID = resp.Urn, string(idToRead)
state = resp.Properties
}
}()
return res, nil
return nil
}
// RegisterResource creates and registers a new resource object. t is the fully qualified type token and name is
// the "name" part to use in creating a stable and globally unique URN for the object. state contains the goal state
// RegisterResource creates and registers a new resource object. t is the fully qualified type token and name is
// the "name" part to use in creating a stable and globally unique URN for the object. props contains the goal state
// for the resource object and opts contains optional settings that govern the way the resource is created.
//
// The value passed to resource must be a pointer to a struct. The fields of this struct that correspond to output
// properties of the resource must have types that are assignable from Output, and must have a `pulumi` tag that
// records the name of the corresponding output property. The struct must embed either the ResourceState or the
// CustomResourceState type.
//
// For example, given a custom resource with an int-typed output "foo" and a string-typed output "bar", one would
// define the following CustomResource type:
//
// type MyResource struct {
// pulumi.CustomResourceState
//
// Foo pulumi.IntOutput `pulumi:"foo"`
// Bar pulumi.StringOutput `pulumi:"bar"`
// }
//
// And invoke RegisterResource like so:
//
// var resource MyResource
// err := ctx.RegisterResource(tok, name, props, &resource, opts...)
//
func (ctx *Context) RegisterResource(
t, name string, custom bool, props map[string]interface{}, opts ...ResourceOpt) (*ResourceState, error) {
t, name string, props Input, resource Resource, opts ...ResourceOption) error {
if t == "" {
return nil, errors.New("resource type argument cannot be empty")
return errors.New("resource type argument cannot be empty")
} else if name == "" {
return nil, errors.New("resource name argument (for URN creation) cannot be empty")
return errors.New("resource name argument (for URN creation) cannot be empty")
}
_, custom := resource.(CustomResource)
if _, isProvider := resource.(ProviderResource); isProvider && !strings.HasPrefix(t, "pulumi:providers:") {
return errors.New("provider resource type must begin with \"pulumi:providers:\"")
}
if props != nil {
propsType := reflect.TypeOf(props)
if propsType.Kind() == reflect.Ptr {
propsType = propsType.Elem()
}
if propsType.Kind() != reflect.Struct {
return errors.New("props must be a struct or a pointer to a struct")
}
}
options := &resourceOptions{}
for _, o := range opts {
o.applyResourceOption(options)
}
if options.Parent == nil {
options.Parent = ctx.stack
}
// Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown.
if err := ctx.beginRPC(); err != nil {
return nil, err
return err
}
// Create resolvers for the resource's outputs.
res := makeResourceState(custom, props)
res.providers = mergeProviders(t, opts...)
res := makeResourceState(t, resource, mergeProviders(t, options.Parent, options.Provider, options.Providers))
// 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.
go func() {
// No matter the outcome, make sure all promises are resolved and that we've signaled completion of this RPC.
var urn, resID string
var inputs *resourceInputs
var state *structpb.Struct
var err error
defer func() {
res.resolve(ctx.DryRun(), err, props, urn, resID, state)
ctx.endRPC()
res.resolve(ctx.DryRun(), err, inputs, urn, resID, state)
ctx.endRPC(err)
}()
// Prepare the inputs for an impending operation.
inputs, err := ctx.prepareResourceInputs(props, t, res.providers, opts...)
inputs, err = ctx.prepareResourceInputs(props, t, res.providers, options)
if err != nil {
return
}
@ -313,77 +435,45 @@ func (ctx *Context) RegisterResource(
}
}()
return res, nil
return nil
}
// 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
// Map from pkg to provider
func (ctx *Context) RegisterComponentResource(
t, name string, resource ComponentResource, opts ...ResourceOption) error {
return ctx.RegisterResource(t, name, nil, resource, opts...)
}
// resourceState contains the results of a resource registration operation.
type resourceState struct {
outputs map[string]Output
providers map[string]ProviderResource
}
// 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
}
// checks all possible sources of providers and merges them with preference given to the most specific
func mergeProviders(t string, opts ...ResourceOpt) map[string]ProviderResource {
var parent Resource
var provider ProviderResource
providers := make(map[string]ProviderResource)
for _, opt := range opts {
if parent == nil && opt.Parent != nil {
parent = opt.Parent
}
if provider == nil && opt.Provider != nil {
provider = opt.Provider
}
if len(providers) == 0 && opt.Providers != nil {
for k, v := range opt.Providers {
providers[k] = v
}
}
}
func mergeProviders(t string, parent Resource, provider ProviderResource,
providers map[string]ProviderResource) map[string]ProviderResource {
// copy parent providers, giving precedence to existing providers
// copy parent providers
result := make(map[string]ProviderResource)
if parent != nil {
rs, ok := parent.(*ResourceState)
if ok {
for k, v := range rs.providers {
if _, has := providers[k]; !has {
providers[k] = v
}
}
for k, v := range parent.getProviders() {
result[k] = v
}
}
pkg := getPackage(t)
// copy provider map
for k, v := range providers {
result[k] = v
}
// copy specified provider which has highest precedence
// copy specific provider, if any
if provider != nil {
providers[pkg] = provider
pkg := getPackage(t)
result[pkg] = provider
}
return providers
}
// GetProvider takes a URN and returns the associated provider
func (state *ResourceState) GetProvider(t string) ProviderResource {
pkg := getPackage(t)
return state.providers[pkg]
return result
}
// getPackage takes in a type and returns the pkg
@ -397,66 +487,122 @@ func getPackage(t string) string {
// makeResourceState creates a set of resolvers that we'll use to finalize state, for URNs, IDs, and output
// properties.
func makeResourceState(custom bool, props map[string]interface{}) *ResourceState {
state := &ResourceState{}
func makeResourceState(t string, resourceV Resource, providers map[string]ProviderResource) *resourceState {
resource := reflect.ValueOf(resourceV)
state.urn = URNOutput(newOutput(state))
if custom {
state.id = IDOutput(newOutput(state))
typ := resource.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return &resourceState{}
}
state.State = make(map[string]Output)
for key := range props {
state.State[key] = newOutput(state)
resource, typ = resource.Elem(), typ.Elem()
var rs *ResourceState
var crs *CustomResourceState
var prs *ProviderResourceState
switch r := resourceV.(type) {
case *ResourceState:
rs = r
case *CustomResourceState:
crs = r
case *ProviderResourceState:
prs = r
}
state.providers = make(map[string]ProviderResource)
state := &resourceState{outputs: map[string]Output{}}
for i := 0; i < typ.NumField(); i++ {
fieldV := resource.Field(i)
if !fieldV.CanSet() {
continue
}
field := typ.Field(i)
switch {
case field.Anonymous && field.Type == resourceStateType:
rs = fieldV.Addr().Interface().(*ResourceState)
case field.Anonymous && field.Type == customResourceStateType:
crs = fieldV.Addr().Interface().(*CustomResourceState)
case field.Anonymous && field.Type == providerResourceStateType:
prs = fieldV.Addr().Interface().(*ProviderResourceState)
case field.Type.Implements(outputType):
tag := typ.Field(i).Tag.Get("pulumi")
if tag == "" {
continue
}
output := newOutput(field.Type, resourceV)
fieldV.Set(reflect.ValueOf(output))
state.outputs[tag] = output
}
}
if prs != nil {
crs = &prs.CustomResourceState
prs.pkg = t[len("pulumi:providers:"):]
}
if crs != nil {
rs = &crs.ResourceState
crs.id = IDOutput{newOutputState(idType, resourceV)}
state.outputs["id"] = crs.id
}
contract.Assert(rs != nil)
rs.providers = providers
rs.urn = URNOutput{newOutputState(urnType, resourceV)}
state.outputs["urn"] = rs.urn
return state
}
// resolve resolves the resource outputs using the given error and/or values.
func (state *ResourceState) resolve(dryrun bool, err error, inputs map[string]interface{}, urn, id string,
func (state *resourceState) resolve(dryrun bool, err error, inputs *resourceInputs, urn, id string,
result *structpb.Struct) {
var outprops map[string]interface{}
var inprops resource.PropertyMap
if inputs != nil {
inprops = inputs.resolvedProps
}
var outprops resource.PropertyMap
if err == nil {
outprops, err = unmarshalOutputs(result)
outprops, err = plugin.UnmarshalProperties(result, plugin.MarshalOptions{KeepSecrets: true})
}
if err != nil {
// If there was an error, we must reject everything: URN, ID, and state properties.
state.urn.s.reject(err)
if state.id.s != nil {
state.id.s.reject(err)
}
for _, o := range state.State {
o.s.reject(err)
// If there was an error, we must reject everything.
for _, output := range state.outputs {
output.reject(err)
}
return
}
// 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)
outprops["urn"] = resource.NewStringProperty(urn)
if id != "" || !dryrun {
outprops["id"] = resource.NewStringProperty(id)
} else {
outprops["id"] = resource.MakeComputed(resource.PropertyValue{})
}
// 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]
for k, output := range state.outputs {
// If this is an unknown or missing value during a dry run, do nothing.
v, ok := outprops[resource.PropertyKey(k)]
if !ok && !dryrun {
v = inprops[resource.PropertyKey(k)]
}
known := true
if v.IsNull() || v.IsComputed() || v.IsOutput() {
known = !dryrun
}
// Allocate storage for the unmarshalled output.
dest := reflect.New(output.ElementType()).Elem()
if err = unmarshalOutput(v, dest); err != nil {
output.reject(err)
} else {
output.resolve(dest.Interface(), known)
}
o.s.resolve(v, isKnown(v))
}
}
@ -466,6 +612,7 @@ type resourceInputs struct {
deps []string
protect bool
provider string
resolvedProps resource.PropertyMap
rpcProps *structpb.Struct
rpcPropertyDeps map[string]*pulumirpc.RegisterResourceRequest_PropertyDependencies
deleteBeforeReplace bool
@ -475,21 +622,28 @@ type resourceInputs struct {
}
// prepareResourceInputs prepares the inputs for a resource operation, shared between read and register.
func (ctx *Context) prepareResourceInputs(props map[string]interface{}, t string,
providers map[string]ProviderResource, opts ...ResourceOpt) (*resourceInputs, error) {
func (ctx *Context) prepareResourceInputs(props Input, t string,
providers map[string]ProviderResource, opts *resourceOptions) (*resourceInputs, error) {
// Get the parent and dependency URNs from the options, in addition to the protection bit. If there wasn't an
// explicit parent, and a root stack resource exists, we will automatically parent to that.
parent, optDeps, protect, provider, deleteBeforeReplace,
importID, ignoreChanges, err := ctx.getOpts(t, providers, opts...)
importID, ignoreChanges, err := ctx.getOpts(t, providers, opts)
if err != nil {
return nil, errors.Wrap(err, "resolving options")
}
timeouts := ctx.getTimeouts(opts...)
// Serialize all properties, first by awaiting them, and then marshaling them to the requisite gRPC values.
resolvedProps, propertyDeps, rpcDeps, err := marshalInputs(props)
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
// Marshal all properties for the RPC call.
keepUnknowns := ctx.DryRun()
rpcProps, propertyDeps, rpcDeps, err := marshalInputs(props, keepUnknowns)
rpcProps, err := plugin.MarshalProperties(
resolvedProps,
plugin.MarshalOptions{KeepUnknowns: keepUnknowns})
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
@ -528,68 +682,43 @@ func (ctx *Context) prepareResourceInputs(props map[string]interface{}, t string
deps: deps,
protect: protect,
provider: provider,
resolvedProps: resolvedProps,
rpcProps: rpcProps,
rpcPropertyDeps: rpcPropertyDeps,
deleteBeforeReplace: deleteBeforeReplace,
importID: string(importID),
customTimeouts: timeouts,
customTimeouts: getTimeouts(opts.CustomTimeouts),
ignoreChanges: ignoreChanges,
}, nil
}
func (ctx *Context) getTimeouts(opts ...ResourceOpt) *pulumirpc.RegisterResourceRequest_CustomTimeouts {
func getTimeouts(custom *CustomTimeouts) *pulumirpc.RegisterResourceRequest_CustomTimeouts {
var timeouts pulumirpc.RegisterResourceRequest_CustomTimeouts
for _, opt := range opts {
if opt.CustomTimeouts != nil {
timeouts.Update = opt.CustomTimeouts.Update
timeouts.Create = opt.CustomTimeouts.Create
timeouts.Delete = opt.CustomTimeouts.Delete
}
if custom != nil {
timeouts.Update = custom.Update
timeouts.Create = custom.Create
timeouts.Delete = custom.Delete
}
return &timeouts
}
// getOpts returns a set of resource options from an array of them. This includes the parent URN, any dependency URNs,
// a boolean indicating whether the resource is to be protected, and the URN and ID of the resource's provider, if any.
func (ctx *Context) getOpts(t string, providers map[string]ProviderResource, opts ...ResourceOpt) (
func (ctx *Context) getOpts(t string, providers map[string]ProviderResource, opts *resourceOptions) (
URN, []URN, bool, string, bool, ID, []string, error) {
var parent Resource
var deps []Resource
var protect bool
var provider ProviderResource
var deleteBeforeReplace bool
var importID ID
var ignoreChanges []string
for _, opt := range opts {
if parent == nil && opt.Parent != nil {
parent = opt.Parent
}
if deps == nil && opt.DependsOn != nil {
deps = opt.DependsOn
}
if !protect && opt.Protect {
protect = true
}
if provider == nil && opt.Provider != nil {
provider = opt.Provider
}
if !deleteBeforeReplace && opt.DeleteBeforeReplace {
deleteBeforeReplace = true
}
if importID == "" && opt.Import != "" {
importID = opt.Import
}
if ignoreChanges == nil && opt.IgnoreChanges != nil {
ignoreChanges = opt.IgnoreChanges
if opts.Import != nil {
id, _, err := opts.Import.ToIDOutput().awaitID(context.TODO())
if err != nil {
return "", nil, false, "", false, "", nil, err
}
importID = id
}
var parentURN URN
if parent == nil {
parentURN = ctx.stackR
} else {
urn, _, err := parent.URN().await(context.TODO())
if opts.Parent != nil {
urn, _, err := opts.Parent.URN().awaitURN(context.TODO())
if err != nil {
return "", nil, false, "", false, "", nil, err
}
@ -597,10 +726,10 @@ func (ctx *Context) getOpts(t string, providers map[string]ProviderResource, opt
}
var depURNs []URN
if deps != nil {
depURNs = make([]URN, len(deps))
for i, r := range deps {
urn, _, err := r.URN().await(context.TODO())
if opts.DependsOn != nil {
depURNs = make([]URN, len(opts.DependsOn))
for i, r := range opts.DependsOn {
urn, _, err := r.URN().awaitURN(context.TODO())
if err != nil {
return "", nil, false, "", false, "", nil, err
}
@ -608,6 +737,7 @@ func (ctx *Context) getOpts(t string, providers map[string]ProviderResource, opt
}
}
provider := opts.Provider
if provider == nil {
pkg := getPackage(t)
provider = providers[pkg]
@ -622,15 +752,15 @@ func (ctx *Context) getOpts(t string, providers map[string]ProviderResource, opt
providerRef = pr
}
return parentURN, depURNs, protect, providerRef, deleteBeforeReplace, importID, ignoreChanges, nil
return parentURN, depURNs, opts.Protect, providerRef, opts.DeleteBeforeReplace, importID, opts.IgnoreChanges, nil
}
func (ctx *Context) resolveProviderReference(provider ProviderResource) (string, error) {
urn, _, err := provider.URN().await(context.TODO())
urn, _, err := provider.URN().awaitURN(context.TODO())
if err != nil {
return "", err
}
id, known, err := provider.ID().await(context.TODO())
id, known, err := provider.ID().awaitID(context.TODO())
if err != nil {
return "", err
}
@ -659,10 +789,14 @@ func (ctx *Context) beginRPC() error {
}
// endRPC signals the completion of an RPC and notifies any potential awaiters when outstanding RPCs hit zero.
func (ctx *Context) endRPC() {
func (ctx *Context) endRPC(err error) {
ctx.rpcsLock.Lock()
defer ctx.rpcsLock.Unlock()
if err != nil && ctx.rpcError == nil {
ctx.rpcError = err
}
ctx.rpcs--
if ctx.rpcs == 0 {
ctx.rpcsDone.Broadcast()
@ -685,42 +819,53 @@ func (ctx *Context) waitForRPCs() {
ctx.rpcs = noMoreRPCs
}
var _ Resource = (*ResourceState)(nil)
var _ CustomResource = (*ResourceState)(nil)
var _ ComponentResource = (*ResourceState)(nil)
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 {
keepUnknowns := ctx.DryRun()
outsMarshalled, _, _, err := marshalInputs(outs, keepUnknowns)
if err != nil {
return errors.Wrap(err, "marshaling outputs")
}
func (ctx *Context) RegisterResourceOutputs(resource Resource, outs Map) error {
// Note that we're about to make an outstanding RPC request, so that we can rendezvous during shutdown.
if err = ctx.beginRPC(); err != nil {
if err := ctx.beginRPC(); err != nil {
return err
}
// Register the outputs
logging.V(9).Infof("RegisterResourceOutputs(%s): RPC call being made", urn)
_, err = ctx.monitor.RegisterResourceOutputs(ctx.ctx, &pulumirpc.RegisterResourceOutputsRequest{
Urn: string(urn),
Outputs: outsMarshalled,
})
if err != nil {
return errors.Wrap(err, "registering outputs")
}
go func() {
// No matter the outcome, make sure all promises are resolved and that we've signaled completion of this RPC.
var err error
defer func() {
// Signal the completion of this RPC and notify any potential awaiters.
ctx.endRPC(err)
}()
logging.V(9).Infof("RegisterResourceOutputs(%s): success", urn)
urn, _, err := resource.URN().awaitURN(context.TODO())
if err != nil {
return
}
outsResolved, _, err := marshalInput(outs, anyType, true)
if err != nil {
return
}
keepUnknowns := ctx.DryRun()
outsMarshalled, err := plugin.MarshalProperties(
outsResolved.ObjectValue(),
plugin.MarshalOptions{KeepUnknowns: keepUnknowns})
if err != nil {
return
}
// Register the outputs
logging.V(9).Infof("RegisterResourceOutputs(%s): RPC call being made", urn)
_, err = ctx.monitor.RegisterResourceOutputs(ctx.ctx, &pulumirpc.RegisterResourceOutputsRequest{
Urn: string(urn),
Outputs: outsMarshalled,
})
logging.V(9).Infof("RegisterResourceOutputs(%s): %v", urn, err)
}()
// Signal the completion of this RPC and notify any potential awaiters.
ctx.endRPC()
return nil
}
// Export registers a key and value pair with the current context's stack.
func (ctx *Context) Export(name string, value interface{}) {
func (ctx *Context) Export(name string, value Input) {
ctx.exports[name] = value
}

215
sdk/go/pulumi/generate.go Normal file
View file

@ -0,0 +1,215 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build ignore
package main
import (
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"text/template"
"unicode"
)
type builtin struct {
Name string
Type string
inputType string
implements []string
Implements []*builtin
elementType string
Example string
}
func (b builtin) DefineInputType() bool {
return b.inputType == "" && b.Type != "AssetOrArchive"
}
func (b builtin) DefinePtrType() bool {
return strings.HasSuffix(b.Name, "Ptr")
}
func (b builtin) PtrType() string {
return b.inputType[1:]
}
func (b builtin) DefineInputMethods() bool {
return b.Type != "AssetOrArchive"
}
func (b builtin) DefineElem() bool {
return b.DefinePtrType()
}
func (b builtin) ElemReturnType() string {
return strings.TrimSuffix(b.Name, "Ptr")
}
func (b builtin) ElemElementType() string {
return strings.TrimPrefix(b.Type, "*")
}
func (b builtin) DefineIndex() bool {
return strings.HasSuffix(b.Name, "Array")
}
func (b builtin) IndexReturnType() string {
return strings.TrimSuffix(b.Name, "Array")
}
func (b builtin) IndexElementType() string {
return strings.TrimPrefix(b.elementType, "[]")
}
func (b builtin) DefineMapIndex() bool {
return strings.HasSuffix(b.Name, "Map")
}
func (b builtin) MapIndexElementType() string {
return strings.TrimPrefix(b.elementType, "map[string]")
}
func (b builtin) MapIndexReturnType() string {
return strings.TrimSuffix(b.Name, "Map")
}
func (b builtin) ElementType() string {
if b.elementType != "" {
return b.elementType
}
return b.Type
}
func (b builtin) InputType() string {
if b.inputType != "" {
return b.inputType
}
return b.Name
}
var builtins = makeBuiltins([]*builtin{
{Name: "Archive", Type: "Archive", inputType: "*archive", implements: []string{"AssetOrArchive"}, Example: "NewFileArchive(\"foo.zip\")"},
{Name: "Asset", Type: "Asset", inputType: "*asset", implements: []string{"AssetOrArchive"}, Example: "NewFileAsset(\"foo.txt\")"},
{Name: "AssetOrArchive", Type: "AssetOrArchive", Example: "NewFileArchive(\"foo.zip\")"},
{Name: "Bool", Type: "bool", Example: "Bool(true)"},
{Name: "Float32", Type: "float32", Example: "Float32(1.3)"},
{Name: "Float64", Type: "float64", Example: "Float64(999.9)"},
{Name: "ID", Type: "ID", inputType: "ID", implements: []string{"String"}, Example: "ID(\"foo\")"},
{Name: "Input", Type: "interface{}", Example: "String(\"any\")"},
{Name: "Int", Type: "int", Example: "Int(42)"},
{Name: "Int16", Type: "int16", Example: "Int16(33)"},
{Name: "Int32", Type: "int32", Example: "Int32(24)"},
{Name: "Int64", Type: "int64", Example: "Int64(15)"},
{Name: "Int8", Type: "int8", Example: "Int8(6)"},
{Name: "String", Type: "string", Example: "String(\"foo\")"},
{Name: "URN", Type: "URN", inputType: "URN", implements: []string{"String"}, Example: "URN(\"foo\")"},
{Name: "Uint", Type: "uint", Example: "Uint(42)"},
{Name: "Uint16", Type: "uint16", Example: "Uint16(33)"},
{Name: "Uint32", Type: "uint32", Example: "Uint32(24)"},
{Name: "Uint64", Type: "uint64", Example: "Uint64(15)"},
{Name: "Uint8", Type: "uint8", Example: "Uint8(6)"},
})
func unexported(s string) string {
runes := []rune(s)
allCaps := true
for _, r := range runes {
if !unicode.IsUpper(r) {
allCaps = false
break
}
}
if allCaps {
return strings.ToLower(s)
}
return string(append([]rune{unicode.ToLower(runes[0])}, runes[1:]...))
}
var funcs = template.FuncMap{
"Unexported": unexported,
}
func makeBuiltins(primitives []*builtin) []*builtin {
// Augment primitives with array and map types.
var builtins []*builtin
for _, p := range primitives {
name := ""
if p.Name != "Input" {
builtins = append(builtins, p)
name = p.Name
}
switch name {
case "Archive", "Asset", "AssetOrArchive", "":
// do nothing
default:
builtins = append(builtins, &builtin{Name: name + "Ptr", Type: "*" + p.Type, inputType: "*" + unexported(p.Type) + "Ptr", Example: fmt.Sprintf("%sPtr(%s(%s))", name, p.Type, p.Example)})
}
builtins = append(builtins, &builtin{Name: name + "Array", Type: "[]" + name + "Input", elementType: "[]" + p.Type, Example: fmt.Sprintf("%sArray{%s}", name, p.Example)})
builtins = append(builtins, &builtin{Name: name + "Map", Type: "map[string]" + name + "Input", elementType: "map[string]" + p.Type, Example: fmt.Sprintf("%sMap{\"baz\": %s}", name, p.Example)})
}
nameToBuiltin := map[string]*builtin{}
for _, b := range builtins {
nameToBuiltin[b.Name] = b
}
for _, b := range builtins {
for _, i := range b.implements {
b.Implements = append(b.Implements, nameToBuiltin[i])
}
}
return builtins
}
func main() {
templates, err := template.New("templates").Funcs(funcs).ParseGlob("./templates/*")
if err != nil {
log.Fatalf("failed to parse templates: %v", err)
}
data := map[string]interface{}{
"Builtins": builtins,
}
for _, t := range templates.Templates() {
filename := strings.TrimRight(t.Name(), ".template")
f, err := os.Create(filename)
if err != nil {
log.Fatalf("failed to create %v: %v", filename, err)
}
if err := t.Execute(f, data); err != nil {
log.Fatalf("failed to execute %v: %v", t.Name(), err)
}
f.Close()
gofmt := exec.Command("gofmt", "-s", "-w", filename)
stderr, err := gofmt.StderrPipe()
if err != nil {
log.Fatalf("failed to pipe stderr from gofmt: %v", err)
}
go func() {
io.Copy(os.Stderr, stderr)
}()
if err := gofmt.Run(); err != nil {
log.Fatalf("failed to gofmt %v: %v", filename, err)
}
}
}

173
sdk/go/pulumi/mocks.go Normal file
View file

@ -0,0 +1,173 @@
package pulumi
import (
"log"
"github.com/golang/protobuf/ptypes/empty"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/pulumi/pulumi/pkg/tokens"
"golang.org/x/net/context"
"google.golang.org/grpc"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
)
type MockResourceMonitor interface {
Call(token string, args resource.PropertyMap, provider string) (resource.PropertyMap, error)
NewResource(typeToken, name string, inputs resource.PropertyMap,
provider, id string) (string, resource.PropertyMap, error)
}
func WithMocks(project, stack string, mocks MockResourceMonitor) RunOption {
return func(info *RunInfo) {
info.Project, info.Stack, info.Mocks = project, stack, mocks
}
}
type mockMonitor struct {
project string
stack string
mocks MockResourceMonitor
}
func (m *mockMonitor) newURN(parent, typ, name string) string {
parentType := tokens.Type("")
if parentURN := resource.URN(parent); parentURN != "" && parentURN.Type() != resource.RootStackType {
parentType = parentURN.QualifiedType()
}
return string(resource.NewURN(tokens.QName(m.stack), tokens.PackageName(m.project), parentType, tokens.Type(typ),
tokens.QName(name)))
}
func (m *mockMonitor) SupportsFeature(ctx context.Context, in *pulumirpc.SupportsFeatureRequest,
opts ...grpc.CallOption) (*pulumirpc.SupportsFeatureResponse, error) {
return &pulumirpc.SupportsFeatureResponse{
HasSupport: true,
}, nil
}
func (m *mockMonitor) Invoke(ctx context.Context, in *pulumirpc.InvokeRequest,
opts ...grpc.CallOption) (*pulumirpc.InvokeResponse, error) {
args, err := plugin.UnmarshalProperties(in.GetArgs(), plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
resultV, err := m.mocks.Call(in.GetTok(), args, in.GetProvider())
if err != nil {
return nil, err
}
result, err := plugin.MarshalProperties(resultV, plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
return &pulumirpc.InvokeResponse{
Return: result,
}, nil
}
func (m *mockMonitor) StreamInvoke(ctx context.Context, in *pulumirpc.InvokeRequest,
opts ...grpc.CallOption) (pulumirpc.ResourceMonitor_StreamInvokeClient, error) {
panic("not implemented")
}
func (m *mockMonitor) ReadResource(ctx context.Context, in *pulumirpc.ReadResourceRequest,
opts ...grpc.CallOption) (*pulumirpc.ReadResourceResponse, error) {
stateIn, err := plugin.UnmarshalProperties(in.GetProperties(), plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
_, state, err := m.mocks.NewResource(in.GetType(), in.GetName(), stateIn, in.GetProvider(), in.GetId())
if err != nil {
return nil, err
}
stateOut, err := plugin.MarshalProperties(state, plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
return &pulumirpc.ReadResourceResponse{
Urn: m.newURN(in.GetParent(), in.GetType(), in.GetName()),
Properties: stateOut,
}, nil
}
func (m *mockMonitor) RegisterResource(ctx context.Context, in *pulumirpc.RegisterResourceRequest,
opts ...grpc.CallOption) (*pulumirpc.RegisterResourceResponse, error) {
if in.GetType() == string(resource.RootStackType) {
return &pulumirpc.RegisterResourceResponse{
Urn: m.newURN(in.GetParent(), in.GetType(), in.GetName()),
}, nil
}
inputs, err := plugin.UnmarshalProperties(in.GetObject(), plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
id, state, err := m.mocks.NewResource(in.GetType(), in.GetName(), inputs, in.GetProvider(), in.GetImportId())
if err != nil {
return nil, err
}
stateOut, err := plugin.MarshalProperties(state, plugin.MarshalOptions{KeepSecrets: true})
if err != nil {
return nil, err
}
return &pulumirpc.RegisterResourceResponse{
Urn: m.newURN(in.GetParent(), in.GetType(), in.GetName()),
Id: id,
Object: stateOut,
}, nil
}
func (m *mockMonitor) RegisterResourceOutputs(ctx context.Context, in *pulumirpc.RegisterResourceOutputsRequest,
opts ...grpc.CallOption) (*empty.Empty, error) {
return &empty.Empty{}, nil
}
type mockEngine struct {
logger *log.Logger
rootResource string
}
// Log logs a global message in the engine, including errors and warnings.
func (m *mockEngine) Log(ctx context.Context, in *pulumirpc.LogRequest,
opts ...grpc.CallOption) (*empty.Empty, error) {
if m.logger != nil {
m.logger.Printf("%s: %s", in.GetSeverity(), in.GetMessage())
}
return &empty.Empty{}, nil
}
// GetRootResource gets the URN of the root resource, the resource that should be the root of all
// otherwise-unparented resources.
func (m *mockEngine) GetRootResource(ctx context.Context, in *pulumirpc.GetRootResourceRequest,
opts ...grpc.CallOption) (*pulumirpc.GetRootResourceResponse, error) {
return &pulumirpc.GetRootResourceResponse{
Urn: m.rootResource,
}, nil
}
// SetRootResource sets the URN of the root resource.
func (m *mockEngine) SetRootResource(ctx context.Context, in *pulumirpc.SetRootResourceRequest,
opts ...grpc.CallOption) (*pulumirpc.SetRootResourceResponse, error) {
m.rootResource = in.GetUrn()
return &pulumirpc.SetRootResourceResponse{}, nil
}

24
sdk/go/pulumi/printf.go Normal file
View file

@ -0,0 +1,24 @@
package pulumi
import (
"fmt"
"io"
)
func Printf(format string, args ...interface{}) IntOutput {
return All(args...).ApplyT(func(args []interface{}) (int, error) {
return fmt.Printf(format, args...)
}).(IntOutput)
}
func Fprintf(w io.Writer, format string, args ...interface{}) IntOutput {
return All(args...).ApplyT(func(args []interface{}) (int, error) {
return fmt.Fprintf(w, format, args...)
}).(IntOutput)
}
func Sprintf(format string, args ...interface{}) StringOutput {
return All(args...).ApplyT(func(args []interface{}) string {
return fmt.Sprintf(format, args...)
}).(StringOutput)
}

View file

@ -0,0 +1,32 @@
package pulumi
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSprintfPrompt(t *testing.T) {
out := Sprintf("%v %v %v", "foo", 42, true)
v, known, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
assert.Equal(t, fmt.Sprintf("%v %v %v", "foo", 42, true), v)
}
func TestSprintfInputs(t *testing.T) {
out := Sprintf("%v %v %v", String("foo"), Int(42), Bool(true))
v, known, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
assert.Equal(t, fmt.Sprintf("%v %v %v", "foo", 42, true), v)
}
func TestSprintfOutputs(t *testing.T) {
out := Sprintf("%v %v %v", ToOutput("foo"), ToOutput(42), ToOutput(true))
v, known, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
assert.Equal(t, fmt.Sprintf("%v %v %v", "foo", 42, true), v)
}

View file

@ -1,597 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll
package pulumi
import (
"context"
"reflect"
"sync"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/go/pulumi/asset"
)
const (
outputPending = iota
outputResolved
outputRejected
)
// Output helps encode the relationship between resources in a Pulumi application. Specifically an output property
// holds onto a value and the resource it came from. An output value can then be provided when constructing new
// resources, allowing that new resource to know both the value as well as the resource the value came from. This
// allows for a precise "dependency graph" to be created, which properly tracks the relationship between resources.
type Output struct {
s *outputState // protect against value aliasing.
}
// outputState is a heap-allocated block of state for each output property, in case of aliasing.
type outputState struct {
mutex sync.Mutex
cond *sync.Cond
state uint32 // one of output{Pending,Resolved,Rejected}
value interface{} // the value of this output if it is resolved.
err error // the error associated with this output if it is rejected.
known bool // true if this output's value is known.
deps []Resource // the dependencies associated with this output property.
}
func (o *outputState) dependencies() []Resource {
if o == nil {
return nil
}
return o.deps
}
func (o *outputState) fulfill(value interface{}, known bool, err error) {
if o == nil {
return
}
o.mutex.Lock()
defer func() {
o.mutex.Unlock()
o.cond.Broadcast()
}()
if o.state != outputPending {
return
}
if err != nil {
o.state, o.err, o.known = outputRejected, err, true
} else {
o.state, o.value, o.known = outputResolved, value, known
}
}
func (o *outputState) resolve(value interface{}, known bool) {
o.fulfill(value, known, nil)
}
func (o *outputState) reject(err error) {
o.fulfill(nil, true, err)
}
func (o *outputState) await(ctx context.Context) (interface{}, bool, error) {
for {
if o == nil {
// If the state is nil, treat its value as resolved and unknown.
return nil, false, nil
}
o.mutex.Lock()
for o.state == outputPending {
if ctx.Err() != nil {
return nil, true, ctx.Err()
}
o.cond.Wait()
}
o.mutex.Unlock()
if !o.known || o.err != nil {
return nil, o.known, o.err
}
ov, ok := isOutput(o.value)
if !ok {
return o.value, true, nil
}
o = ov.s
}
}
func newOutput(deps ...Resource) Output {
out := Output{
s: &outputState{
deps: deps,
},
}
out.s.cond = sync.NewCond(&out.s.mutex)
return out
}
var outputType = reflect.TypeOf(Output{})
func isOutput(v interface{}) (Output, bool) {
if v != nil {
rv := reflect.ValueOf(v)
if rv.Type().ConvertibleTo(outputType) {
return rv.Convert(outputType).Interface().(Output), true
}
}
return Output{}, false
}
// 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() (Output, func(interface{}), func(error)) {
out := newOutput()
resolve := func(v interface{}) {
out.s.resolve(v, true)
}
reject := func(err error) {
out.s.reject(err)
}
return out, resolve, reject
}
// ApplyWithContext transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
func (out Output) Apply(applier func(v interface{}) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
// The provided context can be used to reject the output as canceled.
func (out Output) ApplyWithContext(ctx context.Context,
applier func(ctx context.Context, v interface{}) (interface{}, error)) Output {
result := newOutput(out.s.deps...)
go func() {
v, known, err := out.s.await(ctx)
if err != nil || !known {
result.s.fulfill(nil, known, err)
return
}
// If we have a known value, run the applier to transform it.
u, err := applier(ctx, v)
if err != nil {
result.s.reject(err)
return
}
// Fulfill the result.
result.s.fulfill(u, true, nil)
}()
return result
}
// Outputs is a map of property name to value, one for each resource output property.
type Outputs map[string]Output
// ArchiveOutput is an Output that is typed to return archive values.
type ArchiveOutput Output
var archiveType = reflect.TypeOf((*asset.Archive)(nil)).Elem()
// Apply applies a transformation to the archive value when it is available.
func (out ArchiveOutput) Apply(applier func(asset.Archive) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v asset.Archive) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out ArchiveOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, asset.Archive) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, archiveType).(asset.Archive))
})
}
// ArrayOutput is an Output that is typed to return arrays of values.
type ArrayOutput Output
var arrayType = reflect.TypeOf((*[]interface{})(nil)).Elem()
// Apply applies a transformation to the archive value when it is available.
func (out ArrayOutput) Apply(applier func([]interface{}) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v []interface{}) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out ArrayOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, []interface{}) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, arrayType).([]interface{}))
})
}
// AssetOutput is an Output that is typed to return asset values.
type AssetOutput Output
var assetType = reflect.TypeOf((*asset.Asset)(nil)).Elem()
// Apply applies a transformation to the archive value when it is available.
func (out AssetOutput) Apply(applier func(asset.Asset) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v asset.Asset) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out AssetOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, asset.Asset) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, assetType).(asset.Asset))
})
}
// BoolOutput is an Output that is typed to return bool values.
type BoolOutput Output
var boolType = reflect.TypeOf(false)
// Apply applies a transformation to the archive value when it is available.
func (out BoolOutput) Apply(applier func(bool) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v bool) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out BoolOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, bool) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, boolType).(bool))
})
}
// Float32Output is an Output that is typed to return float32 values.
type Float32Output Output
var float32Type = reflect.TypeOf(float32(0))
// Apply applies a transformation to the archive value when it is available.
func (out Float32Output) Apply(applier func(float32) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v float32) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Float32Output) ApplyWithContext(ctx context.Context, applier func(context.Context, float32) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, float32Type).(float32))
})
}
// Float64Output is an Output that is typed to return float64 values.
type Float64Output Output
var float64Type = reflect.TypeOf(float64(0))
// Apply applies a transformation to the archive value when it is available.
func (out Float64Output) Apply(applier func(float64) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v float64) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Float64Output) ApplyWithContext(ctx context.Context, applier func(context.Context, float64) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, float64Type).(float64))
})
}
// IDOutput is an Output that is typed to return ID values.
type IDOutput Output
var stringType = reflect.TypeOf("")
func (out IDOutput) await(ctx context.Context) (ID, bool, error) {
id, known, err := out.s.await(ctx)
if !known || err != nil {
return "", known, err
}
return ID(convert(id, stringType).(string)), true, nil
}
// Apply applies a transformation to the archive value when it is available.
func (out IDOutput) Apply(applier func(ID) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v ID) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out IDOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, ID) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, ID(convert(v, stringType).(string)))
})
}
// IntOutput is an Output that is typed to return int values.
type IntOutput Output
var intType = reflect.TypeOf(int(0))
// Apply applies a transformation to the archive value when it is available.
func (out IntOutput) Apply(applier func(int) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v int) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out IntOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, int) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, intType).(int))
})
}
// Int8Output is an Output that is typed to return int8 values.
type Int8Output Output
var int8Type = reflect.TypeOf(int8(0))
// Apply applies a transformation to the archive value when it is available.
func (out Int8Output) Apply(applier func(int8) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v int8) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Int8Output) ApplyWithContext(ctx context.Context, applier func(context.Context, int8) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, int8Type).(int8))
})
}
// Int16Output is an Output that is typed to return int16 values.
type Int16Output Output
var int16Type = reflect.TypeOf(int16(0))
// Apply applies a transformation to the archive value when it is available.
func (out Int16Output) Apply(applier func(int16) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v int16) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Int16Output) ApplyWithContext(ctx context.Context, applier func(context.Context, int16) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, int16Type).(int16))
})
}
// Int32Output is an Output that is typed to return int32 values.
type Int32Output Output
var int32Type = reflect.TypeOf(int32(0))
// Apply applies a transformation to the archive value when it is available.
func (out Int32Output) Apply(applier func(int32) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v int32) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Int32Output) ApplyWithContext(ctx context.Context, applier func(context.Context, int32) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, int32Type).(int32))
})
}
// Int64Output is an Output that is typed to return int64 values.
type Int64Output Output
var int64Type = reflect.TypeOf(int64(0))
// Apply applies a transformation to the archive value when it is available.
func (out Int64Output) Apply(applier func(int64) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v int64) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Int64Output) ApplyWithContext(ctx context.Context, applier func(context.Context, int64) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, int64Type).(int64))
})
}
// MapOutput is an Output that is typed to return map values.
type MapOutput Output
var mapType = reflect.TypeOf(map[string]interface{}{})
// Apply applies a transformation to the number value when it is available.
func (out MapOutput) Apply(applier func(map[string]interface{}) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v map[string]interface{}) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the number value when it is available.
func (out MapOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, map[string]interface{}) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, mapType).(map[string]interface{}))
})
}
// StringOutput is an Output that is typed to return number values.
type StringOutput Output
// Apply applies a transformation to the archive value when it is available.
func (out StringOutput) Apply(applier func(string) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v string) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out StringOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, string) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, stringType).(string))
})
}
// UintOutput is an Output that is typed to return uint values.
type UintOutput Output
var uintType = reflect.TypeOf(uint(0))
// Apply applies a transformation to the archive value when it is available.
func (out UintOutput) Apply(applier func(uint) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v uint) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out UintOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, uint) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, uintType).(uint))
})
}
// Uint8Output is an Output that is typed to return uint8 values.
type Uint8Output Output
var uint8Type = reflect.TypeOf(uint8(0))
// Apply applies a transformation to the archive value when it is available.
func (out Uint8Output) Apply(applier func(uint8) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v uint8) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Uint8Output) ApplyWithContext(ctx context.Context, applier func(context.Context, uint8) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, uint8Type).(uint8))
})
}
// Uint16Output is an Output that is typed to return uint16 values.
type Uint16Output Output
var uint16Type = reflect.TypeOf(uint16(0))
// Apply applies a transformation to the archive value when it is available.
func (out Uint16Output) Apply(applier func(uint16) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v uint16) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Uint16Output) ApplyWithContext(ctx context.Context, applier func(context.Context, uint16) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, uint16Type).(uint16))
})
}
// Uint32Output is an Output that is typed to return uint32 values.
type Uint32Output Output
var uint32Type = reflect.TypeOf(uint32(0))
// Apply applies a transformation to the archive value when it is available.
func (out Uint32Output) Apply(applier func(uint32) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v uint32) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Uint32Output) ApplyWithContext(ctx context.Context, applier func(context.Context, uint32) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, uint32Type).(uint32))
})
}
// Uint64Output is an Output that is typed to return uint64 values.
type Uint64Output Output
var uint64Type = reflect.TypeOf(uint64(0))
// Apply applies a transformation to the archive value when it is available.
func (out Uint64Output) Apply(applier func(uint64) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v uint64) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out Uint64Output) ApplyWithContext(ctx context.Context, applier func(context.Context, uint64) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, convert(v, uint64Type).(uint64))
})
}
// URNOutput is an Output that is typed to return URN values.
type URNOutput Output
func (out URNOutput) await(ctx context.Context) (URN, bool, error) {
urn, known, err := out.s.await(ctx)
if !known || err != nil {
return "", known, err
}
return URN(convert(urn, stringType).(string)), true, nil
}
// Apply applies a transformation to the archive value when it is available.
func (out URNOutput) Apply(applier func(URN) (interface{}, error)) Output {
return out.ApplyWithContext(context.Background(), func(_ context.Context, v URN) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext applies a transformation to the archive value when it is available.
func (out URNOutput) ApplyWithContext(ctx context.Context, applier func(context.Context, URN) (interface{}, error)) Output {
return Output(out).ApplyWithContext(ctx, func(ctx context.Context, v interface{}) (interface{}, error) {
return applier(ctx, URN(convert(v, stringType).(string)))
})
}
func convert(v interface{}, to reflect.Type) interface{} {
rv := reflect.ValueOf(v)
if !rv.Type().ConvertibleTo(to) {
panic(errors.Errorf("cannot convert output value of type %s to %s", rv.Type(), to))
}
return rv.Convert(to).Interface()
}

View file

@ -1,270 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
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()
go func() {
resolve(42)
}()
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()
go func() {
reject(errors.New("boom"))
}()
v, _, err := out.s.await(context.Background())
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
func TestArrayOutputs(t *testing.T) {
out, resolve, _ := NewOutput()
go func() {
resolve([]interface{}{nil, 0, "x"})
}()
{
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()
go func() {
resolve(true)
}()
{
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()
go func() {
resolve(map[string]interface{}{
"x": 1,
"y": false,
"z": "abc",
})
}()
{
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()
go func() {
resolve(42.345)
}()
{
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()
go func() {
resolve("a stringy output")
}()
{
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()
go func() {
other, resolveOther, _ := NewOutput()
resolve(other)
go func() { resolveOther(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()
go func() {
other, _, rejectOther := NewOutput()
resolve(other)
go func() { rejectOther(errors.New("boom")) }()
}()
v, _, err := out.s.await(context.Background())
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
func TestOutputApply(t *testing.T) {
// Test that resolved outputs lead to applies being run.
{
out, resolve, _ := NewOutput()
go func() { resolve(42) }()
var ranApp bool
b := IntOutput(out)
app := b.Apply(func(v int) (interface{}, error) {
ranApp = true
return v + 1, 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, 43)
}
// Test that resolved, but unknown outputs, skip the running of applies.
{
out := newOutput()
go func() { out.s.fulfill(42, false, nil) }()
var ranApp bool
b := IntOutput(out)
app := b.Apply(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
_, 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()
go func() { reject(errors.New("boom")) }()
var ranApp bool
b := IntOutput(out)
app := b.Apply(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
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()
go func() { resolve(42) }()
var ranApp bool
b := IntOutput(out)
app := b.Apply(func(v int) (interface{}, error) {
other, resolveOther, _ := NewOutput()
go func() { resolveOther(v + 1) }()
ranApp = true
return 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, 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()
go func() { resolve(42) }()
var ranApp bool
b := IntOutput(out)
app := b.Apply(func(v int) (interface{}, error) {
other, _, rejectOther := NewOutput()
go func() { rejectOther(errors.New("boom")) }()
ranApp = true
return other, nil
})
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

@ -14,6 +14,8 @@
package pulumi
import "reflect"
type (
// ID is a unique identifier assigned by a resource provider to a resource.
ID string
@ -21,10 +23,63 @@ type (
URN string
)
var resourceStateType = reflect.TypeOf(ResourceState{})
var customResourceStateType = reflect.TypeOf(CustomResourceState{})
var providerResourceStateType = reflect.TypeOf(ProviderResourceState{})
// ResourceState is the base
type ResourceState struct {
urn URNOutput `pulumi:"urn"`
providers map[string]ProviderResource
}
func (s ResourceState) URN() URNOutput {
return s.urn
}
func (s ResourceState) GetProvider(token string) ProviderResource {
return s.providers[getPackage(token)]
}
func (s ResourceState) getProviders() map[string]ProviderResource {
return s.providers
}
func (ResourceState) isResource() {}
type CustomResourceState struct {
ResourceState
id IDOutput `pulumi:"id"`
}
func (s CustomResourceState) ID() IDOutput {
return s.id
}
func (CustomResourceState) isCustomResource() {}
type ProviderResourceState struct {
CustomResourceState
pkg string
}
func (s ProviderResourceState) getPackage() string {
return s.pkg
}
// 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
// getProviders returns the provider map for this resource.
getProviders() map[string]ProviderResource
// isResource() is a marker method used to ensure that all Resource types embed a ResourceState.
isResource()
}
// CustomResource is a cloud resource whose create, read, update, and delete (CRUD) operations are managed by performing
@ -35,6 +90,8 @@ type CustomResource interface {
// 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
isCustomResource()
}
// ComponentResource is a resource that aggregates one or more other child resources into a higher level abstraction.
@ -48,10 +105,17 @@ type ComponentResource interface {
// be used for a given resource by passing it in ResourceOpt.Provider.
type ProviderResource interface {
CustomResource
getPackage() string
}
// ResourceOpt contains optional settings that control a resource's behavior.
type ResourceOpt struct {
type CustomTimeouts struct {
Create string
Update string
Delete string
}
type resourceOptions struct {
// Parent is an optional parent resource to which this resource belongs.
Parent Resource
// DependsOn is an optional array of explicit dependencies on other resources.
@ -68,23 +132,137 @@ type ResourceOpt struct {
// the cloud resource with the given ID. The inputs to the resource's constructor must align with the resource's
// current state. Once a resource has been imported, the import property must be removed from the resource's
// options.
Import ID
Import IDInput
// CustomTimeouts is an optional configuration block used for CRUD operations
CustomTimeouts *CustomTimeouts
// Ignore changes to any of the specified properties.
IgnoreChanges []string
}
// InvokeOpt contains optional settings that control an invoke's behavior.
type InvokeOpt struct {
type invokeOptions struct {
// Parent is an optional parent resource to use for default provider options for this invoke.
Parent Resource
// Provider is an optional provider resource to use for this invoke.
Provider ProviderResource
}
type CustomTimeouts struct {
Create string
Update string
Delete string
type ResourceOption interface {
applyResourceOption(*resourceOptions)
}
type InvokeOption interface {
applyInvokeOption(*invokeOptions)
}
type ResourceOrInvokeOption interface {
ResourceOption
InvokeOption
}
type resourceOption func(*resourceOptions)
func (o resourceOption) applyResourceOption(opts *resourceOptions) {
o(opts)
}
type resourceOrInvokeOption func(ro *resourceOptions, io *invokeOptions)
func (o resourceOrInvokeOption) applyResourceOption(opts *resourceOptions) {
o(opts, nil)
}
func (o resourceOrInvokeOption) applyInvokeOption(opts *invokeOptions) {
o(nil, opts)
}
// Parent sets the parent resource to which this resource or invoke belongs.
func Parent(r Resource) ResourceOrInvokeOption {
return resourceOrInvokeOption(func(ro *resourceOptions, io *invokeOptions) {
switch {
case ro != nil:
ro.Parent = r
case io != nil:
io.Parent = r
}
})
}
// Provider sets the provider resource to use for a resource's CRUD operations or an invoke's call.
func Provider(r ProviderResource) ResourceOrInvokeOption {
return resourceOrInvokeOption(func(ro *resourceOptions, io *invokeOptions) {
switch {
case ro != nil:
ro.Provider = r
case io != nil:
io.Provider = r
}
})
}
// DependsOn is an optional array of explicit dependencies on other resources.
func DependsOn(o []Resource) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.DependsOn = append(ro.DependsOn, o...)
})
}
// Protect, when set to true, ensures that this resource cannot be deleted (without first setting it to false).
func Protect(o bool) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.Protect = o
})
}
// Providers is an optional list of providers to use for a resource's children.
func Providers(o ...ProviderResource) ResourceOption {
m := map[string]ProviderResource{}
for _, p := range o {
m[p.getPackage()] = p
}
return ProviderMap(m)
}
// ProviderMap is an optional map of package to provider resource for a component resource.
func ProviderMap(o map[string]ProviderResource) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
if o != nil {
if ro.Providers == nil {
ro.Providers = make(map[string]ProviderResource)
}
for k, v := range o {
ro.Providers[k] = v
}
}
})
}
// DeleteBeforeReplace, when set to true, ensures that this resource is deleted prior to replacement.
func DeleteBeforeReplace(o bool) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.DeleteBeforeReplace = o
})
}
// Import, when provided with a resource ID, indicates that this resource's provider should import its state from
// the cloud resource with the given ID. The inputs to the resource's constructor must align with the resource's
// current state. Once a resource has been imported, the import property must be removed from the resource's
// options.
func Import(o IDInput) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.Import = o
})
}
// Timeouts is an optional configuration block used for CRUD operations
func Timeouts(o *CustomTimeouts) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.CustomTimeouts = o
})
}
// Ignore changes to any of the specified properties.
func IgnoreChanges(o []string) ResourceOption {
return resourceOption(func(ro *resourceOptions) {
ro.IgnoreChanges = o
})
}

View file

@ -17,307 +17,521 @@ package pulumi
import (
"reflect"
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"
"github.com/pulumi/pulumi/sdk/go/pulumi/asset"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// marshalInputs turns resource property inputs into a gRPC struct suitable for marshaling.
func marshalInputs(props map[string]interface{},
keepUnknowns bool) (*structpb.Struct, map[string][]URN, []URN, error) {
func mapStructTypes(from, to reflect.Type) func(reflect.Value, int) (reflect.StructField, reflect.Value) {
contract.Assert(from.Kind() == reflect.Struct)
contract.Assert(to.Kind() == reflect.Struct)
var depURNs []URN
pmap, pdeps := make(map[string]interface{}), make(map[string][]URN)
for key := range props {
// Get the underlying value, possibly waiting for an output to arrive.
v, resourceDeps, err := marshalInput(props[key])
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "awaiting input property %s", key)
if from == to {
return func(v reflect.Value, i int) (reflect.StructField, reflect.Value) {
if !v.IsValid() {
return to.Field(i), reflect.Value{}
}
return to.Field(i), v.Field(i)
}
}
nameToIndex := map[string]int{}
numFields := to.NumField()
for i := 0; i < numFields; i++ {
nameToIndex[to.Field(i).Name] = i
}
return func(v reflect.Value, i int) (reflect.StructField, reflect.Value) {
fieldName := from.Field(i).Name
j, ok := nameToIndex[fieldName]
if !ok {
panic(errors.Errorf("unknown field %v when marshaling inputs of type %v to %v", fieldName, from, to))
}
pmap[key] = v
field := to.Field(j)
if !v.IsValid() {
return field, reflect.Value{}
}
return field, v.Field(j)
}
}
// marshalInputs turns resource property inputs into a map suitable for marshaling.
func marshalInputs(props Input) (resource.PropertyMap, map[string][]URN, []URN, error) {
var depURNs []URN
depset := map[URN]bool{}
pmap, pdeps := resource.PropertyMap{}, map[string][]URN{}
if props == nil {
return pmap, pdeps, depURNs, nil
}
pv := reflect.ValueOf(props)
if pv.Kind() == reflect.Ptr {
pv = pv.Elem()
}
pt := pv.Type()
contract.Assert(pt.Kind() == reflect.Struct)
// We use the resolved type to decide how to convert inputs to outputs.
rt := props.ElementType()
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
contract.Assert(rt.Kind() == reflect.Struct)
getMappedField := mapStructTypes(pt, rt)
// Now, marshal each field in the input.
numFields := pt.NumField()
for i := 0; i < numFields; i++ {
destField, _ := getMappedField(reflect.Value{}, i)
tag := destField.Tag.Get("pulumi")
if tag == "" {
continue
}
// Get the underlying value, possibly waiting for an output to arrive.
v, resourceDeps, err := marshalInput(pv.Field(i).Interface(), destField.Type, true)
if err != nil {
return nil, nil, nil, errors.Wrapf(err, "awaiting input property %s", tag)
}
pmap[resource.PropertyKey(tag)] = v
// Record all dependencies accumulated from reading this property.
deps := make([]URN, 0, len(resourceDeps))
var deps []URN
pdepset := map[URN]bool{}
for _, dep := range resourceDeps {
depURN, _, err := dep.URN().await(context.TODO())
depURN, _, err := dep.URN().awaitURN(context.TODO())
if err != nil {
return nil, nil, nil, err
}
deps = append(deps, depURN)
if !pdepset[depURN] {
deps = append(deps, depURN)
pdepset[depURN] = true
}
if !depset[depURN] {
depURNs = append(depURNs, depURN)
depset[depURN] = true
}
}
if len(deps) > 0 {
pdeps[tag] = deps
}
pdeps[key] = deps
depURNs = append(depURNs, deps...)
}
// Marshal all properties for the RPC call.
m, err := plugin.MarshalProperties(
resource.NewPropertyMapFromMap(pmap),
plugin.MarshalOptions{KeepUnknowns: keepUnknowns},
)
return m, pdeps, depURNs, err
return pmap, pdeps, depURNs, nil
}
// `gosec` thinks these are credentials, but they are not.
// nolint: gosec
const (
rpcTokenSpecialSigKey = "4dabf18193072939515e22adb298388d"
rpcTokenSpecialAssetSig = "c44067f5952c0a294b673a41bacd8c17"
rpcTokenSpecialArchiveSig = "0def7320c3a5731c473e5ecbe6d01bc7"
rpcTokenSpecialSecretSig = "1b47061264138c4ac30d75fd1eb44270"
rpcTokenUnknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9"
)
const rpcTokenUnknownValue = "04da6b54-80e4-46f7-96ec-b56ff0331ba9"
const cannotAwaitFmt = "cannot marshal Output value of type %T; please use Apply to access the Output's value"
// marshalInput marshals an input value, returning its raw serializable value along with any dependencies.
func marshalInput(v interface{}) (interface{}, []Resource, error) {
func marshalInput(v interface{}, destType reflect.Type, await bool) (resource.PropertyValue, []Resource, error) {
for {
// If v is nil, just return that.
if v == nil {
return nil, nil, nil
return resource.PropertyValue{}, nil, nil
}
// If this is an Output, recurse.
if out, ok := isOutput(v); ok {
return marshalInputOutput(out)
valueType := reflect.TypeOf(v)
// If this is an Input, make sure it is of the proper type and await it if it is an output/
var deps []Resource
if input, ok := v.(Input); ok {
valueType = input.ElementType()
// If the element type of the input is not identical to the type of the destination and the destination is
// not the any type (i.e. interface{}), attempt to convert the input to an appropriately-typed output.
if valueType != destType && destType != anyType {
if newOutput, ok := callToOutputMethod(context.TODO(), reflect.ValueOf(input), destType); ok {
// We were able to convert the input. Use the result as the new input value.
input, valueType = newOutput, destType
} else if !valueType.AssignableTo(destType) {
err := errors.Errorf("cannot marshal an input of type %T as a value of type %v", input, destType)
return resource.PropertyValue{}, nil, err
}
}
// If the input is an Output, await its value. The returned value is fully resolved.
if output, ok := input.(Output); ok {
if !await {
return resource.PropertyValue{}, nil, errors.Errorf(cannotAwaitFmt, output)
}
// Await the output.
ov, known, err := output.await(context.TODO())
if err != nil {
return resource.PropertyValue{}, nil, err
}
// If the value is unknown, return the appropriate sentinel.
if !known {
return resource.MakeComputed(resource.NewStringProperty("")), output.dependencies(), nil
}
v, deps = ov, output.dependencies()
}
}
// Next, look for some well known types.
// 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:
case *asset:
return resource.NewAssetProperty(&resource.Asset{
Path: v.Path(),
Text: v.Text(),
URI: v.URI(),
}), deps, nil
case *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)
aa, _, err := marshalInput(a, anyType, await)
if err != nil {
return nil, nil, err
return resource.PropertyValue{}, nil, err
}
assets[k] = aa
assets[k] = aa.V
}
}
return map[string]interface{}{
rpcTokenSpecialSigKey: rpcTokenSpecialAssetSig,
"assets": assets,
"path": v.Path(),
"uri": v.URI(),
}, nil, nil
return resource.NewArchiveProperty(&resource.Archive{
Assets: assets,
Path: v.Path(),
URI: v.URI(),
}), deps, nil
case CustomResource:
deps = append(deps, v)
// Resources aren't serializable; instead, serialize a reference to ID, tracking as a dependency.
e, d, err := marshalInput(v.ID())
e, d, err := marshalInput(v.ID(), idType, await)
if err != nil {
return nil, nil, err
return resource.PropertyValue{}, nil, err
}
return e, append([]Resource{v}, d...), nil
return e, append(deps, d...), nil
}
contract.Assertf(valueType.AssignableTo(destType), "%v: cannot assign %v to %v", v, valueType, destType)
if destType.Kind() == reflect.Interface {
destType = valueType
}
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
return resource.NewBoolProperty(rv.Bool()), deps, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return resource.NewNumberProperty(float64(rv.Int())), deps, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return resource.NewNumberProperty(float64(rv.Uint())), deps, nil
case reflect.Float32, reflect.Float64:
return resource.NewNumberProperty(rv.Float()), deps, nil
case reflect.Ptr, reflect.Interface:
// Dereference non-nil pointers and interfaces.
if rv.IsNil() {
return nil, nil, nil
return resource.PropertyValue{}, deps, nil
}
rv = rv.Elem()
v, destType = rv.Elem().Interface(), destType.Elem()
continue
case reflect.String:
return resource.NewStringProperty(rv.String()), deps, nil
case reflect.Array, reflect.Slice:
if rv.IsNil() {
return resource.PropertyValue{}, deps, nil
}
destElem := destType.Elem()
// If an array or a slice, create a new array by recursing into elements.
var arr []interface{}
var deps []Resource
var arr []resource.PropertyValue
for i := 0; i < rv.Len(); i++ {
elem := rv.Index(i)
e, d, err := marshalInput(elem.Interface())
e, d, err := marshalInput(elem.Interface(), destElem, await)
if err != nil {
return nil, nil, err
return resource.PropertyValue{}, nil, err
}
if !e.IsNull() {
arr = append(arr, e)
}
arr = append(arr, e)
deps = append(deps, d...)
}
return arr, deps, nil
return resource.NewArrayProperty(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
}
if rv.Type().Key().Kind() != reflect.String {
return resource.PropertyValue{}, nil,
errors.Errorf("expected map keys to be strings; got %v", rv.Type().Key())
}
obj[k] = mv
if rv.IsNil() {
return resource.PropertyValue{}, deps, nil
}
destElem := destType.Elem()
// For maps, only support string-based keys, and recurse into the values.
obj := resource.PropertyMap{}
for _, key := range rv.MapKeys() {
value := rv.MapIndex(key)
mv, d, err := marshalInput(value.Interface(), destElem, await)
if err != nil {
return resource.PropertyValue{}, nil, err
}
if !mv.IsNull() {
obj[resource.PropertyKey(key.String())] = 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)
}
v = rv.Interface()
}
return resource.NewObjectProperty(obj), deps, nil
case reflect.Struct:
obj := resource.PropertyMap{}
typ := rv.Type()
getMappedField := mapStructTypes(typ, destType)
for i := 0; i < typ.NumField(); i++ {
destField, _ := getMappedField(reflect.Value{}, i)
tag := destField.Tag.Get("pulumi")
if tag == "" {
continue
}
fv, d, err := marshalInput(rv.Field(i).Interface(), destField.Type, await)
if err != nil {
return resource.PropertyValue{}, nil, err
}
if !fv.IsNull() {
obj[resource.PropertyKey(tag)] = fv
}
deps = append(deps, d...)
}
return resource.NewObjectProperty(obj), deps, nil
}
return resource.PropertyValue{}, nil, errors.Errorf("unrecognized input property type: %v (%T)", v, v)
}
}
func marshalInputOutput(out Output) (interface{}, []Resource, error) {
// Await the value and return its raw value.
ov, known, err := out.s.await(context.TODO())
if err != nil {
return nil, nil, err
}
// If the value is known, marshal it.
if known {
e, d, merr := marshalInput(ov)
if merr != nil {
return nil, nil, merr
}
return e, append(out.s.dependencies(), d...), nil
}
// Otherwise, simply return the unknown value sentinel.
return rpcTokenUnknownValue, out.s.dependencies(), nil
}
// unmarshalOutputs unmarshals all the outputs into a simple map.
func unmarshalOutputs(outs *structpb.Struct) (map[string]interface{}, error) {
outprops, err := plugin.UnmarshalProperties(outs, plugin.MarshalOptions{})
if err != nil {
return nil, err
}
result := make(map[string]interface{})
for k, v := range outprops.Mappable() {
result[k], err = unmarshalOutput(v)
if err != nil {
return nil, err
}
}
return result, nil
}
// 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 {
func unmarshalPropertyValue(v resource.PropertyValue) (interface{}, error) {
switch {
case v.IsComputed() || v.IsOutput():
return nil, nil
case v.IsSecret():
return nil, errors.New("this version of the Pulumi SDK does not support first-class secrets")
case v.IsArray():
arr := v.ArrayValue()
rv := make([]interface{}, len(arr))
for i, e := range arr {
ev, err := unmarshalPropertyValue(e)
if err != nil {
return nil, err
}
rv[i] = ev
}
return rv, nil
case v.IsObject():
m := make(map[string]interface{})
for k, e := range v.ObjectValue() {
ev, err := unmarshalPropertyValue(e)
if err != nil {
return nil, err
}
m[string(k)] = ev
}
return m, nil
case v.IsAsset():
asset := v.AssetValue()
switch {
case asset.IsPath():
return NewFileAsset(asset.Path), nil
case asset.IsText():
return NewStringAsset(asset.Text), nil
case asset.IsURI():
return NewRemoteAsset(asset.URI), nil
}
return nil, errors.New("expected asset to be one of File, String, or Remote; got none")
case v.IsArchive():
archive := v.ArchiveValue()
switch {
case archive.IsAssets():
as := make(map[string]interface{})
for k, v := range archive.Assets {
a, err := unmarshalPropertyValue(resource.NewPropertyValue(v))
if err != nil {
return nil, err
}
as[k] = a
}
return NewAssetArchive(as), nil
case archive.IsPath():
return NewFileArchive(archive.Path), nil
case archive.IsURI():
return NewRemoteArchive(archive.URI), nil
default:
}
return nil, errors.New("expected asset to be one of File, String, or Remote; got none")
default:
return v.V, nil
}
}
// unmarshalOutput unmarshals a single output variable into its runtime representation.
func unmarshalOutput(v resource.PropertyValue, dest reflect.Value) error {
contract.Assert(dest.CanSet())
// Check for nils and unknowns. The destination will be left with the zero value.
if v.IsNull() || v.IsComputed() || v.IsOutput() {
return nil
}
// Allocate storage as necessary.
for dest.Kind() == reflect.Ptr {
elem := reflect.New(dest.Type().Elem())
dest.Set(elem)
dest = elem.Elem()
}
// In the case of assets and archives, turn these into real asset and archive structures.
if m, ok := v.(map[string]interface{}); ok {
if sig, hasSig := m[rpcTokenSpecialSigKey]; hasSig {
switch sig {
case rpcTokenSpecialAssetSig:
if path := m["path"]; path != nil {
return asset.NewFileAsset(cast.ToString(path)), nil
} else if text := m["text"]; text != nil {
return asset.NewStringAsset(cast.ToString(text)), nil
} else if uri := m["uri"]; uri != nil {
return asset.NewRemoteAsset(cast.ToString(uri)), nil
}
return nil, errors.New("expected asset to be one of File, String, or Remote; got none")
case rpcTokenSpecialArchiveSig:
if assets := m["assets"]; assets != nil {
as := make(map[string]interface{})
for k, v := range assets.(map[string]interface{}) {
a, err := unmarshalOutput(v)
if err != nil {
return nil, err
}
as[k] = a
}
return asset.NewAssetArchive(as), nil
} else if path := m["path"]; path != nil {
return asset.NewFileArchive(cast.ToString(path)), nil
} else if uri := m["uri"]; uri != nil {
return asset.NewRemoteArchive(cast.ToString(uri)), nil
}
return nil, errors.New("expected asset to be one of File, String, or Remote; got none")
case rpcTokenSpecialSecretSig:
return nil, errors.New("this version of the Pulumi SDK does not support first-class secrets")
default:
return nil, errors.Errorf("unrecognized signature '%v' in output value", sig)
}
switch {
case v.IsAsset():
if !assetType.AssignableTo(dest.Type()) {
return errors.Errorf("expected a %s, got an asset", dest.Type())
}
asset, err := unmarshalPropertyValue(v)
if err != nil {
return err
}
dest.Set(reflect.ValueOf(asset))
return nil
case v.IsArchive():
if !archiveType.AssignableTo(dest.Type()) {
return errors.Errorf("expected a %s, got an archive", dest.Type())
}
archive, err := unmarshalPropertyValue(v)
if err != nil {
return err
}
dest.Set(reflect.ValueOf(archive))
return nil
case v.IsSecret():
return errors.New("this version of the Pulumi SDK does not support first-class secrets")
}
// For arrays and maps, just make sure to transform them deeply.
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{}
for i := 0; i < rv.Len(); i++ {
elem := rv.Index(i)
e, err := unmarshalOutput(elem.Interface())
if err != nil {
return nil, err
}
arr = append(arr, e)
// Unmarshal based on the desired type.
switch dest.Kind() {
case reflect.Bool:
if !v.IsBool() {
return errors.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
return arr, nil
dest.SetBool(v.BoolValue())
return nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if !v.IsNumber() {
return errors.Errorf("expected an %v, got a %s", dest.Type(), v.TypeString())
}
dest.SetInt(int64(v.NumberValue()))
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if !v.IsNumber() {
return errors.Errorf("expected an %v, got a %s", dest.Type(), v.TypeString())
}
dest.SetUint(uint64(v.NumberValue()))
return nil
case reflect.Float32, reflect.Float64:
if !v.IsNumber() {
return errors.Errorf("expected an %v, got a %s", dest.Type(), v.TypeString())
}
dest.SetFloat(v.NumberValue())
return nil
case reflect.String:
if !v.IsString() {
return errors.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
dest.SetString(v.StringValue())
return nil
case reflect.Slice:
if !v.IsArray() {
return errors.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
arr := v.ArrayValue()
slice := reflect.MakeSlice(dest.Type(), len(arr), len(arr))
for i, e := range arr {
if err := unmarshalOutput(e, slice.Index(i)); err != nil {
return err
}
}
dest.Set(slice)
return nil
case reflect.Map:
// For maps, only support string-based keys, and recurse into the values.
obj := make(map[string]interface{})
for _, key := range rv.MapKeys() {
k, ok := key.Interface().(string)
if !ok {
return nil, errors.Errorf("expected map keys to be strings; got %v", reflect.TypeOf(key.Interface()))
}
value := rv.MapIndex(key)
mv, err := unmarshalOutput(value)
if err != nil {
return nil, err
}
obj[k] = mv
if !v.IsObject() {
return errors.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
return obj, nil
}
return v, nil
keyType, elemType := dest.Type().Key(), dest.Type().Elem()
if keyType.Kind() != reflect.String {
return errors.Errorf("map keys must be assignable from type string")
}
result := reflect.MakeMap(dest.Type())
for k, e := range v.ObjectValue() {
elem := reflect.New(elemType).Elem()
if err := unmarshalOutput(e, elem); err != nil {
return err
}
key := reflect.New(keyType).Elem()
key.SetString(string(k))
result.SetMapIndex(key, elem)
}
dest.Set(result)
return nil
case reflect.Interface:
if !anyType.Implements(dest.Type()) {
return errors.Errorf("cannot unmarshal into non-empty interface type %v", dest.Type())
}
// If we're unmarshaling into the empty interface type, use the property type as the type of the result.
result, err := unmarshalPropertyValue(v)
if err != nil {
return err
}
dest.Set(reflect.ValueOf(result))
return nil
case reflect.Struct:
if !v.IsObject() {
return errors.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
obj := v.ObjectValue()
typ := dest.Type()
for i := 0; i < typ.NumField(); i++ {
fieldV := dest.Field(i)
if !fieldV.CanSet() {
continue
}
tag := typ.Field(i).Tag.Get("pulumi")
if tag == "" {
continue
}
e, ok := obj[resource.PropertyKey(tag)]
if !ok {
continue
}
if err := unmarshalOutput(e, fieldV); err != nil {
return err
}
}
return nil
default:
return errors.Errorf("cannot unmarshal into type %v", dest.Type())
}
}

View file

@ -12,71 +12,115 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: unused,deadcode
package pulumi
import (
"reflect"
"testing"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/sdk/go/pulumi/asset"
)
type test struct {
S string `pulumi:"s"`
A bool `pulumi:"a"`
B int `pulumi:"b"`
StringAsset Asset `pulumi:"cStringAsset"`
FileAsset Asset `pulumi:"cFileAsset"`
RemoteAsset Asset `pulumi:"cRemoteAsset"`
AssetArchive Archive `pulumi:"dAssetArchive"`
FileArchive Archive `pulumi:"dFileArchive"`
RemoteArchive Archive `pulumi:"dRemoteArchive"`
E interface{} `pulumi:"e"`
Array []interface{} `pulumi:"fArray"`
Map map[string]interface{} `pulumi:"fMap"`
G string `pulumi:"g"`
H string `pulumi:"h"`
I string `pulumi:"i"`
}
type testInputs struct {
S StringInput
A BoolInput
B IntInput
StringAsset AssetInput
FileAsset AssetInput
RemoteAsset AssetInput
AssetArchive ArchiveInput
FileArchive ArchiveInput
RemoteArchive ArchiveInput
E Input
Array ArrayInput
Map MapInput
G StringInput
H StringInput
I StringInput
}
func (testInputs) ElementType() reflect.Type {
return reflect.TypeOf(test{})
}
// 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()
resolve("outputty")
out2 := newOutput()
out2.s.fulfill(nil, false, nil)
out3 := Output{}
input := map[string]interface{}{
"s": "a string",
"a": true,
"b": 42,
"cStringAsset": asset.NewStringAsset("put a lime in the coconut"),
"cFileAsset": asset.NewFileAsset("foo.txt"),
"cRemoteAsset": asset.NewRemoteAsset("https://pulumi.com/fake/asset.txt"),
"dAssetArchive": asset.NewAssetArchive(map[string]interface{}{
"subAsset": asset.NewFileAsset("bar.txt"),
"subArchive": asset.NewFileArchive("bar.zip"),
out2 := newOutputState(reflect.TypeOf(""))
out2.fulfill(nil, false, nil)
inputs := testInputs{
S: String("a string"),
A: Bool(true),
B: Int(42),
StringAsset: NewStringAsset("put a lime in the coconut"),
FileAsset: NewFileAsset("foo.txt"),
RemoteAsset: NewRemoteAsset("https://pulumi.com/fake/txt"),
AssetArchive: NewAssetArchive(map[string]interface{}{
"subAsset": NewFileAsset("bar.txt"),
"subArchive": NewFileArchive("bar.zip"),
}),
"dFileArchive": asset.NewFileArchive("foo.zip"),
"dRemoteArchive": asset.NewRemoteArchive("https://pulumi.com/fake/archive.zip"),
"e": out,
"fArray": []interface{}{0, 1.3, "x", false},
"fMap": map[string]interface{}{
"x": "y",
"y": 999.9,
"z": false,
FileArchive: NewFileArchive("foo.zip"),
RemoteArchive: NewRemoteArchive("https://pulumi.com/fake/archive.zip"),
E: out,
Array: Array{Int(0), Float32(1.3), String("x"), Bool(false)},
Map: Map{
"x": String("y"),
"y": Float64(999.9),
"z": Bool(false),
},
"g": out2,
"h": URN("foo"),
"i": out3,
G: StringOutput{out2},
H: URN("foo"),
I: StringOutput{},
}
// Marshal those inputs.
m, pdeps, deps, err := marshalInputs(input, true)
resolved, pdeps, deps, err := marshalInputs(inputs)
assert.Nil(t, err)
if !assert.Nil(t, err) {
assert.Equal(t, len(input), len(pdeps))
assert.Equal(t, reflect.TypeOf(inputs).NumField(), len(pdeps))
assert.Equal(t, 0, len(deps))
// Now just unmarshal and ensure the resulting map matches.
res, err := unmarshalOutputs(m)
resV, err := unmarshalPropertyValue(resource.NewObjectProperty(resolved))
if !assert.Nil(t, err) {
if !assert.NotNil(t, res) {
if !assert.NotNil(t, resV) {
res := resV.(map[string]interface{})
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, "put a lime in the coconut", res["cStringAsset"].(Asset).Text())
assert.Equal(t, "foo.txt", res["cFileAsset"].(Asset).Path())
assert.Equal(t, "https://pulumi.com/fake/txt", res["cRemoteAsset"].(Asset).URI())
ar := res["dAssetArchive"].(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, "bar.txt", ar["subAsset"].(Asset).Path())
assert.Equal(t, "bar.zip", ar["subrchive"].(Archive).Path())
assert.Equal(t, "foo.zip", res["dFileArchive"].(Archive).Path())
assert.Equal(t, "https://pulumi.com/fake/archive.zip", res["dRemoteArchive"].(Archive).URI())
assert.Equal(t, "outputty", res["e"])
aa := res["fArray"].([]interface{})
assert.Equal(t, 4, len(aa))
@ -95,92 +139,252 @@ func TestMarshalRoundtrip(t *testing.T) {
}
}
}
}
// 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))
type nestedTypeInput interface {
Input
}
// 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"])
}
}
}
var nestedTypeType = reflect.TypeOf((*nestedType)(nil)).Elem()
type nestedType struct {
Foo string `pulumi:"foo"`
Bar int `pulumi:"bar"`
}
type nestedTypeInputs struct {
Foo StringInput `pulumi:"foo"`
Bar IntInput `pulumi:"bar"`
}
func (nestedTypeInputs) ElementType() reflect.Type {
return nestedTypeType
}
func (nestedTypeInputs) isNestedType() {}
type nestedTypeOutput struct{ *OutputState }
func (nestedTypeOutput) ElementType() reflect.Type {
return nestedTypeType
}
func (nestedTypeOutput) isNestedType() {}
func init() {
RegisterOutputType(nestedTypeOutput{})
}
type testResourceArgs struct {
URN URN `pulumi:"urn"`
ID ID `pulumi:"id"`
Any interface{} `pulumi:"any"`
Archive Archive `pulumi:"archive"`
Array []interface{} `pulumi:"array"`
Asset Asset `pulumi:"asset"`
Bool bool `pulumi:"bool"`
Float32 float32 `pulumi:"float32"`
Float64 float64 `pulumi:"float64"`
Int int `pulumi:"int"`
Int8 int8 `pulumi:"int8"`
Int16 int16 `pulumi:"int16"`
Int32 int32 `pulumi:"int32"`
Int64 int64 `pulumi:"int64"`
Map map[string]interface{} `pulumi:"map"`
String string `pulumi:"string"`
Uint uint `pulumi:"uint"`
Uint8 uint8 `pulumi:"uint8"`
Uint16 uint16 `pulumi:"uint16"`
Uint32 uint32 `pulumi:"uint32"`
Uint64 uint64 `pulumi:"uint64"`
Nested nestedType `pulumi:"nested"`
}
type testResourceInputs struct {
URN URNInput
ID IDInput
Any Input
Archive ArchiveInput
Array ArrayInput
Asset AssetInput
Bool BoolInput
Float32 Float32Input
Float64 Float64Input
Int IntInput
Int8 Int8Input
Int16 Int16Input
Int32 Int32Input
Int64 Int64Input
Map MapInput
String StringInput
Uint UintInput
Uint8 Uint8Input
Uint16 Uint16Input
Uint32 Uint32Input
Uint64 Uint64Input
Nested nestedTypeInput
}
func (*testResourceInputs) ElementType() reflect.Type {
return reflect.TypeOf((*testResourceArgs)(nil))
}
type testResource struct {
CustomResourceState
Any AnyOutput `pulumi:"any"`
Archive ArchiveOutput `pulumi:"archive"`
Array ArrayOutput `pulumi:"array"`
Asset AssetOutput `pulumi:"asset"`
Bool BoolOutput `pulumi:"bool"`
Float32 Float32Output `pulumi:"float32"`
Float64 Float64Output `pulumi:"float64"`
Int IntOutput `pulumi:"int"`
Int8 Int8Output `pulumi:"int8"`
Int16 Int16Output `pulumi:"int16"`
Int32 Int32Output `pulumi:"int32"`
Int64 Int64Output `pulumi:"int64"`
Map MapOutput `pulumi:"map"`
String StringOutput `pulumi:"string"`
Uint UintOutput `pulumi:"uint"`
Uint8 Uint8Output `pulumi:"uint8"`
Uint16 Uint16Output `pulumi:"uint16"`
Uint32 Uint32Output `pulumi:"uint32"`
Uint64 Uint64Output `pulumi:"uint64"`
Nested nestedTypeOutput `pulumi:"nested"`
}
func TestResourceState(t *testing.T) {
state := makeResourceState(true, map[string]interface{}{"baz": nil})
var theResource testResource
state := makeResourceState("", &theResource, nil)
s, _, _, _ := marshalInputs(map[string]interface{}{"baz": "qux"}, true)
resolved, _, _, _ := marshalInputs(&testResourceInputs{
Any: String("foo"),
Archive: NewRemoteArchive("https://pulumi.com/fake/archive.zip"),
Array: Array{String("foo")},
Asset: NewStringAsset("put a lime in the coconut"),
Bool: Bool(true),
Float32: Float32(42.0),
Float64: Float64(3.14),
Int: Int(-1),
Int8: Int8(-2),
Int16: Int16(-3),
Int32: Int32(-4),
Int64: Int64(-5),
Map: Map{"foo": String("bar")},
String: String("qux"),
Uint: Uint(1),
Uint8: Uint8(2),
Uint16: Uint16(3),
Uint32: Uint32(4),
Uint64: Uint64(5),
Nested: nestedTypeInputs{
Foo: String("bar"),
Bar: Int(42),
},
})
s, err := plugin.MarshalProperties(
resolved,
plugin.MarshalOptions{KeepUnknowns: true})
assert.NoError(t, err)
state.resolve(false, nil, nil, "foo", "bar", s)
input := map[string]interface{}{
"urn": state.urn,
"id": state.id,
"baz": state.State["baz"],
input := &testResourceInputs{
URN: theResource.URN(),
ID: theResource.ID(),
Any: theResource.Any,
Archive: theResource.Archive,
Array: theResource.Array,
Asset: theResource.Asset,
Bool: theResource.Bool,
Float32: theResource.Float32,
Float64: theResource.Float64,
Int: theResource.Int,
Int8: theResource.Int8,
Int16: theResource.Int16,
Int32: theResource.Int32,
Int64: theResource.Int64,
Map: theResource.Map,
String: theResource.String,
Uint: theResource.Uint,
Uint8: theResource.Uint8,
Uint16: theResource.Uint16,
Uint32: theResource.Uint32,
Uint64: theResource.Uint64,
Nested: theResource.Nested,
}
m, pdeps, deps, err := marshalInputs(input, true)
resolved, pdeps, deps, err := marshalInputs(input)
assert.Nil(t, err)
assert.Equal(t, map[string][]URN{
"urn": {"foo"},
"id": {"foo"},
"baz": {"foo"},
"urn": {"foo"},
"id": {"foo"},
"any": {"foo"},
"archive": {"foo"},
"array": {"foo"},
"asset": {"foo"},
"bool": {"foo"},
"float32": {"foo"},
"float64": {"foo"},
"int": {"foo"},
"int8": {"foo"},
"int16": {"foo"},
"int32": {"foo"},
"int64": {"foo"},
"map": {"foo"},
"string": {"foo"},
"uint": {"foo"},
"uint8": {"foo"},
"uint16": {"foo"},
"uint32": {"foo"},
"uint64": {"foo"},
"nested": {"foo"},
}, pdeps)
assert.Equal(t, []URN{"foo", "foo", "foo"}, deps)
assert.Equal(t, []URN{"foo"}, deps)
res, err := unmarshalOutputs(m)
res, err := unmarshalPropertyValue(resource.NewObjectProperty(resolved))
assert.Nil(t, err)
assert.Equal(t, map[string]interface{}{
"urn": "foo",
"id": "bar",
"baz": "qux",
"urn": "foo",
"id": "bar",
"any": "foo",
"archive": NewRemoteArchive("https://pulumi.com/fake/archive.zip"),
"array": []interface{}{"foo"},
"asset": NewStringAsset("put a lime in the coconut"),
"bool": true,
"float32": 42.0,
"float64": 3.14,
"int": -1.0,
"int8": -2.0,
"int16": -3.0,
"int32": -4.0,
"int64": -5.0,
"map": map[string]interface{}{"foo": "bar"},
"string": "qux",
"uint": 1.0,
"uint8": 2.0,
"uint16": 3.0,
"uint32": 4.0,
"uint64": 5.0,
"nested": map[string]interface{}{
"foo": "bar",
"bar": 42.0,
},
}, res)
}
func TestUnmarshalUnsupportedSecret(t *testing.T) {
m, _, err := marshalInput(map[string]interface{}{
rpcTokenSpecialSigKey: rpcTokenSpecialSecretSig,
})
assert.NoError(t, err)
_, err = unmarshalOutput(m)
assert.Error(t, err)
}
secret := resource.MakeSecret(resource.NewPropertyValue("foo"))
func TestUnmarshalUnknownSig(t *testing.T) {
m, _, err := marshalInput(map[string]interface{}{
rpcTokenSpecialSigKey: "foobar",
})
assert.NoError(t, err)
_, err = unmarshalOutput(m)
_, err := unmarshalPropertyValue(secret)
assert.Error(t, err)
var sv string
err = unmarshalOutput(secret, reflect.ValueOf(&sv).Elem())
assert.Error(t, err)
}

View file

@ -27,11 +27,14 @@ import (
"github.com/pulumi/pulumi/pkg/util/contract"
)
// A RunOption is used to control the behavior of Run and RunErr.
type RunOption func(*RunInfo)
// Run executes the body of a Pulumi program, granting it access to a deployment context that it may use
// to register resources and orchestrate deployment activities. This connects back to the Pulumi engine using gRPC.
// If the program fails, the process will be terminated and the function will not return.
func Run(body RunFunc) {
if err := RunErr(body); err != nil {
func Run(body RunFunc, opts ...RunOption) {
if err := RunErr(body, opts...); err != nil {
fmt.Fprintf(os.Stderr, "error: program failed: %v\n", err)
os.Exit(1)
}
@ -39,20 +42,23 @@ func Run(body RunFunc) {
// RunErr executes the body of a Pulumi program, granting it access to a deployment context that it may use
// to register resources and orchestrate deployment activities. This connects back to the Pulumi engine using gRPC.
func RunErr(body RunFunc) error {
func RunErr(body RunFunc, opts ...RunOption) error {
// Parse the info out of environment variables. This is a lame contract with the caller, but helps to keep
// boilerplate to a minimum in the average Pulumi Go program.
// TODO(joe): this is a fine default, but consider `...RunOpt`s to control how we get the various addresses, etc.
info := getEnvInfo()
for _, o := range opts {
o(&info)
}
// Validate some properties.
if info.Project == "" {
return errors.Errorf("missing project name")
} else if info.Stack == "" {
return errors.New("missing stack name")
} else if info.MonitorAddr == "" {
} else if info.MonitorAddr == "" && info.Mocks == nil {
return errors.New("missing resource monitor RPC address")
} else if info.EngineAddr == "" {
} else if info.EngineAddr == "" && info.Mocks == nil {
return errors.New("missing engine RPC address")
}
@ -72,16 +78,13 @@ func RunWithContext(ctx *Context, body RunFunc) error {
info := ctx.info
// Create a root stack resource that we'll parent everything to.
reg, err := ctx.RegisterResource(
"pulumi:pulumi:Stack", fmt.Sprintf("%s-%s", info.Project, info.Stack), false, nil)
var stack ResourceState
err := ctx.RegisterResource(
"pulumi:pulumi:Stack", fmt.Sprintf("%s-%s", info.Project, info.Stack), nil, &stack)
if err != nil {
return err
}
ctx.stackR, _, err = reg.URN().await(context.TODO())
if err != nil {
return err
}
contract.Assertf(ctx.stackR != "", "expected root stack resource to have a non-empty URN")
ctx.stack = stack
// Execute the body.
var result error
@ -90,12 +93,15 @@ func RunWithContext(ctx *Context, body RunFunc) error {
}
// Register all the outputs to the stack object.
if err = ctx.RegisterResourceOutputs(ctx.stackR, ctx.exports); err != nil {
if err = ctx.RegisterResourceOutputs(ctx.stack, Map(ctx.exports)); err != nil {
result = multierror.Append(result, err)
}
// Ensure all outstanding RPCs have completed before proceeding. Also, prevent any new RPCs from happening.
// Ensure all outstanding RPCs have completed before proceeding. Also, prevent any new RPCs from happening.
ctx.waitForRPCs()
if ctx.rpcError != nil {
return ctx.rpcError
}
// Propagate the error from the body, if any.
return result
@ -114,6 +120,7 @@ type RunInfo struct {
DryRun bool
MonitorAddr string
EngineAddr string
Mocks MockResourceMonitor
}
// getEnvInfo reads various program information from the process environment.

190
sdk/go/pulumi/run_test.go Normal file
View file

@ -0,0 +1,190 @@
package pulumi
import (
"reflect"
"testing"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/stretchr/testify/assert"
)
type testMonitor struct {
CallF func(tok string, args resource.PropertyMap, provider string) (resource.PropertyMap, error)
NewResourceF func(typeToken, name string, inputs resource.PropertyMap,
provider, id string) (string, resource.PropertyMap, error)
}
func (m *testMonitor) Call(tok string, args resource.PropertyMap, provider string) (resource.PropertyMap, error) {
if m.CallF == nil {
return resource.PropertyMap{}, nil
}
return m.CallF(tok, args, provider)
}
func (m *testMonitor) NewResource(typeToken, name string, inputs resource.PropertyMap,
provider, id string) (string, resource.PropertyMap, error) {
if m.NewResourceF == nil {
return name, resource.PropertyMap{}, nil
}
return m.NewResourceF(typeToken, name, inputs, provider, id)
}
type testResource2 struct {
CustomResourceState
Foo StringOutput `pulumi:"foo"`
}
type testResource2Args struct {
Foo string `pulumi:"foo"`
Bar string `pulumi:"bar"`
Baz string `pulumi:"baz"`
Bang string `pulumi:"bang"`
}
type testResource2Inputs struct {
Foo StringInput
Bar StringInput
Baz StringInput
Bang StringInput
}
func (*testResource2Inputs) ElementType() reflect.Type {
return reflect.TypeOf((*testResource2Args)(nil))
}
type invokeArgs struct {
Bang string `pulumi:"bang"`
Bar string `pulumi:"bar"`
}
type invokeResult struct {
Foo string `pulumi:"foo"`
Baz string `pulumi:"baz"`
}
func TestRegisterResource(t *testing.T) {
mocks := &testMonitor{
NewResourceF: func(typeToken, name string, inputs resource.PropertyMap,
provider, id string) (string, resource.PropertyMap, error) {
assert.Equal(t, "test:resource:type", typeToken)
assert.Equal(t, "resA", name)
assert.True(t, inputs.DeepEquals(resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "oof",
"bar": "rab",
"baz": "zab",
"bang": "gnab",
})))
assert.Equal(t, "", provider)
assert.Equal(t, "", id)
return "someID", resource.PropertyMap{"foo": resource.NewStringProperty("qux")}, nil
},
}
err := RunErr(func(ctx *Context) error {
var res testResource2
err := ctx.RegisterResource("test:resource:type", "resA", &testResource2Inputs{
Foo: String("oof"),
Bar: String("rab"),
Baz: String("zab"),
Bang: String("gnab"),
}, &res)
assert.NoError(t, err)
id, known, err := await(res.ID())
assert.NoError(t, err)
assert.True(t, known)
assert.Equal(t, ID("someID"), id)
urn, known, err := await(res.URN())
assert.NoError(t, err)
assert.True(t, known)
assert.NotEqual(t, "", urn)
foo, known, err := await(res.Foo)
assert.NoError(t, err)
assert.True(t, known)
assert.Equal(t, "qux", foo)
return nil
}, WithMocks("project", "stack", mocks))
assert.NoError(t, err)
}
func TestReadResource(t *testing.T) {
mocks := &testMonitor{
NewResourceF: func(typeToken, name string, state resource.PropertyMap,
provider, id string) (string, resource.PropertyMap, error) {
assert.Equal(t, "test:resource:type", typeToken)
assert.Equal(t, "resA", name)
assert.True(t, state.DeepEquals(resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "oof",
})))
assert.Equal(t, "", provider)
assert.Equal(t, "someID", id)
return id, resource.PropertyMap{"foo": resource.NewStringProperty("qux")}, nil
},
}
err := RunErr(func(ctx *Context) error {
var res testResource2
err := ctx.ReadResource("test:resource:type", "resA", ID("someID"), &testResource2Inputs{
Foo: String("oof"),
}, &res)
assert.NoError(t, err)
id, known, err := await(res.ID())
assert.NoError(t, err)
assert.True(t, known)
assert.Equal(t, ID("someID"), id)
urn, known, err := await(res.URN())
assert.NoError(t, err)
assert.True(t, known)
assert.NotEqual(t, "", urn)
foo, known, err := await(res.Foo)
assert.NoError(t, err)
assert.True(t, known)
assert.Equal(t, "qux", foo)
return nil
}, WithMocks("project", "stack", mocks))
assert.NoError(t, err)
}
func TestInvoke(t *testing.T) {
mocks := &testMonitor{
CallF: func(token string, args resource.PropertyMap, provider string) (resource.PropertyMap, error) {
assert.Equal(t, "test:index:func", token)
assert.True(t, args.DeepEquals(resource.NewPropertyMapFromMap(map[string]interface{}{
"bang": "gnab",
"bar": "rab",
})))
return resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "oof",
"baz": "zab",
}), nil
},
}
err := RunErr(func(ctx *Context) error {
var result invokeResult
err := ctx.Invoke("test:index:func", &invokeArgs{
Bang: "gnab",
Bar: "rab",
}, &result)
assert.NoError(t, err)
assert.Equal(t, "oof", result.Foo)
assert.Equal(t, "zab", result.Baz)
return nil
}, WithMocks("project", "stack", mocks))
assert.NoError(t, err)
}

View file

@ -0,0 +1,149 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll, interfacer
package pulumi
import (
"context"
"reflect"
)
{{range .Builtins}}
// Apply{{.Name}} is like ApplyT, but returns a {{.Name}}Output.
func (o *OutputState) Apply{{.Name}}(applier interface{}) {{.Name}}Output {
return o.ApplyT(applier).({{.Name}}Output)
}
// Apply{{.Name}}WithContext is like ApplyTWithContext, but returns a {{.Name}}Output.
func (o *OutputState) Apply{{.Name}}WithContext(ctx context.Context, applier interface{}) {{.Name}}Output {
return o.ApplyTWithContext(ctx, applier).({{.Name}}Output)
}
{{end}}
{{with $builtins := .Builtins}}
{{range $builtins}}
var {{.Name | Unexported}}Type = reflect.TypeOf((*{{.ElementType}})(nil)).Elem()
// {{.Name}}Input is an input type that accepts {{.Name}} and {{.Name}}Output values.
type {{.Name}}Input interface {
Input
To{{.Name}}Output() {{.Name}}Output
To{{.Name}}OutputWithContext(ctx context.Context) {{.Name}}Output
}
{{if .DefineInputType}}
// {{.Name}} is an input type for {{.Type}} values.
type {{.Name}} {{.Type}}
{{else if .DefinePtrType}}
type {{.PtrType}} {{.ElemElementType}}
// {{.Name}} is an input type for {{.Type}} values.
func {{.Name}}(v {{.ElemElementType}}) {{.Name}}Input {
return ({{.InputType}})(&v)
}
{{end}}
{{if .DefineInputMethods}}
// ElementType returns the element type of this Input ({{.ElementType}}).
func ({{.InputType}}) ElementType() reflect.Type {
return {{.Name | Unexported}}Type
}
func (in {{.InputType}}) To{{.Name}}Output() {{.Name}}Output {
return ToOutput(in).({{.Name}}Output)
}
func (in {{.InputType}}) To{{.Name}}OutputWithContext(ctx context.Context) {{.Name}}Output {
return ToOutputWithContext(ctx, in).({{.Name}}Output)
}
{{with $builtin := .}}
{{range $t := .Implements}}
func (in {{$builtin.InputType}}) To{{$t.Name}}Output() {{$t.Name}}Output {
return in.To{{$t.Name}}OutputWithContext(context.Background())
}
func (in {{$builtin.InputType}}) To{{$t.Name}}OutputWithContext(ctx context.Context) {{$t.Name}}Output {
return in.To{{$builtin.Name}}OutputWithContext(ctx).To{{$t.Name}}OutputWithContext(ctx)
}
{{end}}
{{end}}
{{end}}
// {{.Name}}Output is an Output that returns {{.ElementType}} values.
type {{.Name}}Output struct { *OutputState }
// ElementType returns the element type of this Output ({{.ElementType}}).
func ({{.Name}}Output) ElementType() reflect.Type {
return {{.Name | Unexported}}Type
}
func (o {{.Name}}Output) To{{.Name}}Output() {{.Name}}Output {
return o
}
func (o {{.Name}}Output) To{{.Name}}OutputWithContext(ctx context.Context) {{.Name}}Output {
return o
}
{{with $builtin := .}}
{{range $t := .Implements}}
func (o {{$builtin.Name}}Output) To{{$t.Name}}Output() {{$t.Name}}Output {
return o.To{{$t.Name}}OutputWithContext(context.Background())
}
func (o {{$builtin.Name}}Output) To{{$t.Name}}OutputWithContext(ctx context.Context) {{$t.Name}}Output {
return o.ApplyTWithContext(ctx, func(_ context.Context, v {{$builtin.ElementType}}) {{$t.ElementType}} {
return ({{$t.ElementType}})(v)
}).({{$t.Name}}Output)
}
{{end}}
{{end}}
{{if .DefineElem}}
func (o {{.Name}}Output) Elem() {{.ElemReturnType}}Output {
return o.ApplyT(func (v {{.ElementType}}) {{.ElemElementType}} {
return *v
}).({{.ElemReturnType}}Output)
}
{{end}}
{{if .DefineIndex}}
func (o {{.Name}}Output) Index(i IntInput) {{.IndexReturnType}}Output {
return All(o, i).ApplyT(func(vs []interface{}) {{.IndexElementType}} {
return vs[0].({{.ElementType}})[vs[1].(int)]
}).({{.IndexReturnType}}Output)
}
{{end}}
{{if .DefineMapIndex}}
func (o {{.Name}}Output) MapIndex(k StringInput) {{.MapIndexReturnType}}Output {
return All(o, k).ApplyT(func(vs []interface{}) {{.MapIndexElementType}} {
return vs[0].({{.ElementType}})[vs[1].(string)]
}).({{.MapIndexReturnType}}Output)
}
{{end}}
{{end}}
{{end}}
func getResolvedValue(input Input) (reflect.Value, bool) {
switch input := input.(type) {
case *asset, *archive:
return reflect.ValueOf(input), true
default:
return reflect.Value{}, false
}
}
func init() {
{{- range .Builtins}}
RegisterOutputType({{.Name}}Output{})
{{- end}}
}

View file

@ -0,0 +1,268 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll, unconvert
package pulumi
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestOutputApply(t *testing.T) {
// Test that resolved outputs lead to applies being run.
{
out := newIntOutput()
go func() { out.resolve(42, true) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
v, known, err := await(app)
assert.True(t, ranApp)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, v, 43)
}
// Test that resolved, but unknown outputs, skip the running of applies.
{
out := newIntOutput()
go func() { out.resolve(42, false) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
_, known, err := await(app)
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 := newIntOutput()
go func() { out.reject(errors.New("boom")) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
ranApp = true
return v + 1, nil
})
v, _, err := await(app)
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 := newIntOutput()
go func() { out.resolve(42, true) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
other, resolveOther, _ := NewOutput()
go func() { resolveOther(v + 1) }()
ranApp = true
return other, nil
})
v, known, err := await(app)
assert.True(t, ranApp)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, v, 43)
app = out.ApplyT(func(v int) (interface{}, error) {
other, resolveOther, _ := NewOutput()
go func() { resolveOther(v + 2) }()
ranApp = true
return other, nil
})
v, known, err = await(app)
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 := newIntOutput()
go func() { out.resolve(42, true) }()
var ranApp bool
app := out.ApplyT(func(v int) (interface{}, error) {
other, _, rejectOther := NewOutput()
go func() { rejectOther(errors.New("boom")) }()
ranApp = true
return other, nil
})
v, _, err := await(app)
assert.True(t, ranApp)
assert.NotNil(t, err)
assert.Nil(t, v)
app = out.ApplyT(func(v int) (interface{}, error) {
other, _, rejectOther := NewOutput()
go func() { rejectOther(errors.New("boom")) }()
ranApp = true
return other, nil
})
v, _, err = await(app)
assert.True(t, ranApp)
assert.NotNil(t, err)
assert.Nil(t, v)
}
// Test that applies return appropriate concrete implementations of Output based on the callback type
{
out := newIntOutput()
go func() { out.resolve(42, true) }()
{{range .Builtins}}
t.Run("ApplyT::{{.Name}}Output", func(t *testing.T) {
_, ok := out.ApplyT(func(v int) {{.ElementType}} { return *new({{.ElementType}}) }).({{.Name}}Output)
assert.True(t, ok)
})
{{end}}
}
// Test some chained applies.
{
type myStructType struct {
foo int
bar string
}
out := newIntOutput()
go func() { out.resolve(42, true) }()
out2 := StringOutput{newOutputState(reflect.TypeOf(""))}
go func() { out2.resolve("hello", true) }()
res := out.
ApplyT(func(v int) myStructType {
return myStructType{foo: v, bar: "qux,zed"}
}).
ApplyT(func(v interface{}) (string, error) {
bar := v.(myStructType).bar
if bar != "qux,zed" {
return "", errors.New("unexpected value")
}
return bar, nil
}).
ApplyT(func (v string) ([]string, error) {
strs := strings.Split(v, ",")
if len(strs) != 2 {
return nil, errors.New("unexpected value")
}
return []string{strs[0], strs[1]}, nil
})
res2 := out.
ApplyT(func(v int) myStructType {
return myStructType{foo: v, bar: "foo,bar"}
}).
ApplyT(func(v interface{}) (string, error) {
bar := v.(myStructType).bar
if bar != "foo,bar" {
return "", errors.New("unexpected value")
}
return bar, nil
}).
ApplyT(func (v string) ([]string, error) {
strs := strings.Split(v, ",")
if len(strs) != 2 {
return nil, errors.New("unexpected value")
}
return []string{strs[0], strs[1]}, nil
})
res3 := All(res, res2).ApplyT(func (v []interface{}) string {
res, res2 := v[0].([]string), v[1].([]string)
return strings.Join(append(res2, res...), ",")
})
res4 := All(out, out2).ApplyT(func(v []interface{}) *myStructType {
return &myStructType{
foo: v[0].(int),
bar: v[1].(string),
}
})
res5 := All(res3, res4).Apply(func (v interface{}) (interface{}, error) {
vs := v.([]interface{})
res3 := vs[0].(string)
res4 := vs[1].(*myStructType)
return fmt.Sprintf("%v;%v;%v", res3, res4.foo, res4.bar), nil
})
_, ok := res.(StringArrayOutput)
assert.True(t, ok)
v, known, err := await(res)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, []string{"qux", "zed"}, v)
_, ok = res2.(StringArrayOutput)
assert.True(t, ok)
v, known, err = await(res2)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, []string{"foo", "bar"}, v)
_, ok = res3.(StringOutput)
assert.True(t, ok)
v, known, err = await(res3)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, "foo,bar,qux,zed", v)
_, ok = res4.(AnyOutput)
assert.True(t, ok)
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)
assert.Nil(t, err)
assert.True(t, known)
assert.Equal(t, "foo,bar,qux,zed;42;hello", v)
}
}
// Test that ToOutput works with all builtin input types
{{range .Builtins}}
func TestToOutput{{.Name}}(t *testing.T) {
out := ToOutput({{.Example}})
_, ok := out.({{.Name}}Input)
assert.True(t, ok)
_, known, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
out = ToOutput(out)
_, ok = out.({{.Name}}Input)
assert.True(t, ok)
_, known, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
}
{{end}}

791
sdk/go/pulumi/types.go Normal file
View file

@ -0,0 +1,791 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll, interfacer
package pulumi
import (
"context"
"reflect"
"sync"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Output helps encode the relationship between resources in a Pulumi application. Specifically an output property
// holds onto a value and the resource it came from. An output value can then be provided when constructing new
// resources, allowing that new resource to know both the value as well as the resource the value came from. This
// allows for a precise "dependency graph" to be created, which properly tracks the relationship between resources.
type Output interface {
ElementType() reflect.Type
Apply(applier func(interface{}) (interface{}, error)) AnyOutput
ApplyWithContext(ctx context.Context, applier func(context.Context, interface{}) (interface{}, error)) AnyOutput
ApplyT(applier interface{}) Output
ApplyTWithContext(ctx context.Context, applier interface{}) Output
getState() *OutputState
dependencies() []Resource
fulfillValue(value reflect.Value, known bool, err error)
resolveValue(value reflect.Value, known bool)
fulfill(value interface{}, known bool, err error)
resolve(value interface{}, known bool)
reject(err error)
await(ctx context.Context) (interface{}, bool, error)
}
var outputType = reflect.TypeOf((*Output)(nil)).Elem()
var inputType = reflect.TypeOf((*Input)(nil)).Elem()
var concreteTypeToOutputType sync.Map // map[reflect.Type]reflect.Type
// RegisterOutputType registers an Output type with the Pulumi runtime. If a value of this type's concrete type is
// returned by an Apply, the Apply will return the specific Output type.
func RegisterOutputType(output Output) {
elementType := output.ElementType()
existing, hasExisting := concreteTypeToOutputType.LoadOrStore(elementType, reflect.TypeOf(output))
if hasExisting {
panic(errors.Errorf("an output type for %v is already registered: %v", elementType, existing))
}
}
const (
outputPending = iota
outputResolved
outputRejected
)
// OutputState holds the internal details of an Output and implements the Apply and ApplyWithContext methods.
type OutputState struct {
mutex sync.Mutex
cond *sync.Cond
state uint32 // one of output{Pending,Resolved,Rejected}
value interface{} // the value of this output if it is resolved.
err error // the error associated with this output if it is rejected.
known bool // true if this output's value is known.
element reflect.Type // the element type of this output.
deps []Resource // the dependencies associated with this output property.
}
func (o *OutputState) elementType() reflect.Type {
if o == nil {
return anyType
}
return o.element
}
func (o *OutputState) dependencies() []Resource {
if o == nil {
return nil
}
return o.deps
}
func (o *OutputState) fulfill(value interface{}, known bool, err error) {
o.fulfillValue(reflect.ValueOf(value), known, err)
}
func (o *OutputState) fulfillValue(value reflect.Value, known bool, err error) {
if o == nil {
return
}
o.mutex.Lock()
defer func() {
o.mutex.Unlock()
o.cond.Broadcast()
}()
if o.state != outputPending {
return
}
if err != nil {
o.state, o.err, o.known = outputRejected, err, true
} else {
if value.IsValid() {
reflect.ValueOf(&o.value).Elem().Set(value)
}
o.state, o.known = outputResolved, known
}
}
func (o *OutputState) resolve(value interface{}, known bool) {
o.fulfill(value, known, nil)
}
func (o *OutputState) resolveValue(value reflect.Value, known bool) {
o.fulfillValue(value, known, nil)
}
func (o *OutputState) reject(err error) {
o.fulfill(nil, true, err)
}
func (o *OutputState) await(ctx context.Context) (interface{}, bool, error) {
for {
if o == nil {
// If the state is nil, treat its value as resolved and unknown.
return nil, false, nil
}
o.mutex.Lock()
for o.state == outputPending {
if ctx.Err() != nil {
return nil, true, ctx.Err()
}
o.cond.Wait()
}
o.mutex.Unlock()
if !o.known || o.err != nil {
return nil, o.known, o.err
}
// If the result is an Output, await it in turn.
//
// NOTE: this isn't exactly type safe! The element type of the inner output really needs to be assignable to
// the element type of the outer output. We should reconsider this.
ov, ok := o.value.(Output)
if !ok {
return o.value, true, nil
}
o = ov.getState()
}
}
func (o *OutputState) getState() *OutputState {
return o
}
func newOutputState(elementType reflect.Type, deps ...Resource) *OutputState {
out := &OutputState{
element: elementType,
deps: deps,
}
out.cond = sync.NewCond(&out.mutex)
return out
}
var outputStateType = reflect.TypeOf((*OutputState)(nil))
var outputTypeToOutputState sync.Map // map[reflect.Type]int
func newOutput(typ reflect.Type, deps ...Resource) Output {
contract.Assert(typ.Implements(outputType))
// All values that implement Output must embed a field of type `*OutputState` by virtue of the unexported
// `isOutput` method. If we yet haven't recorded the index of this field for the ouptut type `typ`, find and
// record it.
outputFieldV, ok := outputTypeToOutputState.Load(typ)
if !ok {
outputField := -1
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if f.Anonymous && f.Type == outputStateType {
outputField = i
break
}
}
contract.Assert(outputField != -1)
outputTypeToOutputState.Store(typ, outputField)
outputFieldV = outputField
}
// Create the new output.
output := reflect.New(typ).Elem()
state := newOutputState(output.Interface().(Output).ElementType(), deps...)
output.Field(outputFieldV.(int)).Set(reflect.ValueOf(state))
return output.Interface().(Output)
}
// 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() (Output, func(interface{}), func(error)) {
out := newOutputState(anyType)
resolve := func(v interface{}) {
out.resolve(v, true)
}
reject := func(err error) {
out.reject(err)
}
return AnyOutput{out}, resolve, reject
}
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorType = reflect.TypeOf((*error)(nil)).Elem()
func makeContextful(fn interface{}, elementType reflect.Type) interface{} {
fv := reflect.ValueOf(fn)
if fv.Kind() != reflect.Func {
panic(errors.New("applier must be a function"))
}
ft := fv.Type()
if ft.NumIn() != 1 || !elementType.AssignableTo(ft.In(0)) {
panic(errors.Errorf("applier must have 1 input parameter assignable from %v", elementType))
}
var outs []reflect.Type
switch ft.NumOut() {
case 1:
// Okay
outs = []reflect.Type{ft.Out(0)}
case 2:
// Second out parameter must be of type error
if !ft.Out(1).AssignableTo(errorType) {
panic(errors.New("applier's second return type must be assignable to error"))
}
outs = []reflect.Type{ft.Out(0), ft.Out(1)}
default:
panic(errors.New("appplier must return exactly one or two values"))
}
ins := []reflect.Type{contextType, ft.In(0)}
contextfulType := reflect.FuncOf(ins, outs, ft.IsVariadic())
contextfulFunc := reflect.MakeFunc(contextfulType, func(args []reflect.Value) []reflect.Value {
// Slice off the context argument and call the applier.
return fv.Call(args[1:])
})
return contextfulFunc.Interface()
}
func checkApplier(fn interface{}, elementType reflect.Type) reflect.Value {
fv := reflect.ValueOf(fn)
if fv.Kind() != reflect.Func {
panic(errors.New("applier must be a function"))
}
ft := fv.Type()
if ft.NumIn() != 2 || !contextType.AssignableTo(ft.In(0)) || !elementType.AssignableTo(ft.In(1)) {
panic(errors.Errorf("applier's input parameters must be assignable from %v and %v", contextType, elementType))
}
switch ft.NumOut() {
case 1:
// Okay
case 2:
// Second out parameter must be of type error
if !ft.Out(1).AssignableTo(errorType) {
panic(errors.New("applier's second return type must be assignable to error"))
}
default:
panic(errors.New("appplier must return exactly one or two values"))
}
// Okay
return fv
}
// Apply transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
func (o *OutputState) Apply(applier func(interface{}) (interface{}, error)) AnyOutput {
return o.ApplyWithContext(context.Background(), func(_ context.Context, v interface{}) (interface{}, error) {
return applier(v)
})
}
// ApplyWithContext transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
func (o *OutputState) ApplyWithContext(ctx context.Context, applier func(context.Context, interface{}) (interface{}, error)) AnyOutput {
return o.ApplyTWithContext(ctx, applier).(AnyOutput)
}
// ApplyT transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
//
// The applier function must have one of the following signatures:
//
// func (v U) T
// func (v U) (T, error)
//
// U must be assignable from the ElementType of the Output. If T is a type that has a registered Output type, the
// result of ApplyT will be of the registered Output type, and can be used in an appropriate type assertion:
//
// stringOutput := pulumi.String("hello").ToStringOutput()
// intOutput := stringOutput.ApplyT(func(v string) int {
// return len(v)
// }).(pulumi.IntOutput)
//
// Otherwise, the result will be of type AnyOutput:
//
// stringOutput := pulumi.String("hello").ToStringOutput()
// intOutput := stringOutput.ApplyT(func(v string) []rune {
// return []rune(v)
// }).(pulumi.AnyOutput)
//
func (o *OutputState) ApplyT(applier interface{}) Output {
return o.ApplyTWithContext(context.Background(), makeContextful(applier, o.elementType()))
}
var anyOutputType = reflect.TypeOf((*AnyOutput)(nil)).Elem()
// ApplyTWithContext transforms the data of the output property using the applier func. The result remains an output
// property, and accumulates all implicated dependencies, so that resources can be properly tracked using a DAG.
// This function does not block awaiting the value; instead, it spawns a Goroutine that will await its availability.
// The provided context can be used to reject the output as canceled.
//
// The applier function must have one of the following signatures:
//
// func (ctx context.Context, v U) T
// func (ctx context.Context, v U) (T, error)
//
// U must be assignable from the ElementType of the Output. If T is a type that has a registered Output type, the
// result of ApplyT will be of the registered Output type, and can be used in an appropriate type assertion:
//
// stringOutput := pulumi.String("hello").ToStringOutput()
// intOutput := stringOutput.ApplyTWithContext(func(_ context.Context, v string) int {
// return len(v)
// }).(pulumi.IntOutput)
//
// Otherwise, the result will be of type AnyOutput:
//
// stringOutput := pulumi.String("hello").ToStringOutput()
// intOutput := stringOutput.ApplyT(func(_ context.Context, v string) []rune {
// return []rune(v)
// }).(pulumi.AnyOutput)
//
func (o *OutputState) ApplyTWithContext(ctx context.Context, applier interface{}) Output {
fn := checkApplier(applier, o.elementType())
resultType := anyOutputType
if ot, ok := concreteTypeToOutputType.Load(fn.Type().Out(0)); ok {
resultType = ot.(reflect.Type)
}
result := newOutput(resultType, o.dependencies()...)
go func() {
v, known, err := o.await(ctx)
if err != nil || !known {
result.fulfill(nil, known, err)
return
}
// If we have a known value, run the applier to transform it.
results := fn.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(v)})
if len(results) == 2 && !results[1].IsNil() {
result.reject(results[1].Interface().(error))
return
}
// Fulfill the result.
result.fulfillValue(results[0], true, nil)
}()
return result
}
// All returns an ArrayOutput that will resolve when all of the provided inputs will resolve. Each element of the
// array will contain the resolved value of the corresponding output. The output will be rejected if any of the inputs
// is rejected.
func All(inputs ...interface{}) ArrayOutput {
return AllWithContext(context.Background(), inputs...)
}
// AllWithContext returns an ArrayOutput that will resolve when all of the provided inputs will resolve. Each
// element of the array will contain the resolved value of the corresponding output. The output will be rejected if any
// of the inputs is rejected.
func AllWithContext(ctx context.Context, inputs ...interface{}) ArrayOutput {
return ToOutputWithContext(ctx, inputs).(ArrayOutput)
}
func gatherDependencies(v interface{}) []Resource {
depSet := make(map[Resource]struct{})
gatherDependencySet(reflect.ValueOf(v), depSet)
if len(depSet) == 0 {
return nil
}
deps := make([]Resource, 0, len(depSet))
for d := range depSet {
deps = append(deps, d)
}
return deps
}
func gatherDependencySet(v reflect.Value, deps map[Resource]struct{}) {
for {
// Check for an Output that we can pull dependencies off of.
if v.Type().Implements(outputType) && v.CanInterface() {
output := v.Convert(outputType).Interface().(Output)
for _, d := range output.dependencies() {
deps[d] = struct{}{}
}
return
}
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
if v.IsNil() {
return
}
v = v.Elem()
continue
case reflect.Struct:
numFields := v.Type().NumField()
for i := 0; i < numFields; i++ {
gatherDependencySet(v.Field(i), deps)
}
case reflect.Array, reflect.Slice:
l := v.Len()
for i := 0; i < l; i++ {
gatherDependencySet(v.Index(i), deps)
}
case reflect.Map:
iter := v.MapRange()
for iter.Next() {
gatherDependencySet(iter.Key(), deps)
gatherDependencySet(iter.Value(), deps)
}
}
return
}
}
func checkToOutputMethod(m reflect.Value, outputType reflect.Type) bool {
if !m.IsValid() {
return false
}
mt := m.Type()
if mt.NumIn() != 1 || mt.In(0) != contextType {
return false
}
return mt.NumOut() == 1 && mt.Out(0) == outputType
}
func callToOutputMethod(ctx context.Context, input reflect.Value, resolvedType reflect.Type) (Output, bool) {
ot, ok := concreteTypeToOutputType.Load(resolvedType)
if !ok {
return nil, false
}
outputType := ot.(reflect.Type)
toOutputMethodName := "To" + outputType.Name() + "WithContext"
toOutputMethod := input.MethodByName(toOutputMethodName)
if !checkToOutputMethod(toOutputMethod, outputType) {
return nil, false
}
return toOutputMethod.Call([]reflect.Value{reflect.ValueOf(ctx)})[0].Interface().(Output), true
}
func awaitInputs(ctx context.Context, v, resolved reflect.Value) (bool, error) {
contract.Assert(v.IsValid())
if !resolved.CanSet() {
return true, nil
}
// If the value is an Input with of a different element type, turn it into an Output of the appropriate type and
// await it.
valueType, isInput := v.Type(), false
if v.CanInterface() && valueType.Implements(inputType) {
input, isNonNil := v.Interface().(Input)
if !isNonNil {
// A nil input is already fully-resolved.
return true, nil
}
valueType = input.ElementType()
assignInput := false
// If the element type of the input is not identical to the type of the destination and the destination is not
// the any type (i.e. interface{}), attempt to convert the input to the appropriately-typed output.
if valueType != resolved.Type() && resolved.Type() != anyType {
if newOutput, ok := callToOutputMethod(ctx, reflect.ValueOf(input), resolved.Type()); ok {
// We were able to convert the input. Use the result as the new input value.
input = newOutput
} else if !valueType.AssignableTo(resolved.Type()) {
// If the value type is not assignable to the destination, see if we can assign the input value itself
// to the destination.
if !v.Type().AssignableTo(resolved.Type()) {
panic(errors.Errorf("cannot convert an input of type %T to a value of type %v",
input, resolved.Type()))
} else {
assignInput = true
}
}
}
// If the input is an Output, await its value. The returned value is fully resolved.
if output, ok := input.(Output); ok {
e, known, err := output.await(ctx)
if err != nil || !known {
return known, err
}
if !assignInput {
resolved.Set(reflect.ValueOf(e))
} else {
resolved.Set(reflect.ValueOf(input))
}
return true, nil
}
// Check for types that are already fully-resolved.
if v, ok := getResolvedValue(input); ok {
resolved.Set(v)
return true, nil
}
v, isInput = reflect.ValueOf(input), true
// If we are assigning the input value itself, update the value type.
if assignInput {
valueType = v.Type()
} else {
// Handle pointer inputs.
if v.Kind() == reflect.Ptr {
v, valueType = v.Elem(), valueType.Elem()
resolved.Set(reflect.New(resolved.Type().Elem()))
resolved = resolved.Elem()
}
}
}
contract.Assert(valueType.AssignableTo(resolved.Type()))
// If the resolved type is an interface, make an appropriate destination from the value's type.
if resolved.Kind() == reflect.Interface {
iface := resolved
defer func() { iface.Set(resolved) }()
resolved = reflect.New(valueType).Elem()
}
known, err := true, error(nil)
switch v.Kind() {
case reflect.Interface:
if !v.IsNil() {
return awaitInputs(ctx, v.Elem(), resolved)
}
case reflect.Ptr:
if !v.IsNil() {
resolved.Set(reflect.New(resolved.Type().Elem()))
return awaitInputs(ctx, v.Elem(), resolved.Elem())
}
case reflect.Struct:
typ := v.Type()
getMappedField := mapStructTypes(typ, resolved.Type())
numFields := typ.NumField()
for i := 0; i < numFields; i++ {
_, field := getMappedField(resolved, i)
fknown, ferr := awaitInputs(ctx, v.Field(i), field)
known = known && fknown
if err == nil {
err = ferr
}
}
case reflect.Array:
l := v.Len()
for i := 0; i < l; i++ {
eknown, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
known = known && eknown
if err == nil {
err = eerr
}
}
case reflect.Slice:
l := v.Len()
resolved.Set(reflect.MakeSlice(resolved.Type(), l, l))
for i := 0; i < l; i++ {
eknown, eerr := awaitInputs(ctx, v.Index(i), resolved.Index(i))
known = known && eknown
if err == nil {
err = eerr
}
}
case reflect.Map:
resolved.Set(reflect.MakeMap(resolved.Type()))
resolvedKeyType, resolvedValueType := resolved.Type().Key(), resolved.Type().Elem()
iter := v.MapRange()
for iter.Next() {
kv := reflect.New(resolvedKeyType).Elem()
kknown, kerr := awaitInputs(ctx, iter.Key(), kv)
if err == nil {
err = kerr
}
vv := reflect.New(resolvedValueType).Elem()
vknown, verr := awaitInputs(ctx, iter.Value(), vv)
if err == nil {
err = verr
}
if kerr == nil && verr == nil && kknown && vknown {
resolved.SetMapIndex(kv, vv)
}
known = known && kknown && vknown
}
default:
if isInput {
v = v.Convert(valueType)
}
resolved.Set(v)
}
return known, err
}
// ToOutput returns an Output that will resolve when all Inputs contained in the given value have resolved.
func ToOutput(v interface{}) Output {
return ToOutputWithContext(context.Background(), v)
}
// ToOutputWithContext returns an Output that will resolve when all Outputs contained in the given value have
// resolved.
func ToOutputWithContext(ctx context.Context, v interface{}) Output {
resolvedType := reflect.TypeOf(v)
if input, ok := v.(Input); ok {
resolvedType = input.ElementType()
}
resultType := anyOutputType
if ot, ok := concreteTypeToOutputType.Load(resolvedType); ok {
resultType = ot.(reflect.Type)
}
result := newOutput(resultType, gatherDependencies(v)...)
go func() {
element := reflect.New(resolvedType).Elem()
known, err := awaitInputs(ctx, reflect.ValueOf(v), element)
if err != nil || !known {
result.fulfill(nil, known, err)
return
}
result.resolveValue(element, true)
}()
return result
}
// Input is the type of a generic input value for a Pulumi resource. This type is used in conjunction with Output
// to provide polymorphism over strongly-typed input values.
//
// The intended pattern for nested Pulumi value types is to define an input interface and a plain, input, and output
// variant of the value type that implement the input interface.
//
// For example, given a nested Pulumi value type with the following shape:
//
// type Nested struct {
// Foo int
// Bar string
// }
//
// We would define the following:
//
// var nestedType = reflect.TypeOf((*Nested)(nil)).Elem()
//
// type NestedInput interface {
// pulumi.Input
//
// ToNestedOutput() NestedOutput
// ToNestedOutputWithContext(context.Context) NestedOutput
// }
//
// type Nested struct {
// Foo int `pulumi:"foo"`
// Bar string `pulumi:"bar"`
// }
//
// type NestedInputValue struct {
// Foo pulumi.IntInput `pulumi:"foo"`
// Bar pulumi.StringInput `pulumi:"bar"`
// }
//
// func (NestedInputValue) ElementType() reflect.Type {
// return nestedType
// }
//
// func (v NestedInputValue) ToNestedOutput() NestedOutput {
// return pulumi.ToOutput(v).(NestedOutput)
// }
//
// func (v NestedInputValue) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
// return pulumi.ToOutputWithContext(ctx, v).(NestedOutput)
// }
//
// type NestedOutput struct { *pulumi.OutputState }
//
// func (NestedOutput) ElementType() reflect.Type {
// return nestedType
// }
//
// func (o NestedOutput) ToNestedOutput() NestedOutput {
// return o
// }
//
// func (o NestedOutput) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
// return o
// }
//
type Input interface {
ElementType() reflect.Type
}
var anyType = reflect.TypeOf((*interface{})(nil)).Elem()
func Any(v interface{}) AnyOutput {
return AnyWithContext(context.Background(), v)
}
func AnyWithContext(ctx context.Context, v interface{}) AnyOutput {
// Return an output that resolves when all nested inputs have resolved.
out := newOutput(anyOutputType, gatherDependencies(v)...)
go func() {
var result interface{}
known, err := awaitInputs(ctx, reflect.ValueOf(v), reflect.ValueOf(&result))
out.fulfill(result, known, err)
}()
return out.(AnyOutput)
}
type AnyOutput struct{ *OutputState }
func (AnyOutput) ElementType() reflect.Type {
return anyType
}
func (o IDOutput) awaitID(ctx context.Context) (ID, bool, error) {
id, known, err := o.await(ctx)
if !known || err != nil {
return "", known, err
}
return ID(convert(id, stringType).(string)), true, nil
}
func (o URNOutput) awaitURN(ctx context.Context) (URN, bool, error) {
id, known, err := o.await(ctx)
if !known || err != nil {
return "", known, err
}
return URN(convert(id, stringType).(string)), true, nil
}
func convert(v interface{}, to reflect.Type) interface{} {
rv := reflect.ValueOf(v)
if !rv.Type().ConvertibleTo(to) {
panic(errors.Errorf("cannot convert output value of type %s to %s", rv.Type(), to))
}
return rv.Convert(to).Interface()
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

303
sdk/go/pulumi/types_test.go Normal file
View file

@ -0,0 +1,303 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// nolint: lll
package pulumi
import (
"context"
"reflect"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func await(out Output) (interface{}, bool, error) {
return out.await(context.Background())
}
func assertApplied(t *testing.T, out Output) {
_, known, err := await(out)
assert.True(t, known)
assert.Nil(t, err)
}
func newIntOutput() IntOutput {
return IntOutput{newOutputState(reflect.TypeOf(42))}
}
func TestBasicOutputs(t *testing.T) {
// Just test basic resolve and reject functionality.
{
out, resolve, _ := NewOutput()
go func() {
resolve(42)
}()
v, known, err := await(out)
assert.Nil(t, err)
assert.True(t, known)
assert.NotNil(t, v)
assert.Equal(t, 42, v.(int))
}
{
out, _, reject := NewOutput()
go func() {
reject(errors.New("boom"))
}()
v, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
func TestArrayOutputs(t *testing.T) {
out := ArrayOutput{newOutputState(reflect.TypeOf([]interface{}{}))}
go func() {
out.resolve([]interface{}{nil, 0, "x"}, true)
}()
{
assertApplied(t, out.ApplyT(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 := BoolOutput{newOutputState(reflect.TypeOf(false))}
go func() {
out.resolve(true, true)
}()
{
assertApplied(t, out.ApplyT(func(v bool) (interface{}, error) {
assert.True(t, v)
return nil, nil
}))
}
}
func TestMapOutputs(t *testing.T) {
out := MapOutput{newOutputState(reflect.TypeOf(map[string]interface{}{}))}
go func() {
out.resolve(map[string]interface{}{
"x": 1,
"y": false,
"z": "abc",
}, true)
}()
{
assertApplied(t, out.ApplyT(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 := Float64Output{newOutputState(reflect.TypeOf(float64(0)))}
go func() {
out.resolve(42.345, true)
}()
{
assertApplied(t, out.ApplyT(func(v float64) (interface{}, error) {
assert.Equal(t, 42.345, v)
return nil, nil
}))
}
}
func TestStringOutputs(t *testing.T) {
out := StringOutput{newOutputState(reflect.TypeOf(""))}
go func() {
out.resolve("a stringy output", true)
}()
{
assertApplied(t, out.ApplyT(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()
go func() {
other, resolveOther, _ := NewOutput()
resolve(other)
go func() { resolveOther(99) }()
}()
assertApplied(t, out.ApplyT(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()
go func() {
other, _, rejectOther := NewOutput()
resolve(other)
go func() { rejectOther(errors.New("boom")) }()
}()
v, _, err := await(out)
assert.NotNil(t, err)
assert.Nil(t, v)
}
}
// Test that ToOutput works with a struct type.
func TestToOutputStruct(t *testing.T) {
out := ToOutput(nestedTypeInputs{Foo: String("bar"), Bar: Int(42)})
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
out = ToOutput(out)
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
out = ToOutput(nestedTypeInputs{Foo: ToOutput(String("bar")).(StringInput), Bar: ToOutput(Int(42)).(IntInput)})
_, ok = out.(nestedTypeOutput)
assert.True(t, ok)
v, known, err = await(out)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 42}, v)
}
type arrayLenInput Array
func (arrayLenInput) ElementType() reflect.Type {
return Array{}.ElementType()
}
func (i arrayLenInput) ToIntOutput() IntOutput {
return i.ToIntOutputWithContext(context.Background())
}
func (i arrayLenInput) ToIntOutputWithContext(ctx context.Context) IntOutput {
return ToOutput(i).ApplyT(func(arr []interface{}) int {
return len(arr)
}).(IntOutput)
}
// Test that ToOutput converts inputs appropriately.
func TestToOutputConvert(t *testing.T) {
out := ToOutput(nestedTypeInputs{Foo: ID("bar"), Bar: arrayLenInput{Int(42)}})
_, ok := out.(nestedTypeOutput)
assert.True(t, ok)
v, known, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, nestedType{Foo: "bar", Bar: 1}, v)
}
// Test that ToOutput correctly handles nested inputs and outputs when the argument is an input or interface{}.
func TestToOutputAny(t *testing.T) {
type args struct {
S StringInput
I IntInput
A Input
}
out := ToOutput(&args{
S: ID("hello"),
I: Int(42).ToIntOutput(),
A: Map{"world": Bool(true).ToBoolOutput()},
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
argsV := v.(*args)
si, ok := argsV.S.(ID)
assert.True(t, ok)
assert.Equal(t, ID("hello"), si)
io, ok := argsV.I.(IntOutput)
assert.True(t, ok)
assert.Equal(t, uint32(outputResolved), io.state)
assert.Equal(t, 42, io.value)
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)
}
type args struct {
S string
I int
A interface{}
}
type argsInputs struct {
S StringInput
I IntInput
A Input
}
func (*argsInputs) ElementType() reflect.Type {
return reflect.TypeOf((*args)(nil))
}
// Test that ToOutput correctly handles nested inputs when the argument is an input with no corresponding output type.
func TestToOutputInputAny(t *testing.T) {
out := ToOutput(&argsInputs{
S: ID("hello"),
I: Int(42),
A: Map{"world": Bool(true).ToBoolOutput()},
})
_, ok := out.(AnyOutput)
assert.True(t, ok)
v, known, err := await(out)
assert.True(t, known)
assert.NoError(t, err)
assert.Equal(t, &args{
S: "hello",
I: 42,
A: map[string]interface{}{"world": true},
}, v)
}