Support defining remote components in Go (#6403)

This commit is contained in:
Justin Van Patten 2021-04-16 11:49:21 -07:00 committed by GitHub
parent 507082c003
commit 780a0c8a3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 988 additions and 64 deletions

View file

@ -56,4 +56,7 @@
[#6771](https://github.com/pulumi/pulumi/pull/6771)
[#6781](https://github.com/pulumi/pulumi/pull/6781)
- [sdk/go] Support defining remote components in Go.
[#6403](https://github.com/pulumi/pulumi/pull/6403)
### Bug Fixes

View file

@ -54,6 +54,7 @@ test_fast:: build
test_build:: $(SUB_PROJECTS:%=%_install)
cd tests/integration/construct_component/testcomponent && yarn install && yarn link @pulumi/pulumi && yarn run tsc
cd tests/integration/construct_component/testcomponent-go && go build -o pulumi-resource-testcomponent
cd tests/integration/construct_component_slow/testcomponent && yarn install && yarn link @pulumi/pulumi && yarn run tsc
cd tests/integration/construct_component_plain/testcomponent && yarn install && yarn link @pulumi/pulumi && yarn run tsc

View file

@ -292,6 +292,7 @@
<Target Name="TestBuild">
<Exec Command="yarn run tsc" WorkingDirectory="$(TestsDirectory)\integration\construct_component\testcomponent" />
<Exec Command="go build -o pulumi-resource-testcomponent.exe" WorkingDirectory="$(TestsDirectory)\integration\construct_component\testcomponent-go" />
<Exec Command="yarn run tsc" WorkingDirectory="$(TestsDirectory)\integration\construct_component_slow\testcomponent" />
<Exec Command="yarn run tsc" WorkingDirectory="$(TestsDirectory)\integration\construct_component_plain\testcomponent" />

View file

@ -0,0 +1,162 @@
// 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.
package provider
import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
pbempty "github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type componentProvider struct {
host *HostClient
name string
version string
schema []byte
construct provider.ConstructFunc
}
// ComponentMain is an entrypoint for a resource provider plugin that implements `Construct` for component resources.
// Using it isn't required but can cut down significantly on the amount of boilerplate necessary to fire up a new
// resource provider for components.
func ComponentMain(name, version string, schema []byte, construct provider.ConstructFunc) error {
return Main(name, func(host *HostClient) (pulumirpc.ResourceProviderServer, error) {
return &componentProvider{
host: host,
name: name,
version: version,
schema: schema,
construct: construct,
}, nil
})
}
// GetPluginInfo returns generic information about this plugin, like its version.
func (p *componentProvider) GetPluginInfo(context.Context, *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
return &pulumirpc.PluginInfo{
Version: p.version,
}, nil
}
// GetSchema returns the JSON-encoded schema for this provider's package.
func (p *componentProvider) GetSchema(ctx context.Context,
req *pulumirpc.GetSchemaRequest) (*pulumirpc.GetSchemaResponse, error) {
if v := req.GetVersion(); v != 0 {
return nil, errors.Errorf("unsupported schema version %d", v)
}
schema := string(p.schema)
if schema == "" {
schema = "{}"
}
return &pulumirpc.GetSchemaResponse{Schema: schema}, nil
}
// Configure configures the resource provider with "globals" that control its behavior.
func (p *componentProvider) Configure(ctx context.Context,
req *pulumirpc.ConfigureRequest) (*pulumirpc.ConfigureResponse, error) {
return &pulumirpc.ConfigureResponse{
AcceptSecrets: true,
SupportsPreview: true,
AcceptResources: true,
}, nil
}
// Construct creates a new instance of the provided component resource and returns its state.
func (p *componentProvider) Construct(ctx context.Context,
req *pulumirpc.ConstructRequest) (*pulumirpc.ConstructResponse, error) {
return provider.Construct(ctx, req, p.host.conn, p.construct)
}
// CheckConfig validates the configuration for this provider.
func (p *componentProvider) CheckConfig(ctx context.Context,
req *pulumirpc.CheckRequest) (*pulumirpc.CheckResponse, error) {
return nil, status.Error(codes.Unimplemented, "CheckConfig is not yet implemented")
}
// DiffConfig diffs the configuration for this provider.
func (p *componentProvider) DiffConfig(ctx context.Context,
req *pulumirpc.DiffRequest) (*pulumirpc.DiffResponse, error) {
return nil, status.Error(codes.Unimplemented, "DiffConfig is not yet implemented")
}
// StreamInvoke dynamically executes a built-in function in the provider. The result is streamed
// back as a series of messages.
func (p *componentProvider) StreamInvoke(req *pulumirpc.InvokeRequest,
server pulumirpc.ResourceProvider_StreamInvokeServer) error {
return status.Error(codes.Unimplemented, "StreamInvoke is not yet implemented")
}
// Check validates that the given property bag is valid for a resource of the given type and returns
// the inputs that should be passed to successive calls to Diff, Create, or Update for this
// resource. As a rule, the provider inputs returned by a call to Check should preserve the original
// representation of the properties as present in the program inputs. Though this rule is not
// required for correctness, violations thereof can negatively impact the end-user experience, as
// the provider inputs are using for detecting and rendering diffs.
func (p *componentProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (*pulumirpc.CheckResponse, error) {
return nil, status.Error(codes.Unimplemented, "Check is not yet implemented")
}
// Diff checks what impacts a hypothetical update will have on the resource's properties.
func (p *componentProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulumirpc.DiffResponse, error) {
return nil, status.Error(codes.Unimplemented, "Diff is not yet implemented")
}
// Create allocates a new instance of the provided resource and returns its unique ID afterwards.
// (The input ID must be blank.) If this call fails, the resource must not have been created (i.e.,
// it is "transactional").
func (p *componentProvider) Create(ctx context.Context,
req *pulumirpc.CreateRequest) (*pulumirpc.CreateResponse, error) {
return nil, status.Error(codes.Unimplemented, "Create is not yet implemented")
}
// Read the current live state associated with a resource. Enough state must be include in the
// inputs to uniquely identify the resource; this is typically just the resource ID, but may also
// include some properties.
func (p *componentProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pulumirpc.ReadResponse, error) {
return nil, status.Error(codes.Unimplemented, "Read is not yet implemented")
}
// Update updates an existing resource with new values.
func (p *componentProvider) Update(ctx context.Context,
req *pulumirpc.UpdateRequest) (*pulumirpc.UpdateResponse, error) {
return nil, status.Error(codes.Unimplemented, "Update is not yet implemented")
}
// Delete tears down an existing resource with the given ID. If it fails, the resource is assumed
// to still exist.
func (p *componentProvider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) (*pbempty.Empty, error) {
return nil, status.Error(codes.Unimplemented, "Delete is not yet implemented")
}
// Invoke dynamically executes a built-in function in the provider.
func (p *componentProvider) Invoke(ctx context.Context,
req *pulumirpc.InvokeRequest) (*pulumirpc.InvokeResponse, error) {
return nil, status.Error(codes.Unimplemented, "Invoke is not yet implemented")
}
// 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 *componentProvider) Cancel(context.Context, *pbempty.Empty) (*pbempty.Empty, error) {
return &pbempty.Empty{}, nil
}

View file

@ -54,6 +54,11 @@ func (host *HostClient) Close() error {
return host.conn.Close()
}
// EngineConn provides the engine gRPC client connection.
func (host *HostClient) EngineConn() *grpc.ClientConn {
return host.conn
}
func (host *HostClient) log(
context context.Context, sev diag.Severity, urn resource.URN, msg string, ephemeral bool,
) error {

View file

@ -79,7 +79,10 @@ func NewContext(ctx context.Context, info RunInfo) (*Context, error) {
var engineConn *grpc.ClientConn
var engine pulumirpc.EngineClient
if addr := info.EngineAddr; addr != "" {
if info.engineConn != nil {
engineConn = info.engineConn
engine = pulumirpc.NewEngineClient(engineConn)
} else if addr := info.EngineAddr; addr != "" {
conn, err := grpc.Dial(
info.EngineAddr,
grpc.WithInsecure(),

271
sdk/go/pulumi/provider.go Normal file
View file

@ -0,0 +1,271 @@
// 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.
package pulumi
import (
"context"
"reflect"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
)
type constructFunc func(ctx *Context, typ, name string, inputs map[string]interface{},
options ResourceOption) (URNInput, Input, error)
// construct adapts the gRPC ConstructRequest/ConstructResponse to/from the Pulumi Go SDK programming model.
func construct(ctx context.Context, req *pulumirpc.ConstructRequest, engineConn *grpc.ClientConn,
constructF constructFunc) (*pulumirpc.ConstructResponse, error) {
// Configure the RunInfo.
runInfo := RunInfo{
Project: req.GetProject(),
Stack: req.GetStack(),
Config: req.GetConfig(),
Parallel: int(req.GetParallel()),
DryRun: req.GetDryRun(),
MonitorAddr: req.GetMonitorEndpoint(),
engineConn: engineConn,
}
pulumiCtx, err := NewContext(ctx, runInfo)
if err != nil {
return nil, errors.Wrap(err, "constructing run context")
}
// Deserialize the inputs and apply appropriate dependencies.
inputDependencies := req.GetInputDependencies()
deserializedInputs, err := plugin.UnmarshalProperties(
req.GetInputs(),
plugin.MarshalOptions{KeepSecrets: true, KeepResources: true, KeepUnknowns: req.GetDryRun()},
)
if err != nil {
return nil, errors.Wrap(err, "unmarshaling inputs")
}
inputs := make(map[string]interface{}, len(deserializedInputs))
for key, input := range deserializedInputs {
k := string(key)
var deps []Resource
if inputDeps, ok := inputDependencies[k]; ok {
deps = make([]Resource, len(inputDeps.GetUrns()))
for i, depURN := range inputDeps.GetUrns() {
deps[i] = newDependencyResource(URN(depURN))
}
}
val, secret, err := unmarshalPropertyValue(pulumiCtx, input)
if err != nil {
return nil, errors.Wrapf(err, "unmarshaling input %s", k)
}
inputs[k] = &constructInput{
value: val,
secret: secret,
deps: deps,
}
}
// Rebuild the resource options.
aliases := make([]Alias, len(req.GetAliases()))
for i, urn := range req.GetAliases() {
aliases[i] = Alias{URN: URN(urn)}
}
dependencies := make([]Resource, len(req.GetDependencies()))
for i, urn := range req.GetDependencies() {
dependencies[i] = newDependencyResource(URN(urn))
}
providers := make(map[string]ProviderResource, len(req.GetProviders()))
for pkg, ref := range req.GetProviders() {
// Parse the URN and ID out of the provider reference.
lastSep := strings.LastIndex(ref, "::")
if lastSep == -1 {
return nil, errors.Errorf("expected '::' in provider reference %s", ref)
}
urn := ref[0:lastSep]
id := ref[lastSep+2:]
providers[pkg] = newDependencyProviderResource(URN(urn), ID(id))
}
var parent Resource
if req.GetParent() != "" {
parent = newDependencyResource(URN(req.GetParent()))
}
opts := resourceOption(func(ro *resourceOptions) {
ro.Aliases = aliases
ro.DependsOn = dependencies
ro.Protect = req.GetProtect()
ro.Providers = providers
ro.Parent = parent
})
urn, state, err := constructF(pulumiCtx, req.GetType(), req.GetName(), inputs, opts)
if err != nil {
return nil, err
}
// Ensure all outstanding RPCs have completed before proceeding. Also, prevent any new RPCs from happening.
pulumiCtx.waitForRPCs()
if pulumiCtx.rpcError != nil {
return nil, errors.Wrap(pulumiCtx.rpcError, "waiting for RPCs")
}
rpcURN, _, _, err := urn.ToURNOutput().awaitURN(ctx)
if err != nil {
return nil, err
}
// Serialize all state properties, first by awaiting them, and then marshaling them to the requisite gRPC values.
resolvedProps, propertyDeps, _, err := marshalInputs(state)
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
// Marshal all properties for the RPC call.
keepUnknowns := req.GetDryRun()
rpcProps, err := plugin.MarshalProperties(
resolvedProps,
plugin.MarshalOptions{KeepSecrets: true, KeepUnknowns: keepUnknowns, KeepResources: pulumiCtx.keepResources})
if err != nil {
return nil, errors.Wrap(err, "marshaling properties")
}
// Convert the property dependencies map for RPC and remove duplicates.
rpcPropertyDeps := make(map[string]*pulumirpc.ConstructResponse_PropertyDependencies)
for k, deps := range propertyDeps {
sort.Slice(deps, func(i, j int) bool { return deps[i] < deps[j] })
urns := make([]string, 0, len(deps))
for i, d := range deps {
if i > 0 && urns[i-1] == string(d) {
continue
}
urns = append(urns, string(d))
}
rpcPropertyDeps[k] = &pulumirpc.ConstructResponse_PropertyDependencies{
Urns: urns,
}
}
return &pulumirpc.ConstructResponse{
Urn: string(rpcURN),
State: rpcProps,
StateDependencies: rpcPropertyDeps,
}, nil
}
type constructInput struct {
value interface{}
secret bool
deps []Resource
}
// constructInputsMap returns the inputs as a Map.
func constructInputsMap(inputs map[string]interface{}) Map {
result := make(Map, len(inputs))
for k, v := range inputs {
val := v.(*constructInput)
output := newOutput(anyOutputType, val.deps...)
output.getState().resolve(val.value, true /*known*/, val.secret, nil)
result[k] = output
}
return result
}
// constructInputsSetArgs sets the inputs on the given args struct.
func constructInputsSetArgs(inputs map[string]interface{}, args interface{}) error {
if args == nil {
return errors.New("args must not be nil")
}
argsV := reflect.ValueOf(args)
typ := argsV.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return errors.New("args must be a pointer to a struct")
}
argsV, typ = argsV.Elem(), typ.Elem()
for k, v := range inputs {
val := v.(*constructInput)
for i := 0; i < typ.NumField(); i++ {
fieldV := argsV.Field(i)
if !fieldV.CanSet() {
continue
}
field := typ.Field(i)
tag, has := field.Tag.Lookup("pulumi")
if !has || tag != k {
continue
}
if !field.Type.Implements(reflect.TypeOf((*Input)(nil)).Elem()) {
continue
}
outputType := anyOutputType
toOutputMethodName := "To" + strings.TrimSuffix(field.Type.Name(), "Input") + "Output"
toOutputMethod, found := field.Type.MethodByName(toOutputMethodName)
if found {
mt := toOutputMethod.Type
if mt.NumIn() != 0 || mt.NumOut() != 1 {
continue
}
outputType = mt.Out(0)
if !outputType.Implements(reflect.TypeOf((*Output)(nil)).Elem()) {
continue
}
}
output := newOutput(outputType, val.deps...)
output.getState().resolve(val.value, true /*known*/, val.secret, nil)
fieldV.Set(reflect.ValueOf(output))
}
}
return nil
}
// newConstructResult converts a resource into its associated URN and state.
func newConstructResult(resource ComponentResource) (URNInput, Input, error) {
if resource == nil {
return nil, nil, errors.New("resource must not be nil")
}
resourceV := reflect.ValueOf(resource)
typ := resourceV.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return nil, nil, errors.New("resource must be a pointer to a struct")
}
resourceV, typ = resourceV.Elem(), typ.Elem()
state := make(Map)
for i := 0; i < typ.NumField(); i++ {
fieldV := resourceV.Field(i)
field := typ.Field(i)
tag, has := field.Tag.Lookup("pulumi")
if !has {
continue
}
val := fieldV.Interface()
if v, ok := val.(Input); ok {
state[tag] = v
}
}
return resource.URN(), state, nil
}

View file

@ -0,0 +1,17 @@
// 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.
// This empty .s file enables the use of go:linkname to make certain unexported functions from
// the pulumi package available in the provider package so the public API can be exposed from the
// provider package.

View file

@ -0,0 +1,92 @@
// 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.
package provider
import (
"context"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
)
// This file relies on implementations in ../provider_linked.go that are made available in this package via
// go:linkname.
type ConstructFunc func(ctx *pulumi.Context, typ, name string, inputs ConstructInputs,
options pulumi.ResourceOption) (*ConstructResult, error)
// Construct adapts the gRPC ConstructRequest/ConstructResponse to/from the Pulumi Go SDK programming model.
func Construct(ctx context.Context, req *pulumirpc.ConstructRequest, engineConn *grpc.ClientConn,
construct ConstructFunc) (*pulumirpc.ConstructResponse, error) {
return linkedConstruct(ctx, req, engineConn, func(pulumiCtx *pulumi.Context, typ, name string,
inputs map[string]interface{}, options pulumi.ResourceOption) (pulumi.URNInput, pulumi.Input, error) {
result, err := construct(pulumiCtx, typ, name, ConstructInputs{inputs: inputs}, options)
if err != nil {
return nil, nil, err
}
return result.URN, result.State, nil
})
}
// ConstructInputs represents the inputs associated with a call to Construct.
type ConstructInputs struct {
inputs map[string]interface{}
}
// Map returns the inputs as a Map.
func (inputs ConstructInputs) Map() pulumi.Map {
return linkedConstructInputsMap(inputs.inputs)
}
// SetArgs sets the inputs on the given args struct.
func (inputs ConstructInputs) SetArgs(args interface{}) error {
return linkedConstructInputsSetArgs(inputs.inputs, args)
}
// ConstructResult is the result of a call to Construct.
type ConstructResult struct {
URN pulumi.URNInput
State pulumi.Input
}
// NewConstructResult creates a ConstructResult from the resource.
func NewConstructResult(resource pulumi.ComponentResource) (*ConstructResult, error) {
urn, state, err := linkedNewConstructResult(resource)
if err != nil {
return nil, err
}
return &ConstructResult{
URN: urn,
State: state,
}, nil
}
type constructFunc func(ctx *pulumi.Context, typ, name string, inputs map[string]interface{},
options pulumi.ResourceOption) (pulumi.URNInput, pulumi.Input, error)
// linkedConstruct is made available here from ../provider_linked.go via go:linkname.
func linkedConstruct(ctx context.Context, req *pulumirpc.ConstructRequest, engineConn *grpc.ClientConn,
constructF constructFunc) (*pulumirpc.ConstructResponse, error)
// linkedConstructInputsMap is made available here from ../provider_linked.go via go:linkname.
func linkedConstructInputsMap(inputs map[string]interface{}) pulumi.Map
// linkedConstructInputsSetArgs is made available here from ../provider_linked.go via go:linkname.
func linkedConstructInputsSetArgs(inputs map[string]interface{}, args interface{}) error
// linkedNewConstructResult is made available here from ../provider_linked.go via go:linkname.
func linkedNewConstructResult(resource pulumi.ComponentResource) (pulumi.URNInput, pulumi.Input, error)

View file

@ -0,0 +1,50 @@
// 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.
//nolint:deadcode,lll
package pulumi
import (
"context"
_ "unsafe" // unsafe is needed to use go:linkname
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"google.golang.org/grpc"
)
// We want the public provider-related APIs to be exported from the provider package, but need to make use of unexported
// functionality in this package for their implementations. To achieve this, go:linkname is used to make the following
// functions available in the provider package.
//go:linkname linkedConstruct github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider.linkedConstruct
func linkedConstruct(ctx context.Context, req *pulumirpc.ConstructRequest, engineConn *grpc.ClientConn,
constructF constructFunc) (*pulumirpc.ConstructResponse, error) {
return construct(ctx, req, engineConn, constructF)
}
//go:linkname linkedConstructInputsMap github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider.linkedConstructInputsMap
func linkedConstructInputsMap(inputs map[string]interface{}) Map {
return constructInputsMap(inputs)
}
//go:linkname linkedConstructInputsSetArgs github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider.linkedConstructInputsSetArgs
func linkedConstructInputsSetArgs(inputs map[string]interface{}, args interface{}) error {
return constructInputsSetArgs(inputs, args)
}
//go:linkname linkedNewConstructResult github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider.linkedNewConstructResult
func linkedNewConstructResult(resource ComponentResource) (URNInput, Input, error) {
return newConstructResult(resource)
}

View file

@ -25,6 +25,8 @@ import (
multierror "github.com/hashicorp/go-multierror"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"google.golang.org/grpc"
)
var ErrPlugins = errors.New("pulumi: plugins requested")
@ -132,6 +134,7 @@ type RunInfo struct {
EngineAddr string
Mocks MockResourceMonitor
getPlugins bool
engineConn *grpc.ClientConn // Pre-existing engine connection. If set this is used over EngineAddr.
}
// getEnvInfo reads various program information from the process environment.

View file

@ -10,6 +10,7 @@ replace (
require (
github.com/blang/semver v3.5.1+incompatible
github.com/golang/protobuf v1.4.3 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/pkg/errors v0.9.1
github.com/pulumi/pulumi-random/sdk/v3 v3.1.2-0.20210323111705-623d65f88c52

View file

@ -0,0 +1,2 @@
pulumi-resource-testcomponent
pulumi-resource-testcomponent.exe

View file

@ -0,0 +1,230 @@
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
package main
import (
"context"
"fmt"
"reflect"
"github.com/pkg/errors"
"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"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
pbempty "github.com/golang/protobuf/ptypes/empty"
)
type Resource struct {
pulumi.CustomResourceState
}
type resourceArgs struct {
Echo interface{} `pulumi:"echo"`
}
type ResourceArgs struct {
Echo pulumi.Input
}
func (ResourceArgs) ElementType() reflect.Type {
return reflect.TypeOf((*resourceArgs)(nil)).Elem()
}
func NewResource(ctx *pulumi.Context, name string, echo pulumi.Input,
opts ...pulumi.ResourceOption) (*Resource, error) {
args := &ResourceArgs{Echo: echo}
var resource Resource
err := ctx.RegisterResource("testcomponent:index:Resource", name, args, &resource, opts...)
if err != nil {
return nil, err
}
return &resource, nil
}
type Component struct {
pulumi.ResourceState
Echo pulumi.Input `pulumi:"echo"`
ChildID pulumi.IDOutput `pulumi:"childId"`
}
type ComponentArgs struct {
Echo pulumi.Input `pulumi:"echo"`
}
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("testcomponent:index:Component", name, component, opts...)
if err != nil {
return nil, err
}
res, err := NewResource(ctx, fmt.Sprintf("child-%s", name), args.Echo, pulumi.Parent(component))
if err != nil {
return nil, err
}
component.Echo = args.Echo //pulumi.ToOutput(args.Echo)
component.ChildID = res.ID()
if err := ctx.RegisterResourceOutputs(component, pulumi.Map{}); err != nil {
return nil, err
}
return component, nil
}
const providerName = "testcomponent"
const version = "0.0.1"
var currentID int
func main() {
err := provider.Main(providerName, func(host *provider.HostClient) (pulumirpc.ResourceProviderServer, error) {
return makeProvider(host, providerName, version)
})
if err != nil {
cmdutil.ExitError(err.Error())
}
}
type testcomponentProvider struct {
host *provider.HostClient
name string
version string
}
func makeProvider(host *provider.HostClient, name, version string) (pulumirpc.ResourceProviderServer, error) {
return &testcomponentProvider{
host: host,
name: name,
version: version,
}, nil
}
func (p *testcomponentProvider) Create(ctx context.Context,
req *pulumirpc.CreateRequest) (*pulumirpc.CreateResponse, error) {
urn := resource.URN(req.GetUrn())
typ := urn.Type()
if typ != "testcomponent:index:Resource" {
return nil, errors.Errorf("Unknown resource type '%s'", typ)
}
id := currentID
currentID++
return &pulumirpc.CreateResponse{
Id: fmt.Sprintf("%v", id),
}, nil
}
func (p *testcomponentProvider) Construct(ctx context.Context,
req *pulumirpc.ConstructRequest) (*pulumirpc.ConstructResponse, error) {
return pulumiprovider.Construct(ctx, req, p.host.EngineConn(), func(ctx *pulumi.Context, typ, name string,
inputs pulumiprovider.ConstructInputs, options pulumi.ResourceOption) (*pulumiprovider.ConstructResult, error) {
if typ != "testcomponent:index:Component" {
return nil, errors.Errorf("unknown resource type %s", typ)
}
args := &ComponentArgs{}
if err := inputs.SetArgs(args); err != nil {
return nil, errors.Wrap(err, "setting args")
}
component, err := NewComponent(ctx, name, args, options)
if err != nil {
return nil, errors.Wrap(err, "creating component")
}
//return pulumiprovider.NewConstructResult(component)
return &pulumiprovider.ConstructResult{
URN: component.URN(),
State: pulumi.Map{
"echo": component.Echo,
"childId": component.ChildID,
},
}, nil
})
}
func (p *testcomponentProvider) CheckConfig(ctx context.Context,
req *pulumirpc.CheckRequest) (*pulumirpc.CheckResponse, error) {
return &pulumirpc.CheckResponse{Inputs: req.GetNews()}, nil
}
func (p *testcomponentProvider) DiffConfig(ctx context.Context,
req *pulumirpc.DiffRequest) (*pulumirpc.DiffResponse, error) {
return &pulumirpc.DiffResponse{}, nil
}
func (p *testcomponentProvider) Configure(ctx context.Context,
req *pulumirpc.ConfigureRequest) (*pulumirpc.ConfigureResponse, error) {
return &pulumirpc.ConfigureResponse{
AcceptSecrets: true,
SupportsPreview: true,
AcceptResources: true,
}, nil
}
func (p *testcomponentProvider) Invoke(ctx context.Context,
req *pulumirpc.InvokeRequest) (*pulumirpc.InvokeResponse, error) {
return nil, errors.Errorf("Unknown Invoke token '%s'", req.GetTok())
}
func (p *testcomponentProvider) StreamInvoke(req *pulumirpc.InvokeRequest,
server pulumirpc.ResourceProvider_StreamInvokeServer) error {
return errors.Errorf("Unknown StreamInvoke token '%s'", req.GetTok())
}
func (p *testcomponentProvider) Check(ctx context.Context,
req *pulumirpc.CheckRequest) (*pulumirpc.CheckResponse, error) {
return &pulumirpc.CheckResponse{Inputs: req.News, Failures: nil}, nil
}
func (p *testcomponentProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulumirpc.DiffResponse, error) {
return &pulumirpc.DiffResponse{}, nil
}
func (p *testcomponentProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pulumirpc.ReadResponse, error) {
return &pulumirpc.ReadResponse{
Id: req.GetId(),
Properties: req.GetProperties(),
}, nil
}
func (p *testcomponentProvider) Update(ctx context.Context,
req *pulumirpc.UpdateRequest) (*pulumirpc.UpdateResponse, error) {
return &pulumirpc.UpdateResponse{
Properties: req.GetNews(),
}, nil
}
func (p *testcomponentProvider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) (*pbempty.Empty, error) {
return &pbempty.Empty{}, nil
}
func (p *testcomponentProvider) GetPluginInfo(context.Context, *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
return &pulumirpc.PluginInfo{
Version: p.version,
}, nil
}
func (p *testcomponentProvider) GetSchema(ctx context.Context,
req *pulumirpc.GetSchemaRequest) (*pulumirpc.GetSchemaResponse, error) {
return &pulumirpc.GetSchemaResponse{}, nil
}
func (p *testcomponentProvider) Cancel(context.Context, *pbempty.Empty) (*pbempty.Empty, error) {
return &pbempty.Empty{}, nil
}

View file

@ -196,25 +196,52 @@ func TestLargeResourceDotNet(t *testing.T) {
// Test remote component construction in .NET.
func TestConstructDotnet(t *testing.T) {
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the .NET program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
const testYarnLinkPulumiEnv = "PULUMI_TEST_YARN_LINK_PULUMI=true"
tests := []struct {
componentDir string
expectedResourceCount int
env []string
}{
{
componentDir: "testcomponent",
expectedResourceCount: 9,
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
env: []string{"PULUMI_TEST_YARN_LINK_PULUMI=true"},
},
{
componentDir: "testcomponent-python",
expectedResourceCount: 9,
env: []string{pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))},
},
{
componentDir: "testcomponent-go",
expectedResourceCount: 8, // One less because no dynamic provider.
},
}
runtimeVenv := pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))
for _, test := range tests {
t.Run(test.componentDir, func(t *testing.T) {
pathEnv := componentPathEnv(t, "construct_component", test.componentDir)
integration.ProgramTest(t,
optsForConstructDotnet(t, test.expectedResourceCount, append(test.env, pathEnv)...))
})
}
}
opts := &integration.ProgramTestOptions{
Env: []string{testYarnLinkPulumiEnv, runtimeVenv},
func optsForConstructDotnet(t *testing.T, expectedResourceCount int, env ...string) *integration.ProgramTestOptions {
return &integration.ProgramTestOptions{
Env: env,
Dir: filepath.Join("construct_component", "dotnet"),
Dependencies: []string{"Pulumi"},
Quick: true,
NoParallel: true, // avoid contention for Dir
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) {
if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.Type)
@ -239,11 +266,6 @@ func TestConstructDotnet(t *testing.T) {
}
},
}
runProgramSubTests(t, opts, map[string]string{
"WithNodeProvider": componentPathEnv(t, "construct_component", "testcomponent"),
"WithPythonProvider": componentPathEnv(t, "construct_component", "testcomponent-python"),
})
}
// Test remote component construction with a child resource that takes a long time to be created, ensuring it's created.

View file

@ -121,19 +121,44 @@ func TestLargeResourceGo(t *testing.T) {
// Test remote component construction in Go.
func TestConstructGo(t *testing.T) {
tests := []struct {
componentDir string
expectedResourceCount int
env []string
}{
{
componentDir: "testcomponent",
expectedResourceCount: 9,
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
env: []string{"PULUMI_TEST_YARN_LINK_PULUMI=true"},
},
{
componentDir: "testcomponent-python",
expectedResourceCount: 9,
env: []string{pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))},
},
{
componentDir: "testcomponent-go",
expectedResourceCount: 8, // One less because no dynamic provider.
},
}
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
const testYarnLinkPulumiEnv = "PULUMI_TEST_YARN_LINK_PULUMI=true"
for _, test := range tests {
t.Run(test.componentDir, func(t *testing.T) {
pathEnv := componentPathEnv(t, "construct_component", test.componentDir)
integration.ProgramTest(t, optsForConstructGo(t, test.expectedResourceCount, append(test.env, pathEnv)...))
})
}
}
runtimeVenv := pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))
opts := &integration.ProgramTestOptions{
Env: []string{testYarnLinkPulumiEnv, runtimeVenv},
func optsForConstructGo(t *testing.T, expectedResourceCount int, env ...string) *integration.ProgramTestOptions {
return &integration.ProgramTestOptions{
Env: env,
Dir: filepath.Join("construct_component", "go"),
Dependencies: []string{
"github.com/pulumi/pulumi/sdk/v3",
@ -142,7 +167,7 @@ func TestConstructGo(t *testing.T) {
NoParallel: true, // avoid contention for Dir
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) {
if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.Type)
@ -170,11 +195,6 @@ func TestConstructGo(t *testing.T) {
}
},
}
runProgramSubTests(t, opts, map[string]string{
"WithNodeProvider": componentPathEnv(t, "construct_component", "testcomponent"),
"WithPythonProvider": componentPathEnv(t, "construct_component", "testcomponent-python"),
})
}
// Test remote component construction with a child resource that takes a long time to be created, ensuring it's created.

View file

@ -677,15 +677,45 @@ func TestConstructNode(t *testing.T) {
t.Skip("Temporarily skipping test on Windows")
}
opts := &integration.ProgramTestOptions{
Env: []string{pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))},
tests := []struct {
componentDir string
expectedResourceCount int
env []string
}{
{
componentDir: "testcomponent",
expectedResourceCount: 9,
},
{
componentDir: "testcomponent-python",
expectedResourceCount: 9,
env: []string{pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))},
},
{
componentDir: "testcomponent-go",
expectedResourceCount: 8, // One less because no dynamic provider.
},
}
for _, test := range tests {
t.Run(test.componentDir, func(t *testing.T) {
pathEnv := componentPathEnv(t, "construct_component", test.componentDir)
integration.ProgramTest(t,
optsForConstructNode(t, test.expectedResourceCount, append(test.env, pathEnv)...))
})
}
}
func optsForConstructNode(t *testing.T, expectedResourceCount int, env ...string) *integration.ProgramTestOptions {
return &integration.ProgramTestOptions{
Env: env,
Dir: filepath.Join("construct_component", "nodejs"),
Dependencies: []string{"@pulumi/pulumi"},
Quick: true,
NoParallel: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) {
if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.Type)
@ -710,11 +740,6 @@ func TestConstructNode(t *testing.T) {
}
},
}
runProgramSubTests(t, opts, map[string]string{
"WithNodeProvider": componentPathEnv(t, "construct_component", "testcomponent"),
"WithPythonProvider": componentPathEnv(t, "construct_component", "testcomponent-python"),
})
}
// Test remote component construction with a child resource that takes a long time to be created, ensuring it's created.

