There's a fair bit of clean up in here, but the meat is: * Allocate the language runtime gRPC client connection on the goroutine that will use it; this eliminates race conditions. * The biggie: there *appears* to be a bug in gRPC's implementation on Linux, where it doesn't implement WaitForReady properly. The behavior I'm observing is that RPC calls will not retry as they are supposed to, but will instead spuriously fail during the RPC startup. To work around this, I've added manual retry logic in the shared plugin creation function so that we won't even try to use the client connection until it is in a well-known state. pulumi/pulumi-fabric#337 tracks getting to the bottom of this and, ideally, removing the work around. The other minor things are: * Separate run.js into its own module, so it doesn't include index.js and do a bunch of random stuff it shouldn't be doing. * Allow run.js to be invoked without a --monitor. This makes testing just the run part of invocation easier (including config, which turned out to be super useful as I was debugging). * Tidy up some messages.
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/pulumi/pulumi-fabric/pkg/tokens"
|
|
lumirpc "github.com/pulumi/pulumi-fabric/sdk/proto/go"
|
|
)
|
|
|
|
const LanguagePluginPrefix = "pulumi-langhost-"
|
|
|
|
// langhost reflects a language host plugin, loaded dynamically for a single language/runtime pair.
|
|
type langhost struct {
|
|
ctx *Context
|
|
runtime string
|
|
plug *plugin
|
|
client lumirpc.LanguageRuntimeClient
|
|
}
|
|
|
|
// NewLanguageRuntime binds to a language's runtime plugin and then creates a gRPC connection to it. If the
|
|
// plugin could not be found, or an error occurs while creating the child process, an error is returned.
|
|
func NewLanguageRuntime(host Host, ctx *Context, runtime string, monitorAddr string) (LanguageRuntime, error) {
|
|
// Go ahead and attempt to load the plugin from the PATH.
|
|
srvexe := LanguagePluginPrefix + strings.Replace(runtime, tokens.QNameDelimiter, "_", -1)
|
|
plug, err := newPlugin(ctx, srvexe,
|
|
fmt.Sprintf("langhost[%v]", runtime), []string{monitorAddr, host.ServerAddr()})
|
|
if err != nil {
|
|
return nil, err
|
|
} else if plug == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &langhost{
|
|
ctx: ctx,
|
|
runtime: runtime,
|
|
plug: plug,
|
|
client: lumirpc.NewLanguageRuntimeClient(plug.Conn),
|
|
}, nil
|
|
}
|
|
|
|
func (h *langhost) Runtime() string { return h.runtime }
|
|
|
|
// Run executes a program in the language runtime for planning or deployment purposes. If info.DryRun is true,
|
|
// the code must not assume that side-effects or final values resulting from resource deployments are actually
|
|
// available. If it is false, on the other hand, a real deployment is occurring and it may safely depend on these.
|
|
func (h *langhost) Run(info RunInfo) (string, error) {
|
|
glog.V(7).Infof("langhost[%v].Run(pwd=%v,program=%v,#args=%v,#config=%v,dryrun=%v) executing",
|
|
h.runtime, info.Pwd, info.Program, len(info.Args), len(info.Config), info.DryRun)
|
|
config := make(map[string]string)
|
|
for k, v := range info.Config {
|
|
config[string(k)] = v
|
|
}
|
|
resp, err := h.client.Run(h.ctx.Request(), &lumirpc.RunRequest{
|
|
Pwd: info.Pwd,
|
|
Program: info.Program,
|
|
Args: info.Args,
|
|
Config: config,
|
|
DryRun: info.DryRun,
|
|
})
|
|
if err != nil {
|
|
glog.V(7).Infof("langhost[%v].Run(pwd=%v,program=%v,...,dryrun=%v) failed: err=%v",
|
|
h.runtime, info.Pwd, info.Program, info.DryRun, err)
|
|
return "", err
|
|
}
|
|
|
|
progerr := resp.GetError()
|
|
glog.V(7).Infof("langhost[%v].RunPlan(pwd=%v,program=%v,...,dryrun=%v) success: progerr=%v",
|
|
h.runtime, info.Pwd, info.Program, info.DryRun, progerr)
|
|
return progerr, nil
|
|
}
|
|
|
|
// Close tears down the underlying plugin RPC connection and process.
|
|
func (h *langhost) Close() error {
|
|
return h.plug.Close()
|
|
}
|