diff --git a/go.mod b/go.mod index 9a9fbcfd..59d99c47 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,13 @@ require ( github.com/hexops/autogold v1.3.0 github.com/iancoleman/strcase v0.2.0 github.com/pkg/errors v0.9.1 - github.com/pulumi/pulumi/pkg/v3 v3.129.1-0.20240819035819-d75251c49f8a - github.com/pulumi/pulumi/sdk/v3 v3.129.1-0.20240819035819-d75251c49f8a + github.com/pulumi/pulumi/pkg/v3 v3.130.1-0.20240831132520-d9df47718158 + github.com/pulumi/pulumi/sdk/v3 v3.130.1-0.20240831132520-d9df47718158 github.com/spf13/afero v1.9.5 github.com/stretchr/testify v1.9.0 github.com/zclconf/go-cty v1.13.2 google.golang.org/grpc v1.63.2 + google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -186,7 +187,6 @@ require ( google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect lukechampine.com/frand v1.4.2 // indirect mvdan.cc/gofumpt v0.5.0 // indirect diff --git a/go.sum b/go.sum index bc57eadb..03fe3873 100644 --- a/go.sum +++ b/go.sum @@ -430,10 +430,10 @@ github.com/pulumi/esc v0.9.1 h1:HH5eEv8sgyxSpY5a8yePyqFXzA8cvBvapfH8457+mIs= github.com/pulumi/esc v0.9.1/go.mod h1:oEJ6bOsjYlQUpjf70GiX+CXn3VBmpwFDxUTlmtUN84c= github.com/pulumi/inflector v0.1.1 h1:dvlxlWtXwOJTUUtcYDvwnl6Mpg33prhK+7mzeF+SobA= github.com/pulumi/inflector v0.1.1/go.mod h1:HUFCjcPTz96YtTuUlwG3i3EZG4WlniBvR9bd+iJxCUY= -github.com/pulumi/pulumi/pkg/v3 v3.129.1-0.20240819035819-d75251c49f8a h1:0+5MrobQVNIJRXxeeVEgNsnS8jVYxwm390K5wuboS+Y= -github.com/pulumi/pulumi/pkg/v3 v3.129.1-0.20240819035819-d75251c49f8a/go.mod h1:W3c7JgO064kUH5IfyQMCgmsAr5iwr3PO6KEeIQiO0dY= -github.com/pulumi/pulumi/sdk/v3 v3.129.1-0.20240819035819-d75251c49f8a h1:1k4pn6Ef3Ts5C6UMKpQGcVzs6GmfHOJWW76wVaAUiKE= -github.com/pulumi/pulumi/sdk/v3 v3.129.1-0.20240819035819-d75251c49f8a/go.mod h1:p1U24en3zt51agx+WlNboSOV8eLlPWYAkxMzVEXKbnY= +github.com/pulumi/pulumi/pkg/v3 v3.130.1-0.20240831132520-d9df47718158 h1:X7NKRRMROkOjou/WYdhH5JhwqFm0QUI98eVBfO5y0zs= +github.com/pulumi/pulumi/pkg/v3 v3.130.1-0.20240831132520-d9df47718158/go.mod h1:8Wt6MyH3CO7ssKjYOuhmZHjV/H8oCeSCKe8jjVskuDo= +github.com/pulumi/pulumi/sdk/v3 v3.130.1-0.20240831132520-d9df47718158 h1:FF8TMAxHXhLPL+ug2M3gulInEULONmg9UL9ajRbdVIc= +github.com/pulumi/pulumi/sdk/v3 v3.130.1-0.20240831132520-d9df47718158/go.mod h1:p1U24en3zt51agx+WlNboSOV8eLlPWYAkxMzVEXKbnY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= diff --git a/pkg/pulumiyaml/packages.go b/pkg/pulumiyaml/packages.go index 6071ccce..b49f04df 100644 --- a/pkg/pulumiyaml/packages.go +++ b/pkg/pulumiyaml/packages.go @@ -18,6 +18,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" ) type ResourceTypeToken string @@ -86,8 +87,8 @@ func (l packageLoader) Close() { } } -func NewPackageLoader() (PackageLoader, error) { - host, err := newResourcePackageHost() +func NewPackageLoader(plugins *workspace.Plugins) (PackageLoader, error) { + host, err := newResourcePackageHost(plugins) if err != nil { return nil, err } @@ -466,7 +467,7 @@ func getResourceConstants(props []*schema.Property) map[string]interface{} { return constantProps } -func newResourcePackageHost() (plugin.Host, error) { +func newResourcePackageHost(plugins *workspace.Plugins) (plugin.Host, error) { cwd, err := os.Getwd() if err != nil { return nil, err @@ -474,7 +475,7 @@ func newResourcePackageHost() (plugin.Host, error) { sink := diag.DefaultSink(os.Stderr, os.Stderr, diag.FormatOptions{ Color: cmdutil.GetGlobalColorization(), }) - pluginCtx, err := plugin.NewContext(sink, sink, nil, nil, cwd, nil, true, nil) + pluginCtx, err := plugin.NewContextWithRoot(sink, sink, nil, cwd, cwd, nil, true, nil, plugins, nil, nil) if err != nil { return nil, err } diff --git a/pkg/server/server.go b/pkg/server/server.go index 554447cb..69243b05 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/version" + "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" @@ -135,6 +136,15 @@ func (host *yamlLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest fmt.Sprintf(`PULUMI_CONFIG=%s`, jsonConfigValue), } + projPath, err := workspace.DetectProjectPathFrom(req.Info.RootDirectory) + if err != nil { + return nil, err + } + proj, err := workspace.LoadProject(projPath) + if err != nil { + return nil, err + } + template, diags, err := host.loadTemplate(compilerEnv) if err != nil { return &pulumirpc.RunResponse{Error: err.Error()}, nil @@ -166,7 +176,7 @@ func (host *yamlLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest ConfigSecretKeys: req.GetConfigSecretKeys(), ConfigPropertyMap: confPropMap, Organization: req.Organization, - Parallel: int(req.GetParallel()), + Parallel: req.GetParallel(), DryRun: req.GetDryRun(), MonitorAddr: req.GetMonitorAddress(), EngineAddr: host.engineAddress, @@ -177,7 +187,7 @@ func (host *yamlLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest defer pctx.Close() // Now instruct the Pulumi Go SDK to run the pulumi YAML interpreter. if err := pulumi.RunWithContext(pctx, func(ctx *pulumi.Context) error { - loader, err := pulumiyaml.NewPackageLoader() + loader, err := pulumiyaml.NewPackageLoader(proj.Plugins) if err != nil { return err } diff --git a/pkg/tests/integration_test.go b/pkg/tests/integration_test.go index 2c5b57fe..6831d8ae 100644 --- a/pkg/tests/integration_test.go +++ b/pkg/tests/integration_test.go @@ -121,3 +121,15 @@ func TestEnvVarsKeepConflictingValues(t *testing.T) { } integration.ProgramTest(t, &testOptions) } + +// Test a local provider plugin. +// +//nolint:paralleltest // ProgramTest calls t.Parallel() +func TestLocalPlugin(t *testing.T) { + integration.ProgramTest(t, &integration.ProgramTestOptions{ + Dir: filepath.Join("testdata", "local"), + LocalProviders: []integration.LocalDependency{ + {Package: "testprovider", Path: "testprovider"}, + }, + }) +} diff --git a/pkg/tests/testdata/local/Pulumi.yaml b/pkg/tests/testdata/local/Pulumi.yaml new file mode 100644 index 00000000..5230f66a --- /dev/null +++ b/pkg/tests/testdata/local/Pulumi.yaml @@ -0,0 +1,10 @@ +name: local +scription: An integration test showing the use of a a local plugin +runtime: yaml +resources: + res1: + type: testprovider:index:Random + properties: + length: 10 + res2: + type: testprovider:index:Echo \ No newline at end of file diff --git a/pkg/tests/testprovider/.gitignore b/pkg/tests/testprovider/.gitignore new file mode 100644 index 00000000..f4015fe7 --- /dev/null +++ b/pkg/tests/testprovider/.gitignore @@ -0,0 +1,2 @@ +pulumi-resource-testprovider +schema-testprovider.json \ No newline at end of file diff --git a/pkg/tests/testprovider/PulumiPlugin.yaml b/pkg/tests/testprovider/PulumiPlugin.yaml new file mode 100644 index 00000000..735ef965 --- /dev/null +++ b/pkg/tests/testprovider/PulumiPlugin.yaml @@ -0,0 +1 @@ +runtime: go \ No newline at end of file diff --git a/pkg/tests/testprovider/component.go b/pkg/tests/testprovider/component.go new file mode 100644 index 00000000..3881a88e --- /dev/null +++ b/pkg/tests/testprovider/component.go @@ -0,0 +1,101 @@ +// Copyright 2016-2021, 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. +//go:build !all +// +build !all + +package main + +import ( + "errors" + "fmt" + "reflect" + + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +type Random struct { + pulumi.CustomResourceState + + Length pulumi.IntOutput `pulumi:"length"` + Result pulumi.StringOutput `pulumi:"result"` +} + +func NewRandom(ctx *pulumi.Context, + name string, args *RandomArgs, opts ...pulumi.ResourceOption, +) (*Random, error) { + if args == nil || args.Length == nil { + return nil, errors.New("missing required argument 'Length'") + } + var resource Random + err := ctx.RegisterResource("testprovider:index:Random", name, args, &resource, opts...) + if err != nil { + return nil, err + } + return &resource, nil +} + +type randomArgs struct { + Length int `pulumi:"length"` + Prefix string `pulumi:"prefix"` +} + +type RandomArgs struct { + Length pulumi.IntInput + Prefix pulumi.StringInput +} + +func (RandomArgs) ElementType() reflect.Type { + return reflect.TypeOf((*randomArgs)(nil)).Elem() +} + +type Component struct { + pulumi.ResourceState + + ChildID pulumi.IDOutput `pulumi:"childId"` +} + +type ComponentArgs struct { + Length int `pulumi:"length"` +} + +func NewComponent(ctx *pulumi.Context, name string, args *ComponentArgs, + opts ...pulumi.ResourceOption, +) (*Component, error) { + if args == nil { + return nil, errors.New("args is required") + } + + component := &Component{} + err := ctx.RegisterComponentResource("testprovider:index:Component", name, component, opts...) + if err != nil { + return nil, err + } + + res, err := NewRandom(ctx, fmt.Sprintf("child-%s", name), &RandomArgs{ + Length: pulumi.Int(args.Length), + }, pulumi.Parent(component)) + if err != nil { + return nil, err + } + + component.ChildID = res.ID() + + if err := ctx.RegisterResourceOutputs(component, pulumi.Map{ + "childId": component.ChildID, + }); err != nil { + return nil, err + } + + return component, nil +} diff --git a/pkg/tests/testprovider/echo.go b/pkg/tests/testprovider/echo.go new file mode 100644 index 00000000..c9d5b05f --- /dev/null +++ b/pkg/tests/testprovider/echo.go @@ -0,0 +1,222 @@ +// Copyright 2016-2021, 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. +//go:build !all +// +build !all + +package main + +import ( + "context" + "os" + "strconv" + + pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" + rpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "google.golang.org/protobuf/types/known/emptypb" +) + +func init() { + providerSchema.Resources["testprovider:index:Echo"] = pschema.ResourceSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "A test resource that echoes its input.", + Properties: map[string]pschema.PropertySpec{ + "echo": { + TypeSpec: pschema.TypeSpec{ + Ref: "pulumi.json#/Any", + }, + Description: "Input to echo.", + }, + }, + Type: "object", + }, + InputProperties: map[string]pschema.PropertySpec{ + "echo": { + TypeSpec: pschema.TypeSpec{ + Ref: "pulumi.json#/Any", + }, + Description: "An echoed input.", + }, + }, + Methods: map[string]string{ + "doEchoMethod": "testprovider:index:Echo/doEchoMethod", + }, + } + providerSchema.Functions["testprovider:index:doEcho"] = pschema.FunctionSpec{ + Description: "A test invoke that echoes its input.", + Inputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "echo": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + Outputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "echo": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + } + if os.Getenv("PULUMI_TEST_MULTI_ARGUMENT_INPUTS") != "" { + // Conditionally add this if an env flag is set, since it does not work with all langs + providerSchema.Functions["testprovider:index:doMultiEcho"] = pschema.FunctionSpec{ + Description: "A test invoke that echoes its input, using multiple inputs.", + MultiArgumentInputs: []string{ + "echoA", + "echoB", + }, + Inputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "echoA": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + "echoB": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + Outputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "echoA": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + "echoB": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + } + } + providerSchema.Functions["testprovider:index:Echo/doEchoMethod"] = pschema.FunctionSpec{ + Description: "A test call that echoes its input.", + Inputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "__self__": { + TypeSpec: pschema.TypeSpec{ + Ref: "#/types/testprovider:index:Echo", + }, + }, + "echo": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + Outputs: &pschema.ObjectTypeSpec{ + Properties: map[string]pschema.PropertySpec{ + "echo": { + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + }, + } +} + +type echoProvider struct { + id int +} + +func (p *echoProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + return &rpc.CheckResponse{Inputs: req.News, Failures: nil}, nil +} + +func (p *echoProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + olds, err := plugin.UnmarshalProperties(req.GetOlds(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}) + if err != nil { + return nil, err + } + + news, err := plugin.UnmarshalProperties(req.GetNews(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}) + if err != nil { + return nil, err + } + + d := olds.Diff(news) + changes := rpc.DiffResponse_DIFF_NONE + var replaces []string + if d != nil && d.Changed("echo") { + changes = rpc.DiffResponse_DIFF_SOME + replaces = append(replaces, "echo") + } + + return &rpc.DiffResponse{ + Changes: changes, + Replaces: replaces, + }, nil +} + +func (p *echoProvider) Create(ctx context.Context, req *rpc.CreateRequest) (*rpc.CreateResponse, error) { + inputs, err := plugin.UnmarshalProperties(req.GetProperties(), plugin.MarshalOptions{ + KeepUnknowns: true, + SkipNulls: true, + }) + if err != nil { + return nil, err + } + + outputProperties, err := plugin.MarshalProperties( + inputs, + plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}, + ) + if err != nil { + return nil, err + } + + p.id++ + return &rpc.CreateResponse{ + Id: strconv.Itoa(p.id), + Properties: outputProperties, + }, nil +} + +func (p *echoProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) { + return &rpc.ReadResponse{ + Id: req.Id, + Properties: req.Properties, + }, nil +} + +func (p *echoProvider) Update(ctx context.Context, req *rpc.UpdateRequest) (*rpc.UpdateResponse, error) { + panic("Update not implemented") +} + +func (p *echoProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + +func (p *echoProvider) Invoke(ctx context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) { + return &rpc.InvokeResponse{Return: req.Args}, nil +} + +func (p *echoProvider) Call(ctx context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) { + return &rpc.CallResponse{Return: req.Args}, nil +} diff --git a/pkg/tests/testprovider/fails_on_create.go b/pkg/tests/testprovider/fails_on_create.go new file mode 100644 index 00000000..6221c2b9 --- /dev/null +++ b/pkg/tests/testprovider/fails_on_create.go @@ -0,0 +1,83 @@ +// Copyright 2016-2023, 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. +//go:build !all +// +build !all + +package main + +import ( + "context" + "errors" + + pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + rpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "google.golang.org/protobuf/types/known/emptypb" +) + +func init() { + providerSchema.Resources["testprovider:index:FailsOnCreate"] = pschema.ResourceSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "A test resource fails on create.", + Properties: map[string]pschema.PropertySpec{}, + Type: "object", + }, + InputProperties: map[string]pschema.PropertySpec{}, + } +} + +type failsOnCreateProvider struct{} + +func (p *failsOnCreateProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + return &rpc.CheckResponse{Inputs: req.News, Failures: nil}, nil +} + +func (p *failsOnCreateProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + return &rpc.DiffResponse{ + Changes: rpc.DiffResponse_DIFF_NONE, + }, nil +} + +func (p *failsOnCreateProvider) Create( + ctx context.Context, req *rpc.CreateRequest, +) (*rpc.CreateResponse, error) { + return nil, errors.New("Create always fails for the FailsOnCreate resource") +} + +func (p *failsOnCreateProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) { + return &rpc.ReadResponse{ + Id: req.Id, + Properties: req.Properties, + }, nil +} + +func (p *failsOnCreateProvider) Update( + ctx context.Context, req *rpc.UpdateRequest, +) (*rpc.UpdateResponse, error) { + panic("Update not implemented") +} + +func (p *failsOnCreateProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + +func (p *failsOnCreateProvider) Invoke(ctx context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) { + // The fails-on-create provider doesn't support any invokes currently. + panic("Invoke not implemented") +} + +func (p *failsOnCreateProvider) Call(ctx context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) { + // The random provider doesn't support any call currently. + panic("Call not implemented") +} diff --git a/pkg/tests/testprovider/fails_on_delete.go b/pkg/tests/testprovider/fails_on_delete.go new file mode 100644 index 00000000..632fa412 --- /dev/null +++ b/pkg/tests/testprovider/fails_on_delete.go @@ -0,0 +1,89 @@ +// Copyright 2016-2023, 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. +//go:build !all +// +build !all + +package main + +import ( + "context" + "errors" + "strconv" + + pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + rpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "google.golang.org/protobuf/types/known/emptypb" +) + +func init() { + providerSchema.Resources["testprovider:index:FailsOnDelete"] = pschema.ResourceSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "A test resource fails on delete.", + Properties: map[string]pschema.PropertySpec{}, + Type: "object", + }, + InputProperties: map[string]pschema.PropertySpec{}, + } +} + +type failsOnDeleteProvider struct { + id int +} + +func (p *failsOnDeleteProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + return &rpc.CheckResponse{Inputs: req.News, Failures: nil}, nil +} + +func (p *failsOnDeleteProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + return &rpc.DiffResponse{ + Changes: rpc.DiffResponse_DIFF_NONE, + }, nil +} + +func (p *failsOnDeleteProvider) Create( + ctx context.Context, req *rpc.CreateRequest, +) (*rpc.CreateResponse, error) { + p.id++ + return &rpc.CreateResponse{ + Id: strconv.Itoa(p.id), + }, nil +} + +func (p *failsOnDeleteProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) { + return &rpc.ReadResponse{ + Id: req.Id, + Properties: req.Properties, + }, nil +} + +func (p *failsOnDeleteProvider) Update( + ctx context.Context, req *rpc.UpdateRequest, +) (*rpc.UpdateResponse, error) { + panic("Update not implemented") +} + +func (p *failsOnDeleteProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { + return nil, errors.New("Delete always fails for the FailsOnDelete resource") +} + +func (p *failsOnDeleteProvider) Invoke(ctx context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) { + // The fails-on-delete provider doesn't support any invokes currently. + panic("Invoke not implemented") +} + +func (p *failsOnDeleteProvider) Call(ctx context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) { + // The random provider doesn't support any call currently. + panic("Call not implemented") +} diff --git a/pkg/tests/testprovider/main.go b/pkg/tests/testprovider/main.go new file mode 100644 index 00000000..a2f737b4 --- /dev/null +++ b/pkg/tests/testprovider/main.go @@ -0,0 +1,370 @@ +// Copyright 2016-2023, 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. +//go:build !all +// +build !all + +// A provider with resources for use in tests. +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + "github.com/pulumi/pulumi/pkg/v3/resource/provider" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + pulumiprovider "github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider" + rpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + providerName = "testprovider" + version = "0.0.1" +) + +var providerSchema = pschema.PackageSpec{ + Name: "testprovider", + Description: "A test provider.", + DisplayName: "testprovider", + + Config: pschema.ConfigSpec{}, + + Provider: pschema.ResourceSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "The provider type for the testprovider package.", + Type: "object", + }, + InputProperties: map[string]pschema.PropertySpec{}, + }, + + Types: map[string]pschema.ComplexTypeSpec{}, + Resources: map[string]pschema.ResourceSpec{}, + Functions: map[string]pschema.FunctionSpec{}, + Language: map[string]pschema.RawMessage{}, +} + +// Minimal set of methods to implement a basic provider. +type testProvider interface { + Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) + Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) + Create(ctx context.Context, req *rpc.CreateRequest) (*rpc.CreateResponse, error) + Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) + Update(ctx context.Context, req *rpc.UpdateRequest) (*rpc.UpdateResponse, error) + Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) + Invoke(ctx context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) + Call(ctx context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) +} + +var testProviders = func() map[string]testProvider { + ep := &echoProvider{} + + testProviders := map[string]testProvider{ + "testprovider:index:Random": &randomProvider{}, + "testprovider:index:Echo": ep, + "testprovider:index:Echo/doEchoMethod": ep, + "testprovider:index:doEcho": ep, + "testprovider:index:doMultiEcho": ep, + "testprovider:index:FailsOnDelete": &failsOnDeleteProvider{}, + "testprovider:index:FailsOnCreate": &failsOnCreateProvider{}, + } + + return testProviders +}() + +func providerForURN(urn string) (testProvider, string, bool) { + ty := string(resource.URN(urn).Type()) + provider, ok := testProviders[ty] + return provider, ty, ok +} + +//nolint:unused,deadcode +func main() { + if err := provider.Main(providerName, func(host *provider.HostClient) (rpc.ResourceProviderServer, error) { + return makeProvider(host, providerName, version) + }); err != nil { + cmdutil.ExitError(err.Error()) + } +} + +type testproviderProvider struct { + rpc.UnimplementedResourceProviderServer + + parameter string + + host *provider.HostClient + name string + version string +} + +func makeProvider(host *provider.HostClient, name, version string) (rpc.ResourceProviderServer, error) { + // Return the new provider + return &testproviderProvider{ + host: host, + name: name, + version: version, + }, nil +} + +// CheckConfig validates the configuration for this provider. +func (p *testproviderProvider) CheckConfig(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + return &rpc.CheckResponse{Inputs: req.GetNews()}, nil +} + +// DiffConfig diffs the configuration for this provider. +func (p *testproviderProvider) DiffConfig(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + return &rpc.DiffResponse{}, nil +} + +// Configure configures the resource provider with "globals" that control its behavior. +func (p *testproviderProvider) Configure(_ context.Context, req *rpc.ConfigureRequest) (*rpc.ConfigureResponse, error) { + return &rpc.ConfigureResponse{ + AcceptSecrets: true, + }, nil +} + +func (p *testproviderProvider) Parameterize(_ context.Context, req *rpc.ParameterizeRequest) (*rpc.ParameterizeResponse, error) { + switch params := req.GetParameters().(type) { + case *rpc.ParameterizeRequest_Args: + args := params.Args.Args + if len(args) != 1 { + return nil, fmt.Errorf("expected exactly one argument") + } + p.parameter = args[0] + case *rpc.ParameterizeRequest_Value: + val := string(params.Value.Value) + if val == "" { + return nil, fmt.Errorf("expected a non-empty string value") + } + p.parameter = val + default: + return nil, fmt.Errorf("unexpected parameter type") + } + + for k, prov := range testProviders { + testProviders[strings.Replace(k, "testprovider", p.parameter, 1)] = prov + } + + return &rpc.ParameterizeResponse{ + Name: p.parameter, + Version: version, + }, nil +} + +// Invoke dynamically executes a built-in function in the provider. +func (p *testproviderProvider) Invoke(_ context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) { + if p, ok := testProviders[req.GetTok()]; ok { + return p.Invoke(context.Background(), req) + } + + tok := req.GetTok() + if tok == "testprovider:index:returnArgs" { + return &rpc.InvokeResponse{ + Return: req.Args, + }, nil + } + return nil, fmt.Errorf("Unknown Invoke token '%s'", tok) +} + +// StreamInvoke dynamically executes a built-in function in the provider. The result is streamed +// back as a series of messages. +func (p *testproviderProvider) StreamInvoke(req *rpc.InvokeRequest, + server rpc.ResourceProvider_StreamInvokeServer, +) error { + tok := req.GetTok() + return fmt.Errorf("Unknown StreamInvoke token '%s'", tok) +} + +func (p *testproviderProvider) Call(_ context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) { + tok := req.GetTok() + + if p, ok := testProviders[tok]; ok { + return p.Call(context.Background(), req) + } + + return nil, fmt.Errorf("Unknown Call token '%s'", tok) +} + +func (p *testproviderProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Check(ctx, req) +} + +// Diff checks what impacts a hypothetical update will have on the resource's properties. +func (p *testproviderProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Diff(ctx, req) +} + +// Create allocates a new instance of the provided resource and returns its unique ID afterwards. +func (p *testproviderProvider) Create(ctx context.Context, req *rpc.CreateRequest) (*rpc.CreateResponse, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Create(ctx, req) +} + +// Read the current live state associated with a resource. +func (p *testproviderProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Read(ctx, req) +} + +// Update updates an existing resource with new values. +func (p *testproviderProvider) Update(ctx context.Context, req *rpc.UpdateRequest) (*rpc.UpdateResponse, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Update(ctx, req) +} + +// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed +// to still exist. +func (p *testproviderProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { + provider, ty, ok := providerForURN(req.GetUrn()) + if !ok { + return nil, fmt.Errorf("Unknown resource type '%s'", ty) + } + return provider.Delete(ctx, req) +} + +// Construct creates a new component resource. +func (p *testproviderProvider) Construct(ctx context.Context, req *rpc.ConstructRequest) (*rpc.ConstructResponse, error) { + if req.Type != "testprovider:index:Component" { + return nil, fmt.Errorf("unknown resource type %s", req.Type) + } + + return pulumiprovider.Construct( + ctx, req, p.host.EngineConn(), + func(ctx *pulumi.Context, typ, name string, inputs pulumiprovider.ConstructInputs, + options pulumi.ResourceOption, + ) (*pulumiprovider.ConstructResult, error) { + args := &ComponentArgs{} + if err := inputs.CopyTo(args); err != nil { + return nil, fmt.Errorf("setting args: %w", err) + } + + component, err := NewComponent(ctx, name, args, options) + if err != nil { + return nil, err + } + + return pulumiprovider.NewConstructResult(component) + }) +} + +// GetPluginInfo returns generic information about this plugin, like its version. +func (p *testproviderProvider) GetPluginInfo(context.Context, *emptypb.Empty) (*rpc.PluginInfo, error) { + return &rpc.PluginInfo{ + Version: p.version, + }, nil +} + +func (p *testproviderProvider) Attach(ctx context.Context, req *rpc.PluginAttach) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + +// GetSchema returns the JSON-serialized schema for the provider. +func (p *testproviderProvider) GetSchema(ctx context.Context, + req *rpc.GetSchemaRequest, +) (*rpc.GetSchemaResponse, error) { + makeJSONString := func(v any) ([]byte, error) { + var out bytes.Buffer + encoder := json.NewEncoder(&out) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + if err := encoder.Encode(v); err != nil { + return nil, err + } + return out.Bytes(), nil + } + + sch := providerSchema + // If we have a parameter, we'll return a copy of the provider's resources and + // functions -- this is just enough to test that the engine is calling + // Parameterize and GetSchema correctly. + if req.SubpackageName != "" { + if req.SubpackageName == p.parameter { + sch = pschema.PackageSpec{ + Name: p.parameter, + Version: "1.0.0", + Parameterization: &pschema.ParameterizationSpec{ + BaseProvider: pschema.BaseProviderSpec{ + Name: "testprovider", + Version: version, + }, + Parameter: []byte(p.parameter), + }, + Resources: map[string]pschema.ResourceSpec{}, + Functions: map[string]pschema.FunctionSpec{}, + } + + for k, r := range providerSchema.Resources { + sch.Resources[strings.Replace(k, "testprovider", p.parameter, 1)] = r + for k, m := range r.Methods { + r.Methods[k] = strings.Replace(m, "testprovider", p.parameter, 1) + } + } + for k, f := range providerSchema.Functions { + sch.Functions[strings.Replace(k, "testprovider", p.parameter, 1)] = f + for k, prop := range f.Inputs.Properties { + if prop.TypeSpec.Ref != "" { + prop.TypeSpec.Ref = strings.Replace(prop.TypeSpec.Ref, "testprovider", p.parameter, 1) + f.Inputs.Properties[k] = prop + } + } + } + } else { + return nil, fmt.Errorf("expected subpackage %s", req.SubpackageName) + } + } + + schemaJSON, err := makeJSONString(sch) + if err != nil { + return nil, err + } + return &rpc.GetSchemaResponse{ + Schema: string(schemaJSON), + }, nil +} + +// Cancel signals the provider to gracefully shut down and abort any ongoing resource operations. +// Operations aborted in this way will return an error (e.g., `Update` and `Create` will either a +// creation error or an initialization error). Since Cancel is advisory and non-blocking, it is up +// to the host to decide how long to wait after Cancel is called before (e.g.) +// hard-closing any gRPC connection. +func (p *testproviderProvider) Cancel(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil +} + +func (p *testproviderProvider) GetMapping(context.Context, *rpc.GetMappingRequest) (*rpc.GetMappingResponse, error) { + return &rpc.GetMappingResponse{}, nil +} diff --git a/pkg/tests/testprovider/random.go b/pkg/tests/testprovider/random.go new file mode 100644 index 00000000..c0b4132b --- /dev/null +++ b/pkg/tests/testprovider/random.go @@ -0,0 +1,192 @@ +// Copyright 2016-2021, 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. +//go:build !all +// +build !all + +package main + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + + pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" + rpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "google.golang.org/protobuf/types/known/emptypb" +) + +func init() { + providerSchema.Resources["testprovider:index:Random"] = pschema.ResourceSpec{ + ObjectTypeSpec: pschema.ObjectTypeSpec{ + Description: "A test resource that generates a random string of a given length and with an optional prefix.", + Properties: map[string]pschema.PropertySpec{ + "length": { + TypeSpec: pschema.TypeSpec{Type: "integer"}, + Description: "The length of the random string (not including the prefix, if any).", + }, + "prefix": { + TypeSpec: pschema.TypeSpec{Type: "string"}, + Description: "An optional prefix.", + }, + "result": { + TypeSpec: pschema.TypeSpec{Type: "string"}, + Description: "A random string.", + }, + }, + Type: "object", + }, + InputProperties: map[string]pschema.PropertySpec{ + "length": { + TypeSpec: pschema.TypeSpec{Type: "integer"}, + Description: "The length of the random string (not including the prefix, if any).", + }, + "prefix": { + TypeSpec: pschema.TypeSpec{Type: "string"}, + Description: "An optional prefix.", + }, + }, + } +} + +type randomProvider struct{} + +func (p *randomProvider) Check(ctx context.Context, req *rpc.CheckRequest) (*rpc.CheckResponse, error) { + return &rpc.CheckResponse{Inputs: req.News, Failures: nil}, nil +} + +func (p *randomProvider) Diff(ctx context.Context, req *rpc.DiffRequest) (*rpc.DiffResponse, error) { + olds, err := plugin.UnmarshalProperties(req.GetOlds(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}) + if err != nil { + return nil, err + } + + news, err := plugin.UnmarshalProperties(req.GetNews(), plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}) + if err != nil { + return nil, err + } + + d := olds.Diff(news) + var replaces []string + changes := rpc.DiffResponse_DIFF_NONE + if d.Changed("length") { + changes = rpc.DiffResponse_DIFF_SOME + replaces = append(replaces, "length") + } + + if d.Changed("prefix") { + changes = rpc.DiffResponse_DIFF_SOME + replaces = append(replaces, "prefix") + } + + return &rpc.DiffResponse{ + Changes: changes, + Replaces: replaces, + }, nil +} + +func (p *randomProvider) Create(ctx context.Context, req *rpc.CreateRequest) (*rpc.CreateResponse, error) { + inputs, err := plugin.UnmarshalProperties(req.GetProperties(), plugin.MarshalOptions{ + KeepUnknowns: true, + SkipNulls: true, + }) + if err != nil { + return nil, err + } + + if !inputs["length"].IsNumber() { + return nil, fmt.Errorf("expected input property 'length' of type 'number' but got '%s", inputs["length"].TypeString()) + } + + n := int(inputs["length"].NumberValue()) + + var prefix string + if p, has := inputs["prefix"]; has { + if !p.IsString() { + return nil, fmt.Errorf("expected input property 'prefix' of type 'string' but got '%s", p.TypeString()) + } + prefix = p.StringValue() + } + + // Actually "create" the random number + result, err := makeRandom(n) + if err != nil { + return nil, err + } + + outputs := resource.NewPropertyMapFromMap(map[string]interface{}{ + "length": n, + "result": prefix + result, + }) + if prefix != "" { + outputs["prefix"] = resource.NewStringProperty(prefix) + } + outputs["result"] = resource.MakeSecret(outputs["result"]) + + outputProperties, err := plugin.MarshalProperties( + outputs, + plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true}, + ) + if err != nil { + return nil, err + } + return &rpc.CreateResponse{ + Id: result, + Properties: outputProperties, + }, nil +} + +func (p *randomProvider) Read(ctx context.Context, req *rpc.ReadRequest) (*rpc.ReadResponse, error) { + // Just return back the input state. + return &rpc.ReadResponse{ + Id: req.Id, + Properties: req.Properties, + }, nil +} + +func (p *randomProvider) Update(ctx context.Context, req *rpc.UpdateRequest) (*rpc.UpdateResponse, error) { + // Our Random resource will never be updated - if there is a diff, it will be a replacement. + panic("Update not implemented") +} + +func (p *randomProvider) Delete(ctx context.Context, req *rpc.DeleteRequest) (*emptypb.Empty, error) { + // Note that for our Random resource, we don't have to do anything on Delete. + return &emptypb.Empty{}, nil +} + +func (p *randomProvider) Invoke(ctx context.Context, req *rpc.InvokeRequest) (*rpc.InvokeResponse, error) { + // The random provider doesn't support any invokes currently. + panic("Invoke not implemented") +} + +func (p *randomProvider) Call(ctx context.Context, req *rpc.CallRequest) (*rpc.CallResponse, error) { + // The random provider doesn't support any call currently. + panic("Call not implemented") +} + +func makeRandom(length int) (string, error) { + charset := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + result := make([]rune, length) + for i := range result { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + result[i] = charset[num.Int64()] + } + return string(result), nil +}