View file

@ -378,19 +378,45 @@ func TestPythonResourceArgs(t *testing.T) {
// Test remote component construction in Python.
func TestConstructPython(t *testing.T) {
tests := []struct {
componentDir string
expectedResourceCount int
env []string
}{
{
componentDir: "testcomponent",
expectedResourceCount: 9,
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
env: []string{"PULUMI_TEST_YARN_LINK_PULUMI=true"},
},
{
componentDir: "testcomponent-python",
expectedResourceCount: 9,
env: []string{pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))},
},
{
componentDir: "testcomponent-go",
expectedResourceCount: 8, // One less because no dynamic provider.
},
}
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
// module to run `yarn install && yarn link @pulumi/pulumi` in the Python program's directory, allowing
// the Node.js dynamic provider plugin to load.
// When the underlying issue has been fixed, the use of this environment variable inside the integration
// test module should be removed.
const testYarnLinkPulumiEnv = "PULUMI_TEST_YARN_LINK_PULUMI=true"
for _, test := range tests {
t.Run(test.componentDir, func(t *testing.T) {
pathEnv := componentPathEnv(t, "construct_component", test.componentDir)
integration.ProgramTest(t,
optsForConstructPython(t, test.expectedResourceCount, append(test.env, pathEnv)...))
})
}
}
runtimeVenv := pulumiRuntimeVirtualEnv(t, filepath.Join("..", ".."))
opts := &integration.ProgramTestOptions{
Env: []string{testYarnLinkPulumiEnv, runtimeVenv},
func optsForConstructPython(t *testing.T, expectedResourceCount int, env ...string) *integration.ProgramTestOptions {
return &integration.ProgramTestOptions{
Env: env,
Dir: filepath.Join("construct_component", "python"),
Dependencies: []string{
filepath.Join("..", "..", "sdk", "python", "env", "src"),
@ -399,7 +425,7 @@ func TestConstructPython(t *testing.T) {
NoParallel: true, // avoid contention for Dir
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 9, len(stackInfo.Deployment.Resources)) {
if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.Type)
@ -427,11 +453,6 @@ func TestConstructPython(t *testing.T) {
}
},
}
runProgramSubTests(t, opts, map[string]string{
"WithNodeProvider": componentPathEnv(t, "construct_component", "testcomponent"),
"WithPythonProvider": componentPathEnv(t, "construct_component", "testcomponent-python"),
})
}
// Test remote component construction with a child resource that takes a long time to be created, ensuring it's created.

View file

@ -566,11 +566,6 @@ func TestConfigPaths(t *testing.T) {
e.RunCommand("pulumi", "stack", "rm", "--yes")
}
//nolint:golint,deadcode
func testComponentPathEnv(t *testing.T) string {
return componentPathEnv(t, "construct_component", "testcomponent")
}
//nolint:golint,deadcode
func testComponentSlowPathEnv(t *testing.T) string {
return componentPathEnv(t, "construct_component_slow", "testcomponent")