mirror of https://github.com/pulumi/pulumi.git
970 lines
32 KiB
Go
970 lines
32 KiB
Go
// Copyright 2016-2024, 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 lifecycletest
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/blang/semver"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
. "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive
|
|
lt "github.com/pulumi/pulumi/pkg/v3/engine/lifecycletest/framework"
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn"
|
|
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
|
|
)
|
|
|
|
func TransformFunction(
|
|
f func(
|
|
name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error),
|
|
) func([]byte) (proto.Message, error) {
|
|
return func(request []byte) (proto.Message, error) {
|
|
var transformationRequest pulumirpc.TransformRequest
|
|
err := proto.Unmarshal(request, &transformationRequest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshaling request: %w", err)
|
|
}
|
|
|
|
mprops, err := plugin.UnmarshalProperties(transformationRequest.Properties, plugin.MarshalOptions{
|
|
KeepUnknowns: true,
|
|
KeepSecrets: true,
|
|
KeepResources: true,
|
|
KeepOutputValues: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshaling properties: %w", err)
|
|
}
|
|
|
|
ret, opts, err := f(
|
|
transformationRequest.Name, transformationRequest.Type, transformationRequest.Custom, transformationRequest.Parent,
|
|
mprops, transformationRequest.Options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mret, err := plugin.MarshalProperties(ret, plugin.MarshalOptions{
|
|
KeepUnknowns: true,
|
|
KeepSecrets: true,
|
|
KeepResources: true,
|
|
KeepOutputValues: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pulumirpc.TransformResponse{
|
|
Properties: mret,
|
|
Options: opts,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func TransformInvokeFunction(
|
|
f func(
|
|
token string, props resource.PropertyMap, opts *pulumirpc.TransformInvokeOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformInvokeOptions, error),
|
|
) func([]byte) (proto.Message, error) {
|
|
return func(request []byte) (proto.Message, error) {
|
|
var transformationRequest pulumirpc.TransformInvokeRequest
|
|
err := proto.Unmarshal(request, &transformationRequest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshaling request: %w", err)
|
|
}
|
|
|
|
margs, err := plugin.UnmarshalProperties(transformationRequest.Args, plugin.MarshalOptions{
|
|
KeepUnknowns: true,
|
|
KeepSecrets: true,
|
|
KeepResources: true,
|
|
KeepOutputValues: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshaling properties: %w", err)
|
|
}
|
|
|
|
ret, opts, err := f(
|
|
transformationRequest.Token, margs, transformationRequest.Options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mret, err := plugin.MarshalProperties(ret, plugin.MarshalOptions{
|
|
KeepUnknowns: true,
|
|
KeepSecrets: true,
|
|
KeepResources: true,
|
|
KeepOutputValues: true,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &pulumirpc.TransformInvokeResponse{
|
|
Args: mret,
|
|
Options: opts,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func pvApply(pv resource.PropertyValue, f func(resource.PropertyValue) resource.PropertyValue) resource.PropertyValue {
|
|
if pv.IsOutput() {
|
|
o := pv.OutputValue()
|
|
if !o.Known {
|
|
return pv
|
|
}
|
|
return resource.NewOutputProperty(resource.Output{
|
|
Element: f(o.Element),
|
|
Known: true,
|
|
Secret: o.Secret,
|
|
Dependencies: o.Dependencies,
|
|
})
|
|
}
|
|
return f(pv)
|
|
}
|
|
|
|
// Test that the engine invokes all transformation functions in the correct order.
|
|
func TestRemoteTransforms(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
callback1, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
props["foo"] = pvApply(props["foo"], func(v resource.PropertyValue) resource.PropertyValue {
|
|
return resource.NewNumberProperty(v.NumberValue() + 1)
|
|
})
|
|
// callback 2 should run before this one so "bar" should exist at this point
|
|
props["bar"] = resource.NewStringProperty(props["bar"].StringValue() + "baz")
|
|
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
callback2, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
props["foo"] = pvApply(props["foo"], func(v resource.PropertyValue) resource.PropertyValue {
|
|
return resource.NewNumberProperty(v.NumberValue() + 1)
|
|
})
|
|
props["bar"] = resource.NewStringProperty("bar")
|
|
// if this is for resB then callback 3 will have run before this one
|
|
if prop, has := props["frob"]; has {
|
|
props["frob"] = resource.MakeSecret(prop)
|
|
} else {
|
|
props["frob"] = resource.NewStringProperty("nofrob")
|
|
}
|
|
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
callback3, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
props["foo"] = pvApply(props["foo"], func(v resource.PropertyValue) resource.PropertyValue {
|
|
return resource.NewNumberProperty(v.NumberValue() + 1)
|
|
})
|
|
props["frob"] = resource.NewStringProperty("frob")
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback1)
|
|
require.NoError(t, err)
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
Transforms: []*pulumirpc.Callback{
|
|
callback2,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(10),
|
|
},
|
|
Transforms: []*pulumirpc.Callback{
|
|
callback3,
|
|
},
|
|
Parent: respA.URN,
|
|
})
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
// Skip display tests because secrets are serialized with the blinding crypter and can't be restored
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, snap.Resources, 3)
|
|
// Check Resources[1] is the resA resource
|
|
res := snap.Resources[1]
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA"), res.URN)
|
|
// Check it's final input properties match what we expected from the transformations
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(3),
|
|
"bar": resource.NewStringProperty("barbaz"),
|
|
"frob": resource.NewStringProperty("nofrob"),
|
|
}, res.Inputs)
|
|
|
|
// Check Resources[2] is the resB resource
|
|
res = snap.Resources[2]
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA$pkgA:m:typA::resB"), res.URN)
|
|
// Check it's final input properties match what we expected from the transformations
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(13),
|
|
"bar": resource.NewStringProperty("barbaz"),
|
|
"frob": resource.MakeSecret(resource.NewStringProperty("frob")),
|
|
}, res.Inputs)
|
|
}
|
|
|
|
// Test that the engine errors if a transformation function returns an unexpected response.
|
|
func TestRemoteTransformBadResponse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
callback1, err := callbacks.Allocate(func(args []byte) (proto.Message, error) {
|
|
// return the wrong message type
|
|
return &pulumirpc.RegisterResourceResponse{
|
|
Urn: "boom",
|
|
}, nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
assert.ErrorContains(t, err, "unmarshaling response: proto:")
|
|
assert.ErrorContains(t, err, "cannot parse invalid wire-format data")
|
|
return err
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.ErrorContains(t, err, "unmarshaling response: proto:")
|
|
assert.ErrorContains(t, err, "cannot parse invalid wire-format data")
|
|
assert.Len(t, snap.Resources, 0)
|
|
}
|
|
|
|
// Test that the engine errors if a transformation function returns an error.
|
|
func TestRemoteTransformErrorResponse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
callback1, err := callbacks.Allocate(func(args []byte) (proto.Message, error) {
|
|
return nil, errors.New("bad transform")
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
assert.ErrorContains(t, err, "Unknown desc = bad transform")
|
|
return err
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.ErrorContains(t, err, "Unknown desc = bad transform")
|
|
assert.Len(t, snap.Resources, 0)
|
|
}
|
|
|
|
// Test that a remote transform applies to a resource inside a component construct.
|
|
func TestRemoteTransformationsConstruct(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
assert.Equal(t, "pkgA:m:typC", string(req.Type))
|
|
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
callback1, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
if typ == "pkgA:m:typA" {
|
|
props["foo"] = pvApply(props["foo"], func(v resource.PropertyValue) resource.PropertyValue {
|
|
return resource.NewNumberProperty(v.NumberValue() + 1)
|
|
})
|
|
}
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typC", "resC", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, snap.Resources, 3)
|
|
// Check Resources[2] is the resA resource
|
|
res := snap.Resources[2]
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typC$pkgA:m:typA::resA"), res.URN)
|
|
// Check it's final input properties match what we expected from the transformations
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(2),
|
|
}, res.Inputs)
|
|
}
|
|
|
|
// Test that all options are passed and can be modified by a transformation function.
|
|
func TestRemoteTransformsOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{}, nil
|
|
}),
|
|
}
|
|
|
|
urnB := "urn:pulumi:test::test::pkgA:m:typA::resB"
|
|
urnC := "urn:pulumi:test::test::pkgA:m:typA::resC"
|
|
urnD := "urn:pulumi:test::test::pkgA:m:typA::resD"
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{})
|
|
require.NoError(t, err)
|
|
|
|
respC, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
|
|
Version: "1.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
callback1, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
// Check that the options are passed through correctly
|
|
assert.Equal(t, []string{"foo"}, opts.AdditionalSecretOutputs)
|
|
assert.Equal(t, urnB, opts.Aliases[0].Alias.(*pulumirpc.Alias_Urn).Urn)
|
|
assert.Equal(t, "16m40s", opts.CustomTimeouts.Create)
|
|
assert.Equal(t, "33m20s", opts.CustomTimeouts.Update)
|
|
assert.Equal(t, "50m0s", opts.CustomTimeouts.Delete)
|
|
assert.True(t, *opts.DeleteBeforeReplace)
|
|
assert.Equal(t, string(respA.URN), opts.DeletedWith)
|
|
assert.Equal(t, []string{string(respA.URN)}, opts.DependsOn)
|
|
assert.Equal(t, []string{"foo"}, opts.IgnoreChanges)
|
|
assert.Equal(t, "http://server", opts.PluginDownloadUrl)
|
|
assert.Equal(t, false, opts.Protect)
|
|
assert.Equal(t, []string{"foo"}, opts.ReplaceOnChanges)
|
|
assert.Equal(t, "2.0.0", opts.Version)
|
|
|
|
// Modify all the options
|
|
opts = &pulumirpc.TransformResourceOptions{
|
|
AdditionalSecretOutputs: []string{"bar"},
|
|
Aliases: []*pulumirpc.Alias{
|
|
{Alias: &pulumirpc.Alias_Urn{Urn: urnB}},
|
|
},
|
|
CustomTimeouts: &pulumirpc.RegisterResourceRequest_CustomTimeouts{
|
|
Create: "1s",
|
|
Update: "2s",
|
|
Delete: "3s",
|
|
},
|
|
DeleteBeforeReplace: nil,
|
|
DeletedWith: string(respC.URN),
|
|
DependsOn: []string{string(respC.URN)},
|
|
IgnoreChanges: []string{"bar"},
|
|
PluginDownloadUrl: "",
|
|
Protect: true,
|
|
ReplaceOnChanges: []string{"bar"},
|
|
Version: "1.0.0",
|
|
}
|
|
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback1)
|
|
require.NoError(t, err)
|
|
|
|
dbr := true
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resD", true, deploytest.ResourceOptions{
|
|
AdditionalSecretOutputs: []resource.PropertyKey{"foo"},
|
|
Aliases: []*pulumirpc.Alias{
|
|
{Alias: &pulumirpc.Alias_Urn{Urn: urnB}},
|
|
},
|
|
CustomTimeouts: &resource.CustomTimeouts{
|
|
Create: 1000,
|
|
Update: 2000,
|
|
Delete: 3000,
|
|
},
|
|
DeleteBeforeReplace: &dbr,
|
|
DeletedWith: respA.URN,
|
|
Dependencies: []resource.URN{respA.URN},
|
|
IgnoreChanges: []string{"foo"},
|
|
PluginDownloadURL: "http://server",
|
|
ReplaceOnChanges: []string{"foo"},
|
|
Version: "2.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, snap.Resources, 5)
|
|
// Check Resources[4] is the resD resource
|
|
res := snap.Resources[4]
|
|
require.Equal(t, resource.URN(urnD), res.URN)
|
|
assert.Equal(t, []resource.PropertyKey{"bar"}, res.AdditionalSecretOutputs)
|
|
assert.Equal(t, resource.CustomTimeouts{
|
|
Create: 1,
|
|
Update: 2,
|
|
Delete: 3,
|
|
}, res.CustomTimeouts)
|
|
assert.Equal(t, resource.URN(urnC), res.DeletedWith)
|
|
assert.Equal(t, true, res.Protect)
|
|
}
|
|
|
|
// Test that a transform can change the dependencies of a resource.
|
|
func TestRemoteTransformsDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "some-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
respA, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, respA.Outputs["foo"].IsNumber())
|
|
|
|
// Register a separate resource that
|
|
respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(10),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
callback, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
// props should be tracking that it depends on resB
|
|
assert.True(t, props["foo"].IsOutput())
|
|
assert.Equal(t, []resource.URN{respB.URN}, props["foo"].OutputValue().Dependencies)
|
|
|
|
// Add a dependency on resA
|
|
props["foo"] = resource.NewOutputProperty(resource.Output{
|
|
Element: respA.Outputs["foo"],
|
|
Known: true,
|
|
Dependencies: []resource.URN{respA.URN},
|
|
})
|
|
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
// Register a resource that initially depends on resB but the transform will turn to depend on resA
|
|
respC, err := monitor.RegisterResource(
|
|
"pkgA:m:typA", "resC", true, deploytest.ResourceOptions{
|
|
Inputs: resource.PropertyMap{
|
|
"foo": respB.Outputs["foo"],
|
|
},
|
|
PropertyDeps: map[resource.PropertyKey][]resource.URN{
|
|
"foo": {respB.URN},
|
|
},
|
|
Transforms: []*pulumirpc.Callback{
|
|
callback,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, respC.Outputs["foo"].IsNumber())
|
|
// This is a custom resource so no output dependencies
|
|
assert.Empty(t, respC.Dependencies)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, snap.Resources, 4)
|
|
// Check Resources[3] is the resC resource
|
|
res := snap.Resources[3]
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resC"), res.URN)
|
|
// Check it's final input properties match what we expected from the transformations
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
}, res.Inputs)
|
|
// Check the dependencies are as expected
|
|
assert.Equal(t, map[resource.PropertyKey][]resource.URN{
|
|
"foo": {resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA")},
|
|
}, res.PropertyDependencies)
|
|
assert.Equal(t, []resource.URN{
|
|
"urn:pulumi:test::test::pkgA:m:typA::resA",
|
|
}, res.Dependencies)
|
|
}
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/15843. Ensure that if a component resource has a
|
|
// transform that's saved and looked up by it's children.
|
|
func TestRemoteComponentTransforms(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
ConstructF: func(
|
|
_ context.Context,
|
|
req plugin.ConstructRequest,
|
|
monitor *deploytest.ResourceMonitor,
|
|
) (plugin.ConstructResponse, error) {
|
|
assert.Equal(t, "pkgA:m:typC", string(req.Type))
|
|
|
|
resp, err := monitor.RegisterResource(req.Type, req.Name, false, deploytest.ResourceOptions{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
Inputs: resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(1),
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return plugin.ConstructResponse{
|
|
URN: resp.URN,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, callbacks.Close()) }()
|
|
|
|
callback1, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
if typ == "pkgA:m:typA" {
|
|
props["foo"] = pvApply(props["foo"], func(v resource.PropertyValue) resource.PropertyValue {
|
|
return resource.NewNumberProperty(v.NumberValue() + 1)
|
|
})
|
|
}
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typC", "resC", false, deploytest.ResourceOptions{
|
|
Remote: true,
|
|
Transforms: []*pulumirpc.Callback{
|
|
callback1,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
}
|
|
|
|
project := p.GetProject()
|
|
snap, err := lt.TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, snap.Resources, 3)
|
|
// Check Resources[2] is the resA resource
|
|
res := snap.Resources[2]
|
|
assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typC$pkgA:m:typA::resA"), res.URN)
|
|
// Check it's final input properties match what we expected from the transformations
|
|
assert.Equal(t, resource.PropertyMap{
|
|
"foo": resource.NewNumberProperty(2),
|
|
}, res.Inputs)
|
|
}
|
|
|
|
func TestTransformsProviderOpt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
Package: "pkgA",
|
|
CreateF: func(_ context.Context, req plugin.CreateRequest) (plugin.CreateResponse, error) {
|
|
return plugin.CreateResponse{
|
|
ID: "some-id",
|
|
Properties: req.Properties,
|
|
Status: resource.StatusOK,
|
|
}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
var explicitProvider string
|
|
var implicitProvider string
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pulumi:providers:pkgA", "explicit", true)
|
|
require.NoError(t, err)
|
|
explicitProvider = string(resp.URN) + "::" + resp.ID.String()
|
|
|
|
resp, err = monitor.RegisterResource("pulumi:providers:pkgA", "implicit", true)
|
|
require.NoError(t, err)
|
|
implicitProvider = string(resp.URN) + "::" + resp.ID.String()
|
|
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
callback, err := callbacks.Allocate(
|
|
TransformFunction(func(name, typ string, custom bool, parent string,
|
|
props resource.PropertyMap, opts *pulumirpc.TransformResourceOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformResourceOptions, error) {
|
|
fmt.Println("provider: ", opts.Provider)
|
|
if opts.Provider == "" {
|
|
opts.Provider = implicitProvider
|
|
}
|
|
|
|
return props, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackTransform(callback)
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "explicitProvider", true, deploytest.ResourceOptions{
|
|
Provider: explicitProvider,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "implicitProvider", true)
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "explicitProvidersMap", true, deploytest.ResourceOptions{
|
|
Providers: map[string]string{"pkgA": explicitProvider},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
resp, err = monitor.RegisterResource("xmy:component:resource", "component", false, deploytest.ResourceOptions{
|
|
Providers: map[string]string{"pkgA": explicitProvider},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "parentedResource", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
resp, err = monitor.RegisterResource("ymy:component:resource", "another-component", false, deploytest.ResourceOptions{
|
|
Provider: explicitProvider,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = monitor.RegisterResource("pkgA:m:typA", "parentedResource", true, deploytest.ResourceOptions{
|
|
Parent: resp.URN,
|
|
})
|
|
require.NoError(t, err)
|
|
return nil
|
|
})
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
},
|
|
},
|
|
}
|
|
snap := p.Run(t, nil)
|
|
assert.NotNil(t, snap)
|
|
assert.Equal(t, 9, len(snap.Resources)) // 2 providers + 7 resources
|
|
sort.Slice(snap.Resources, func(i, j int) bool {
|
|
return snap.Resources[i].URN < snap.Resources[j].URN
|
|
})
|
|
assert.Equal(t, urn.URN("urn:pulumi:test::test::pkgA:m:typA::explicitProvider"), snap.Resources[0].URN)
|
|
assert.Equal(t, explicitProvider, snap.Resources[0].Provider)
|
|
assert.Equal(t, urn.URN("urn:pulumi:test::test::pkgA:m:typA::explicitProvidersMap"), snap.Resources[1].URN)
|
|
assert.Equal(t, explicitProvider, snap.Resources[1].Provider)
|
|
assert.Equal(t, urn.URN("urn:pulumi:test::test::pkgA:m:typA::implicitProvider"), snap.Resources[2].URN)
|
|
assert.Equal(t, implicitProvider, snap.Resources[2].Provider)
|
|
assert.Equal(t,
|
|
urn.URN("urn:pulumi:test::test::xmy:component:resource$pkgA:m:typA::parentedResource"),
|
|
snap.Resources[5].URN)
|
|
assert.Equal(t, explicitProvider, snap.Resources[5].Provider)
|
|
assert.Equal(t,
|
|
urn.URN("urn:pulumi:test::test::ymy:component:resource$pkgA:m:typA::parentedResource"),
|
|
snap.Resources[7].URN)
|
|
assert.Equal(t, implicitProvider, snap.Resources[7].Provider)
|
|
}
|
|
|
|
func TestTransformInvoke(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
Package: "pkgA",
|
|
InvokeF: func(_ context.Context, req plugin.InvokeRequest) (plugin.InvokeResponse, error) {
|
|
return plugin.InvokeResponse{Properties: req.Args}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
var implicitProvider string
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pulumi:providers:pkgA", "implicit", true)
|
|
require.NoError(t, err)
|
|
implicitProvider = string(resp.URN) + "::" + resp.ID.String()
|
|
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
callback, err := callbacks.Allocate(
|
|
TransformInvokeFunction(func(token string,
|
|
args resource.PropertyMap, opts *pulumirpc.TransformInvokeOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformInvokeOptions, error) {
|
|
args["foo"] = resource.NewStringProperty("bar")
|
|
|
|
return args, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackInvokeTransform(callback)
|
|
require.NoError(t, err)
|
|
|
|
input := resource.PropertyMap{
|
|
"foo": resource.NewStringProperty("baz"),
|
|
"bar": resource.NewStringProperty("qux"),
|
|
}
|
|
|
|
result, _, err := monitor.Invoke("pkgA:m:typA", input, implicitProvider, "0.0.0", "")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "bar", result["foo"].StringValue())
|
|
assert.Equal(t, "qux", result["bar"].StringValue())
|
|
return nil
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
},
|
|
},
|
|
}
|
|
_ = p.Run(t, nil)
|
|
}
|
|
|
|
func TestTransformInvokeTransformProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
loaders := []*deploytest.ProviderLoader{
|
|
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
|
|
return &deploytest.Provider{
|
|
Package: "pkgA",
|
|
InvokeF: func(_ context.Context, req plugin.InvokeRequest) (plugin.InvokeResponse, error) {
|
|
return plugin.InvokeResponse{Properties: req.Args}, nil
|
|
},
|
|
}, nil
|
|
}),
|
|
}
|
|
|
|
var implicitProvider string
|
|
programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
|
|
resp, err := monitor.RegisterResource("pulumi:providers:pkgA", "implicit", true)
|
|
require.NoError(t, err)
|
|
implicitProvider = string(resp.URN) + "::" + resp.ID.String()
|
|
|
|
callbacks, err := deploytest.NewCallbacksServer()
|
|
require.NoError(t, err)
|
|
callback, err := callbacks.Allocate(
|
|
TransformInvokeFunction(func(token string,
|
|
args resource.PropertyMap, opts *pulumirpc.TransformInvokeOptions,
|
|
) (resource.PropertyMap, *pulumirpc.TransformInvokeOptions, error) {
|
|
if opts.Provider == "" {
|
|
opts.Provider = implicitProvider
|
|
}
|
|
|
|
return args, opts, nil
|
|
}))
|
|
require.NoError(t, err)
|
|
|
|
err = monitor.RegisterStackInvokeTransform(callback)
|
|
require.NoError(t, err)
|
|
|
|
input := resource.PropertyMap{}
|
|
|
|
_, _, err = monitor.Invoke("pkgA:m:typA", input, "", "", "")
|
|
require.NoError(t, err)
|
|
|
|
return nil
|
|
})
|
|
|
|
hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...)
|
|
p := <.TestPlan{
|
|
Options: lt.TestUpdateOptions{T: t, HostF: hostF},
|
|
Steps: []lt.TestStep{
|
|
{
|
|
Op: Update,
|
|
},
|
|
},
|
|
}
|
|
snap := p.Run(t, nil)
|
|
assert.NotNil(t, snap)
|
|
assert.Equal(t, 1, len(snap.Resources)) // expect no default provider to be created for the invoke
|
|
}
|