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:
parent
faa6d95178
commit
f168bdc1c2
|
@ -9,7 +9,6 @@ linters:
|
|||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- megacheck
|
||||
- misspell
|
||||
|
|
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::
|
||||
|
|
|
@ -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() {}
|
|
@ -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
215
sdk/go/pulumi/generate.go
Normal 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
173
sdk/go/pulumi/mocks.go
Normal 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
24
sdk/go/pulumi/printf.go
Normal 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)
|
||||
}
|
32
sdk/go/pulumi/printf_test.go
Normal file
32
sdk/go/pulumi/printf_test.go
Normal 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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
190
sdk/go/pulumi/run_test.go
Normal 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)
|
||||
}
|
149
sdk/go/pulumi/templates/types_builtins.go.template
Normal file
149
sdk/go/pulumi/templates/types_builtins.go.template
Normal 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}}
|
||||
}
|
268
sdk/go/pulumi/templates/types_builtins_test.go.template
Normal file
268
sdk/go/pulumi/templates/types_builtins_test.go.template
Normal 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
791
sdk/go/pulumi/types.go
Normal 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()
|
||||
}
|
4452
sdk/go/pulumi/types_builtins.go
Normal file
4452
sdk/go/pulumi/types_builtins.go
Normal file
File diff suppressed because it is too large
Load diff
1968
sdk/go/pulumi/types_builtins_test.go
Normal file
1968
sdk/go/pulumi/types_builtins_test.go
Normal file
File diff suppressed because it is too large
Load diff
303
sdk/go/pulumi/types_test.go
Normal file
303
sdk/go/pulumi/types_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue