// Copyright 2016-2022, 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 "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: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, } project := p.GetProject() snap, err := 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: TestUpdateOptions{T: t, HostF: hostF}, Steps: []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: TestUpdateOptions{T: t, HostF: hostF}, Steps: []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: TestUpdateOptions{T: t, HostF: hostF}, Steps: []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 }