Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support defining remote components in Go #6403

Merged
merged 1 commit into from
Apr 16, 2021
Merged

Conversation

justinvp
Copy link
Member

@justinvp justinvp commented Feb 22, 2021

This makes it possible to create a resource provider with a Construct implementation in Go, similar to what we have for Node.

Fixes #5489

sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
@justinvp justinvp marked this pull request as ready for review February 22, 2021 22:33
@justinvp
Copy link
Member Author

justinvp commented Apr 15, 2021

I've force pushed a new commit that addresses the feedback and adds integration tests. I am finishing support for prompt values for a separate PR, rather than including it here.

Here's a summary of what's changed:

  • The public API is now exported from the sdk/v3/go/pulumi/provider package rather than the top-level sdk/v3/go/pulumi package. I decided to use go:linkname to make a small number of unexported functions implemented inside pulumi available inside the pulumi/provider package. This way, we can access the unexported functionality in pulumi but expose the public API from pulumi/provider. This is consistent with what the Go runtime package does to expose some functions implemented inside the runtime package to other packages.

    • As an alternative, I'd considered adding an pulumi/internal package and moving a bunch of functionality from pulumi into it and exporting it, so that both pulumi and pulumi/provider could access it, but this would have been a more significant refactoring.

    • If the use of go:linkname is undesirable (I'm having a hard time coming up with a major downside, other than it being a little unusual and having to maintain the package version in the //go:linkname comments), we could consider moving Construct into the top-level pulumi package.

  • The pulumi/provider package only expose a relatively small surface area, basically a Construct adapter function and that converts the gRPC request/response to/from the Pulumi Go programming model. I've removed the provider implementation that was included. Instead, the Construct adapter is intended to be used in conjunction with the existing helper APIs we have in https://github.com/pulumi/pulumi/tree/master/pkg/resource/provider. And to make it even easier for components, I've added a new ComponentMain function there, which implements a resource provider with the minimal amount of support for components, so you don't have to write a boilerplate provider implementation yourself if you only care about exposing components.

  • I've added a func NewConstructResult(resource pulumi.ComponentResource) (*ConstructResult, error) helper to avoid the boilerplate around return the component's URN and state.

  • I've opened a draft PR of the go boilerplate repo that updates it to use the changes from this PR: Update to the latest provider API pulumi-component-provider-go-boilerplate#3. Here's a quick summary of usage:

import (
	"github.com/pulumi/pulumi/pkg/v3/resource/provider"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
)

// Serve launches the gRPC server for the resource provider.
func Serve(providerName, version string, schema []byte) {
	// Start gRPC service.
	if err := provider.ComponentMain(providerName, version, schema, construct); err != nil {
		cmdutil.ExitError(err.Error())
	}
}
import (
	"github.com/pkg/errors"

	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/provider"
)

func construct(ctx *pulumi.Context, typ, name string, inputs provider.ConstructInputs,
	options pulumi.ResourceOption) (*provider.ConstructResult, error) {
	// TODO: Add support for additional component resources here.
	switch typ {
	case "xyz:index:StaticPage":
		return constructStaticPage(ctx, name, inputs, options)
	default:
		return nil, errors.Errorf("unknown resource type %s", typ)
	}
}

// constructStaticPage is an implementation of Construct for the example StaticPage component.
// It demonstrates converting the raw ConstructInputs to the component's args struct, creating
// the component, and returning its URN and state (outputs).
func constructStaticPage(ctx *pulumi.Context, name string, inputs provider.ConstructInputs,
	options pulumi.ResourceOption) (*provider.ConstructResult, error) {

	// Copy the raw inputs to StaticPageArgs. `inputs.SetArgs` uses the types and `pulumi:` tags
	// on the struct's fields to convert the raw values to the appropriate Input types.
	args := &StaticPageArgs{}
	if err := inputs.SetArgs(args); err != nil {
		return nil, errors.Wrap(err, "setting args")
	}

	// Create the component resource.
	staticPage, err := NewStaticPage(ctx, name, args, options)
	if err != nil {
		return nil, errors.Wrap(err, "creating component")
	}

	// Return the component resource's URN and state. `NewConstructResult` automatically sets the
	// ConstructResult's state based on resource struct fields tagged with `pulumi:` tags with a value
	// that is convertible to `pulumi.Input`.
	return provider.NewConstructResult(staticPage)
}

sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
sdk/go/pulumi/provider.go Outdated Show resolved Hide resolved
continue
}

