pulumi/pkg/resource/deploy/deploytest/pluginhost.go
Pat Gavlin 855f1fd1cd
Revise host mode. (#5317)
* Revise host mode.

The current implementation of host mode uses a `pulumi host` command and
an ad-hoc communication protocol between the engine and client to
connect a language host after the host has begun listening. The most
significant disadvantages of this approach are the communication
protocol (which currently requires the use of stdout), the host-specific
command, and the difficulty of accommodating the typical program-bound
lifetime for an update.

These changes reimplement host mode by adding engine support for
connecting to an existing language runtime service rather than launching
a plugin. This capability is provided via an engine-specific language
runtime, `client`, which accepts the address of the existing languge
runtime service as a runtime option. The CLI exposes this runtime via
the `--client` flag to the `up` and `preview` commands, which similarly
accepts the address of an existing language runtime service as an
argument. These changes also adjust the automation API to consume the
new host mode implementation.
2020-09-14 17:40:17 -07:00

256 lines
6.6 KiB
Go

// Copyright 2016-2018, 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 deploytest
import (
"context"
"fmt"
"sync"
"github.com/blang/semver"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
"google.golang.org/grpc"
"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/rpcutil"
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
pulumirpc "github.com/pulumi/pulumi/sdk/v2/proto/go"
)
type LoadProviderFunc func() (plugin.Provider, error)
type LoadProviderWithHostFunc func(host plugin.Host) (plugin.Provider, error)
type ProviderLoader struct {
pkg tokens.Package
version semver.Version
load LoadProviderFunc
loadWithHost LoadProviderWithHostFunc
}
func NewProviderLoader(pkg tokens.Package, version semver.Version, load LoadProviderFunc) *ProviderLoader {
return &ProviderLoader{
pkg: pkg,
version: version,
load: load,
}
}
func NewProviderLoaderWithHost(pkg tokens.Package, version semver.Version,
load LoadProviderWithHostFunc) *ProviderLoader {
return &ProviderLoader{
pkg: pkg,
version: version,
loadWithHost: load,
}
}
type hostEngine struct {
sink diag.Sink
statusSink diag.Sink
address string
stop chan bool
}
func (e *hostEngine) Log(_ context.Context, req *pulumirpc.LogRequest) (*pbempty.Empty, error) {
var sev diag.Severity
switch req.Severity {
case pulumirpc.LogSeverity_DEBUG:
sev = diag.Debug
case pulumirpc.LogSeverity_INFO:
sev = diag.Info
case pulumirpc.LogSeverity_WARNING:
sev = diag.Warning
case pulumirpc.LogSeverity_ERROR:
sev = diag.Error
default:
return nil, errors.Errorf("Unrecognized logging severity: %v", req.Severity)
}
if req.Ephemeral {
e.statusSink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId))
} else {
e.sink.Logf(sev, diag.StreamMessage(resource.URN(req.Urn), req.Message, req.StreamId))
}
return &pbempty.Empty{}, nil
}
func (e *hostEngine) GetRootResource(_ context.Context,
req *pulumirpc.GetRootResourceRequest) (*pulumirpc.GetRootResourceResponse, error) {
return nil, errors.New("unsupported")
}
func (e *hostEngine) SetRootResource(_ context.Context,
req *pulumirpc.SetRootResourceRequest) (*pulumirpc.SetRootResourceResponse, error) {
return nil, errors.New("unsupported")
}
type pluginHost struct {
providerLoaders []*ProviderLoader
languageRuntime plugin.LanguageRuntime
sink diag.Sink
statusSink diag.Sink
engine *hostEngine
providers map[plugin.Provider]struct{}
closed bool
m sync.Mutex
}
func NewPluginHost(sink, statusSink diag.Sink, languageRuntime plugin.LanguageRuntime,
providerLoaders ...*ProviderLoader) plugin.Host {
engine := &hostEngine{
sink: sink,
statusSink: statusSink,
stop: make(chan bool),
}
port, _, err := rpcutil.Serve(0, engine.stop, []func(*grpc.Server) error{
func(srv *grpc.Server) error {
pulumirpc.RegisterEngineServer(srv, engine)
return nil
},
}, nil)
if err != nil {
panic(fmt.Errorf("could not start engine service: %v", err))
}
engine.address = fmt.Sprintf("127.0.0.1:%v", port)
return &pluginHost{
providerLoaders: providerLoaders,
languageRuntime: languageRuntime,
sink: sink,
statusSink: statusSink,
engine: engine,
providers: make(map[plugin.Provider]struct{}),
}
}
func (host *pluginHost) isClosed() bool {
host.m.Lock()
defer host.m.Unlock()
return host.closed
}
func (host *pluginHost) Provider(pkg tokens.Package, version *semver.Version) (plugin.Provider, error) {
var best *ProviderLoader
for _, l := range host.providerLoaders {
if l.pkg != pkg {
continue
}
if version != nil {
if l.version.EQ(*version) {
best = l
break
}
} else if best == nil || l.version.GT(best.version) {
best = l
}
}
if best == nil {
return nil, nil
}
load := best.load
if load == nil {
load = func() (plugin.Provider, error) {
return best.loadWithHost(host)
}
}
prov, err := load()
if err != nil {
return nil, err
}
host.m.Lock()
defer host.m.Unlock()
host.providers[prov] = struct{}{}
return prov, nil
}
func (host *pluginHost) LanguageRuntime(runtime string) (plugin.LanguageRuntime, error) {
return host.languageRuntime, nil
}
func (host *pluginHost) SignalCancellation() error {
host.m.Lock()
defer host.m.Unlock()
var err error
for prov := range host.providers {
if pErr := prov.SignalCancellation(); pErr != nil {
err = pErr
}
}
return err
}
func (host *pluginHost) Close() error {
host.m.Lock()
defer host.m.Unlock()
go func() { host.engine.stop <- true }()
host.closed = true
return nil
}
func (host *pluginHost) ServerAddr() string {
return host.engine.address
}
func (host *pluginHost) Log(sev diag.Severity, urn resource.URN, msg string, streamID int32) {
if !host.isClosed() {
host.sink.Logf(sev, diag.StreamMessage(urn, msg, streamID))
}
}
func (host *pluginHost) LogStatus(sev diag.Severity, urn resource.URN, msg string, streamID int32) {
if !host.isClosed() {
host.statusSink.Logf(sev, diag.StreamMessage(urn, msg, streamID))
}
}
func (host *pluginHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) {
return nil, errors.New("unsupported")
}
func (host *pluginHost) CloseProvider(provider plugin.Provider) error {
host.m.Lock()
defer host.m.Unlock()
delete(host.providers, provider)
return nil
}
func (host *pluginHost) ListPlugins() []workspace.PluginInfo {
return nil
}
func (host *pluginHost) EnsurePlugins(plugins []workspace.PluginInfo, kinds plugin.Flags) error {
return nil
}
func (host *pluginHost) GetRequiredPlugins(info plugin.ProgInfo,
kinds plugin.Flags) ([]workspace.PluginInfo, error) {
return nil, nil
}
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string,
opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) {
return nil, errors.New("unsupported")
}
func (host *pluginHost) ListAnalyzers() []plugin.Analyzer {
return nil
}