if !field.Type.Implements(reflect.TypeOf((*Input)(nil)).Elem()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[I just found these two comments in drafts that I had intended to send a couple weeks ago! I believe they are still relevant to the design here though - so sharing now ahead of a more thorough review of the updated PR shortly]

I didn't want to force component authors to have to define it twice manually like this. But curious if you think we should force component authors to define these the same way as the Go codegen does.

Two things that I've gotten tripped up on myself working with this:

  1. How do I write my own nested type to use here? Like if I want a Routes: []Route, how do I write []Route so that it works? It appears I need to create a type RouteArrayInput, ensure that has a ToRouteArrayOutputWithContext, and then also a custom Output struct that wraps the raw Route struct? I haven't yet quite figured out what works fully here.
  2. If we had just alllowed using the same thing that codegen produces, then I could just use the types that the Go SDK codegen already generates for me - by having the provider actually depend on the SDK. That may be a terrible idea, but it is nice in that the implementation gets to work with the exact same structs/interfaces as consumers will. It also means I don't have to recreate all of this manually inside the implementation - since there is already a codegen process to create these for the SDK. However, the types implemented in the SDK don't seem to work correctly with the logic as implemented here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also a little strange that these accept Input types - given that at least currently they will always be Outputs, and even once prompt values are possible, the user will always want to be clear here in the types which ones are prompt and which are Output. It will never be desirable for the implementor to see Input types vs. Output types. The implementation will always have to immediately turn the inputs back into outputs to work with them.

If this args struct used Outputs, the implementation here would also be much simpler and less "weird" in that it wouldn't have to discover To<>OutputWithContext to figure out what the output type was to create.

Copy link
Member Author

@justinvp justinvp Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I write my own nested type to use here?

The idea was to follow our documented pattern for inputs:

// Input is the type of a generic input value for a Pulumi resource. This type is used in conjunction with Output
// to provide polymorphism over strongly-typed input values.
//
// The intended pattern for nested Pulumi value types is to define an input interface and a plain, input, and output
// variant of the value type that implement the input interface.
//
// For example, given a nested Pulumi value type with the following shape:
//
// type Nested struct {
// Foo int
// Bar string
// }
//
// We would define the following:
//
// var nestedType = reflect.TypeOf((*Nested)(nil)).Elem()
//
// type NestedInput interface {
// pulumi.Input
//
// ToNestedOutput() NestedOutput
// ToNestedOutputWithContext(context.Context) NestedOutput
// }
//
// type Nested struct {
// Foo int `pulumi:"foo"`
// Bar string `pulumi:"bar"`
// }
//
// type NestedInputValue struct {
// Foo pulumi.IntInput `pulumi:"foo"`
// Bar pulumi.StringInput `pulumi:"bar"`
// }
//
// func (NestedInputValue) ElementType() reflect.Type {
// return nestedType
// }
//
// func (v NestedInputValue) ToNestedOutput() NestedOutput {
// return pulumi.ToOutput(v).(NestedOutput)
// }
//
// func (v NestedInputValue) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
// return pulumi.ToOutputWithContext(ctx, v).(NestedOutput)
// }
//
// type NestedOutput struct { *pulumi.OutputState }
//
// func (NestedOutput) ElementType() reflect.Type {
// return nestedType
// }
//
// func (o NestedOutput) ToNestedOutput() NestedOutput {
// return o
// }
//
// func (o NestedOutput) ToNestedOutputWithContext(ctx context.Context) NestedOutput {
// return o
// }
//
type Input interface {
ElementType() reflect.Type
}

But it is an unfortunate amount of boilerplate to have to write manually (especially so if you'd need to also manually write associated boilerplate for array variants, map variants, etc.), so I fully empathize with the desire from (2) to use the types that the Go SDK codegen generates. However, ideally, we'd have a way to write a much simpler type in the implementation without all the extra ceremony/boilerplate, and drive the schema + SDK generation based on it, not the other way around.

However, the types implemented in the SDK don't seem to work correctly with the logic as implemented here.

Hmmm, do you happen to recall the specific issue? I believe there is a bug in the current implementation in this PR, where the unmarshalled value from the gRPC call isn't converted into the struct type -- so it'll remain a map. I am addressing this issue with the changes for prompt values.

It's also a little strange that these accept Input types - given that at least currently they will always be Outputs, and even once prompt values are possible, the user will always want to be clear here in the types which ones are prompt and which are Output. It will never be desirable for the implementor to see Input types vs. Output types. The implementation will always have to immediately turn the inputs back into outputs to work with them.

If this args struct used Outputs, the implementation here would also be much simpler and less "weird" in that it wouldn't have to discover To<>OutputWithContext to figure out what the output type was to create.

This is a good point. I was thinking you'd write the component implementation the same way you'd write it if you were releasing it as a regular Go library, where (ignoring prompt values for a moment) you would want to type inputs as the Input type to support either Input or Output values (and inside the implementation you'd immediately convert to Output to work with it).

But for the implementation of multi-lang components in Go, I could see how typing it as Output would simplify things here. We'd immediately know the Output type to use and it would save the user from having to convert the value to Output inside the component implementation.

I do still feel like we could support Input though... I could see supporting either Output or Input. If it's Output, great -- use it. If it's Input, one thing I could do is get its ElementType() and then see if the output type has been registered with pulumi.RegisterOutputType (using it if so) (Update: actually, I don't think this is possible), and finally falling back to looking for a To<>Output method.

discover To<>OutputWithContext

Note: after the recent discussion of possibly deprecating the <>WithContext methods, I've switched this to look for just To<>Output.

}

// constructInputsSetArgs sets the inputs on the given args struct.
func constructInputsSetArgs(inputs map[string]interface{}, args interface{}) error {
Copy link
Member Author

@justinvp justinvp Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the implementation of this function isn't quite right, so it's not super important to review the details of it for this PR. I've reimplemented it for prompt values (separate PR on the way), and as part of that, it's leveraging rpc.unmarshalOutput to unmarshal the value.

Edit: Prompt value PR: #6790

// See the License for the specific language governing permissions and
// limitations under the License.

package provider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use plugin.providerServer? Looks like their functionality is similar.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Construct adapter func in this PR adapts the raw gRPC interface to/from the Pulumi Go SDK model.

// Construct creates a new instance of the provided component resource and returns its state.
Construct(context.Context, *ConstructRequest) (*ConstructResponse, error)

So the provider in this file implements the raw gRPC interface.

To use plugin.providerServer, Construct would have to be an adapter of plugin.providerServer's interface:

// Construct creates a new component resource.
Construct(info ConstructInfo, typ tokens.Type, name tokens.QName, parent resource.URN, inputs resource.PropertyMap,
options ConstructOptions) (ConstructResult, error)

Should Construct be an adapter for plugin.providerServer instead? Or should we have adapters for both the raw gRPC interface and plugin.providerServer?

The reason I made Construct an adapter for the raw gRPC interface is because 1) our existing provider boilerplate repo implements a provider using the raw gRPC interface rather than plugin.providerServer and 2) our own native providers also implement the raw gRPC interface over plugin.providerServer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Construct be an adapter for plugin.providerServer instead? Or should we have adapters for both the raw gRPC interface and plugin.providerServer?

The reason I made Construct an adapter for the raw gRPC interface is because 1) our existing provider boilerplate repo implements a provider using the raw gRPC interface rather than plugin.providerServer and 2) our own native providers also implement the raw gRPC interface over plugin.providerServer.

IMO we should adapt from plugin.providerServer, but I think that we can move forward with what you have. In the fullness of time, I'd love for all of our Go providers to be sitting on top of providerServer.

sdk/go/pulumi/provider/empty.s Show resolved Hide resolved
@justinvp justinvp merged commit 780a0c8 into master Apr 16, 2021
@pulumi-bot pulumi-bot deleted the justin/goprovider branch April 16, 2021 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support defining remote components in Go
3 participants