Move language host logic from Node to Go (#901)
* experimental: separate language host from node * Remove langhost details from the NodeJS SDK runtime * Cleanup * Work around an issue where Node sometimes loads the same module twice in two different contexts, resulting in two distinct module objects. Some additional cleanup. * Add some tests * Fix up the Windows script * Fix up the install scripts and Windows build * Code review feedback * Code review feedback: error capitalization
This commit is contained in:
parent
296151e088
commit
e87204d3e1
23
build.proj
23
build.proj
|
@ -58,6 +58,18 @@
|
|||
WorkingDirectory="$(NodeJSSdkDirectory)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="GoCompileNodeSdk">
|
||||
<ItemGroup>
|
||||
<GoPackagesToBuild Include="github.com/pulumi/pulumi/sdk/nodejs/cmd/pulumi-langhost-nodejs" />
|
||||
</ItemGroup>
|
||||
|
||||
<Exec Command="git describe --tags 2>nul" ConsoleToMSBuild="true" Condition="'$(Version)' == ''">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="Version" />
|
||||
</Exec>
|
||||
|
||||
<Exec Command="go install -ldflags "-X main.version=$(Version)" %(GoPackagesToBuild.Identity)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BinplaceNodeSdkProtos">
|
||||
<ItemGroup>
|
||||
<NodeSdkProtosForBinplace Include="$(NodeSdkDirectory)\proto\nodejs\**\*" />
|
||||
|
@ -99,7 +111,7 @@
|
|||
</Target>
|
||||
|
||||
<Target Name="BuildNodeSdk"
|
||||
DependsOnTargets="CopyNodeSdkProtos;BuildNativeRuntimeModule;TypeScriptCompileNodeSdk;BinPlaceNodeSdk">
|
||||
DependsOnTargets="CopyNodeSdkProtos;BuildNativeRuntimeModule;TypeScriptCompileNodeSdk;GoCompileNodeSdk;BinPlaceNodeSdk">
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildGoCmds">
|
||||
|
@ -119,13 +131,18 @@
|
|||
</Target>
|
||||
|
||||
<Target Name="IntegrationTest">
|
||||
<Exec Command="where pulumi-langhost-nodejs.cmd"
|
||||
<Exec Command="where pulumi-langhost-nodejs-exec.cmd"
|
||||
IgnoreExitCode="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="WhereLangHostExecExitCode" />
|
||||
</Exec>
|
||||
|
||||
<Exec Command="where pulumi-langhost-nodejs"
|
||||
IgnoreExitCode="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="WhereLangHostExitCode" />
|
||||
</Exec>
|
||||
|
||||
<Error Text="Please add "$(NodeJSSdkDirectory)\bin" to your path before running integration tests."
|
||||
Condition="$(WhereLangHostExitCode) != 0"/>
|
||||
Condition="$(WhereLangHostExitCode) != 0 Or $(WhereLangHostExecExitCode) != 0"/>
|
||||
|
||||
<!-- Ignore the exit code (but retain it) so we can kill all the lingering node processes even when go test
|
||||
fails. Otherwise, the AppVeyor job would hang until it reached the timeout -->
|
||||
|
|
|
@ -30,12 +30,12 @@ function CopyPackage($pathToModule, $moduleName) {
|
|||
RunGoBuild "github.com/pulumi/pulumi"
|
||||
CopyPackage "$Root\sdk\nodejs\bin" "pulumi"
|
||||
|
||||
Copy-Item "$Root\dist\sdk\nodejs\pulumi-langhost-nodejs.cmd" "$PublishDir\bin"
|
||||
Copy-Item "$Root\dist\sdk\nodejs\pulumi-langhost-nodejs-exec.cmd" "$PublishDir\bin"
|
||||
New-Item -ItemType Directory -Force -Path "$PublishDir\bin\node" | Out-Null
|
||||
Copy-Item "$Root\sdk\nodejs\custom_node\node.exe" "$PublishDir\bin\node"
|
||||
|
||||
|
||||
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-langhost-nodejs.cmd"
|
||||
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-langhost-nodejs-exec.cmd"
|
||||
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-provider-pulumi-nodejs.cmd"
|
||||
|
||||
# By default, if the archive already exists, 7zip will just add files to it, so blow away the existing
|
||||
|
|
|
@ -47,7 +47,7 @@ copy_package() {
|
|||
run_go_build "${ROOT}"
|
||||
|
||||
# Copy over the langhost and dynamic provider
|
||||
cp ${ROOT}/sdk/nodejs/pulumi-langhost-nodejs ${PUBDIR}/bin/
|
||||
cp ${ROOT}/sdk/nodejs/pulumi-langhost-nodejs-exec ${PUBDIR}/bin/
|
||||
cp ${ROOT}/sdk/nodejs/pulumi-provider-pulumi-nodejs ${PUBDIR}/bin/
|
||||
|
||||
# Copy packages
|
||||
|
|
|
@ -2,6 +2,15 @@ PROJECT_NAME := Pulumi Node.JS SDK
|
|||
NODE_MODULE_NAME := pulumi
|
||||
VERSION := $(shell git describe --tags --dirty 2>/dev/null)
|
||||
|
||||
LANGUAGE_HOST := github.com/pulumi/pulumi/sdk/nodejs/cmd/pulumi-langhost-nodejs
|
||||
|
||||
GOMETALINTERBIN := gometalinter
|
||||
GOMETALINTER := ${GOMETALINTERBIN} --config=../../Gometalinter.json
|
||||
|
||||
PROJECT_PKGS := $(shell go list ./cmd...)
|
||||
TESTPARALLELISM := 10
|
||||
TEST_FAST_TIMEOUT := 2m
|
||||
|
||||
include ../../build/common.mk
|
||||
|
||||
export PATH:=$(shell yarn bin 2>/dev/null):$(PATH)
|
||||
|
@ -10,9 +19,11 @@ ensure::
|
|||
cd runtime/native && ./ensure_node_v8.sh
|
||||
|
||||
lint::
|
||||
$(GOMETALINTER) cmd/pulumi-langhost-nodejs/main.go | sort ; exit "$${PIPESTATUS[0]}"
|
||||
tslint -c tslint.json -p tsconfig.json
|
||||
|
||||
build::
|
||||
go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGUAGE_HOST}
|
||||
cd runtime/native && node-gyp configure
|
||||
cp -R ../proto/nodejs/. proto/
|
||||
cd runtime/native/ && node-gyp build
|
||||
|
@ -27,7 +38,8 @@ build::
|
|||
find tests/runtime/langhost/cases/* -type d -exec cp -R {} bin/tests/runtime/langhost/cases/ \;
|
||||
|
||||
install::
|
||||
cp pulumi-langhost-nodejs "$(PULUMI_BIN)"
|
||||
GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGUAGE_HOST}
|
||||
cp pulumi-langhost-nodejs-exec "$(PULUMI_BIN)"
|
||||
cp pulumi-provider-pulumi-nodejs "$(PULUMI_BIN)"
|
||||
rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)/tests"
|
||||
|
||||
|
@ -35,3 +47,4 @@ test_fast::
|
|||
istanbul cover --print none _mocha -- --timeout 15000 'bin/tests/**/*.spec.js'
|
||||
istanbul report text-summary
|
||||
istanbul report text
|
||||
go test -timeout $(TEST_FAST_TIMEOUT) -cover -parallel ${TESTPARALLELISM} ${PROJECT_PKGS}
|
||||
|
|
|
@ -8,4 +8,4 @@ REM
|
|||
REM NOTE: we pass a dummy argument before the actual args because the
|
||||
REM langhost module expects to be invoked as `node path/to/langhost args`,
|
||||
REM but we are invoking it with `-e`.
|
||||
"%~dp0\..\custom_node\node.exe" -e "require('%REQUIRE_ROOT%./cmd/langhost');" dummy_argument %*
|
||||
"%~dp0\..\custom_node\node.exe" -e "require('%REQUIRE_ROOT%./cmd/run');" dummy_argument %*
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||
|
||||
// This is the primary entrypoint for all Pulumi programs that are being watched by the resource planning
|
||||
// monitor. It creates the "host" that is responsible for wiring up gRPC connections to and from the monitor,
|
||||
// and drives execution of a Node.js program, communicating back as required to track all resource allocations.
|
||||
|
||||
import * as minimist from "minimist";
|
||||
import * as runtime from "../../runtime";
|
||||
|
||||
export function main(rawArgs: string[]): void {
|
||||
// Parse command line flags
|
||||
const argv: minimist.ParsedArgs = minimist(rawArgs, {
|
||||
string: [ "tracing" ],
|
||||
});
|
||||
|
||||
// Extract the real arguments containing the monitor and optional server addresses
|
||||
const args = argv._.slice(2);
|
||||
|
||||
// The program requires a single argument: the address of the RPC endpoint for the resource monitor. It
|
||||
// optionally also takes a second argument, a reference back to the engine, but this may be missing.
|
||||
if (args.length === 0) {
|
||||
console.error("fatal: Missing <monitor> address");
|
||||
process.exit(-1);
|
||||
return;
|
||||
}
|
||||
const monitorAddr: string = args[0];
|
||||
let serverAddr: string | undefined;
|
||||
if (args.length > 1) {
|
||||
serverAddr = args[1];
|
||||
}
|
||||
|
||||
// Finally connect up the gRPC client/server and listen for incoming requests.
|
||||
const { server, port } = runtime.serveLanguageHost(monitorAddr, serverAddr, argv["logging"]);
|
||||
|
||||
// Emit the address so the monitor can read it to connect. The gRPC server will keep the message loop alive.
|
||||
console.log(port);
|
||||
}
|
||||
|
||||
main(process.argv);
|
||||
|
226
sdk/nodejs/cmd/pulumi-langhost-nodejs/main.go
Normal file
226
sdk/nodejs/cmd/pulumi-langhost-nodejs/main.go
Normal file
|
@ -0,0 +1,226 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
// pulumi-langhost-nodejs serves as the "language host" for Pulumi
|
||||
// programs written in NodeJS. It is ultimately responsible for spawning the
|
||||
// language runtime that executes the program.
|
||||
//
|
||||
// The program being executed is executed by a shim script called
|
||||
// `pulumi-langhost-nodejs-exec`. This script is written in the hosted
|
||||
// language (in this case, node) and is responsible for initiating RPC
|
||||
// links to the resource monitor and engine.
|
||||
//
|
||||
// It's therefore the responsibility of this program to implement
|
||||
// the LanguageHostServer endpoint by spawning instances of
|
||||
// `pulumi-langhost-nodejs-exec` and forwarding the RPC request arguments
|
||||
// to the command-line.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/golang/glog"
|
||||
pbempty "github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/rpcutil"
|
||||
"github.com/pulumi/pulumi/pkg/version"
|
||||
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
// By convention, the executor is the name of the current program
|
||||
// (pulumi-langhost-nodejs) plus this suffix.
|
||||
nodeExecSuffix = "-exec" // the exec shim for Pulumi to run Node programs.
|
||||
|
||||
// The runtime expects the config object to be saved to this environment variable.
|
||||
pulumiConfigVar = "PULUMI_CONFIG"
|
||||
)
|
||||
|
||||
// Launches the language host RPC endpoint, which in turn fires
|
||||
// up an RPC server implementing the LanguageRuntimeServer RPC
|
||||
// endpoint.
|
||||
func main() {
|
||||
var tracing string
|
||||
var givenExecutor string
|
||||
flag.StringVar(&tracing, "tracing", "",
|
||||
"Emit tracing to a Zipkin-compatible tracing endpoint")
|
||||
|
||||
// You can use the below flag to request that the language host load
|
||||
// a specific executor instead of probing the PATH. This is used specifically
|
||||
// in run.spec.ts to work around some unfortunate Node module loading behavior.
|
||||
flag.StringVar(&givenExecutor, "use-executor", "",
|
||||
"Use the given program as the executor instead of looking for one on PATH")
|
||||
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
cmdutil.InitLogging(false, 0, false)
|
||||
cmdutil.InitTracing(os.Args[0], tracing)
|
||||
var nodeExec string
|
||||
if givenExecutor == "" {
|
||||
pathExec, err := exec.LookPath(os.Args[0] + nodeExecSuffix)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "could not find `%s` on the $PATH", os.Args[0]+nodeExecSuffix)
|
||||
cmdutil.Exit(err)
|
||||
}
|
||||
|
||||
glog.V(3).Infof("language host identified executor from path: `%s`", pathExec)
|
||||
nodeExec = pathExec
|
||||
} else {
|
||||
glog.V(3).Infof("language host asked to use specific executor: `%s`", givenExecutor)
|
||||
nodeExec = givenExecutor
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
cmdutil.Exit(errors.New("missing host engine RPC address as first argument"))
|
||||
}
|
||||
|
||||
monitorAddress := args[0]
|
||||
// Optionally pluck out the engine so we can do logging, etc.
|
||||
var engineAddress string
|
||||
if len(args) > 1 {
|
||||
engineAddress = args[1]
|
||||
}
|
||||
|
||||
// Fire up a gRPC server, letting the kernel choose a free port.
|
||||
port, done, err := rpcutil.Serve(0, nil, []func(*grpc.Server) error{
|
||||
func(srv *grpc.Server) error {
|
||||
host := newLanguageHost(nodeExec, monitorAddress, engineAddress, tracing)
|
||||
pulumirpc.RegisterLanguageRuntimeServer(srv, host)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
cmdutil.Exit(errors.Wrapf(err, "could not start language host RPC server"))
|
||||
}
|
||||
|
||||
// Otherwise, print out the port so that the spawner knows how to reach us.
|
||||
fmt.Printf("%d\n", port)
|
||||
// And finally wait for the server to stop serving.
|
||||
if err := <-done; err != nil {
|
||||
cmdutil.Exit(errors.Wrapf(err, "language host RPC stopped serving"))
|
||||
}
|
||||
}
|
||||
|
||||
// nodeLanguageHost implements the LanguageRuntimeServer interface
|
||||
// for use as an API endpoint.
|
||||
type nodeLanguageHost struct {
|
||||
exec string
|
||||
monitorAddress string
|
||||
engineAddress string
|
||||
tracing string
|
||||
}
|
||||
|
||||
func newLanguageHost(exec, monitorAddress, engineAddress, tracing string) pulumirpc.LanguageRuntimeServer {
|
||||
return &nodeLanguageHost{
|
||||
exec: exec,
|
||||
monitorAddress: monitorAddress,
|
||||
engineAddress: engineAddress,
|
||||
tracing: tracing,
|
||||
}
|
||||
}
|
||||
|
||||
// constructArguments constructs a command-line for `pulumi-langhost-nodejs`
|
||||
// by enumerating all of the optional and non-optional arguments present
|
||||
// in a RunRequest.
|
||||
func (host *nodeLanguageHost) constructArguments(req *pulumirpc.RunRequest) []string {
|
||||
var args []string
|
||||
maybeAppendArg := func(k, v string) {
|
||||
if v != "" {
|
||||
args = append(args, "--"+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
maybeAppendArg("monitor", host.monitorAddress)
|
||||
maybeAppendArg("engine", host.engineAddress)
|
||||
maybeAppendArg("project", req.GetProject())
|
||||
maybeAppendArg("stack", req.GetStack())
|
||||
maybeAppendArg("pwd", req.GetPwd())
|
||||
if req.GetDryRun() {
|
||||
args = append(args, "--dry-run")
|
||||
}
|
||||
|
||||
maybeAppendArg("parallel", fmt.Sprint(req.GetParallel()))
|
||||
maybeAppendArg("tracing", host.tracing)
|
||||
if req.GetProgram() == "" {
|
||||
// If the program path is empty, just use "."; this will cause Node to try to load the default module
|
||||
// file, by default ./index.js, but possibly overridden in the "main" element inside of package.json.
|
||||
args = append(args, ".")
|
||||
} else {
|
||||
args = append(args, req.GetProgram())
|
||||
}
|
||||
|
||||
args = append(args, req.GetArgs()...)
|
||||
return args
|
||||
}
|
||||
|
||||
// constructConfig json-serializes the configuration data given as part of
|
||||
// a RunRequest.
|
||||
func (host *nodeLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
|
||||
configMap := req.GetConfig()
|
||||
if configMap == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(configMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(configJSON), nil
|
||||
}
|
||||
|
||||
// RPC endpoint for LanguageRuntimeServer::Run
|
||||
func (host *nodeLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) {
|
||||
args := host.constructArguments(req)
|
||||
config, err := host.constructConfig(req)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to serialize configuration")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if glog.V(5) {
|
||||
commandStr := strings.Join(args, " ")
|
||||
glog.V(5).Infoln("Language host launching process: ", host.exec, commandStr)
|
||||
}
|
||||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
var errResult string
|
||||
cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name.
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), pulumiConfigVar+"="+string(config))
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// If the program ran, but exited with a non-zero error code. This will happen often, since user
|
||||
// errors will trigger this. So, the error message should look as nice as possible.
|
||||
if status, stok := exiterr.Sys().(syscall.WaitStatus); stok {
|
||||
err = errors.Errorf("Program exited with non-zero exit code: %d", status.ExitStatus())
|
||||
} else {
|
||||
err = errors.Wrapf(exiterr, "Program exited unexpectedly")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we didn't even get to run the program. This ought to never happen unless there's
|
||||
// a bug or system condition that prevented us from running the language exec. Issue a scarier error.
|
||||
err = errors.Wrapf(err, "Problem executing program (could not run language executor)")
|
||||
}
|
||||
|
||||
errResult = err.Error()
|
||||
}
|
||||
|
||||
return &pulumirpc.RunResponse{Error: errResult}, nil
|
||||
}
|
||||
|
||||
func (host *nodeLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
|
||||
return &pulumirpc.PluginInfo{
|
||||
Version: version.Version,
|
||||
}, nil
|
||||
}
|
62
sdk/nodejs/cmd/pulumi-langhost-nodejs/main_test.go
Normal file
62
sdk/nodejs/cmd/pulumi-langhost-nodejs/main_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestArgumentConstruction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("DryRun-NoArguments", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{DryRun: true}
|
||||
args := host.constructArguments(rr)
|
||||
assert.Contains(tt, args, "--dry-run")
|
||||
assert.NotContains(tt, args, "true")
|
||||
})
|
||||
|
||||
t.Run("OptionalArgs-PassedIfSpecified", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{Project: "foo"}
|
||||
args := strings.Join(host.constructArguments(rr), " ")
|
||||
assert.Contains(tt, args, "--project foo")
|
||||
})
|
||||
|
||||
t.Run("OptionalArgs-NotPassedIfNotSpecified", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{}
|
||||
args := strings.Join(host.constructArguments(rr), " ")
|
||||
assert.NotContains(tt, args, "--stack")
|
||||
})
|
||||
|
||||
t.Run("DotIfProgramNotSpecified", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{}
|
||||
args := strings.Join(host.constructArguments(rr), " ")
|
||||
assert.Contains(tt, args, ".")
|
||||
})
|
||||
|
||||
t.Run("ProgramIfProgramSpecified", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{Program: "foobar"}
|
||||
args := strings.Join(host.constructArguments(rr), " ")
|
||||
assert.Contains(tt, args, "foobar")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Config-Empty", func(tt *testing.T) {
|
||||
host := &nodeLanguageHost{}
|
||||
rr := &pulumirpc.RunRequest{Project: "foo"}
|
||||
str, err := host.constructConfig(rr)
|
||||
assert.NoError(tt, err)
|
||||
assert.JSONEq(tt, "{}", str)
|
||||
})
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require("pulumi/cmd/langhost");
|
||||
|
3
sdk/nodejs/pulumi-langhost-nodejs-exec
Executable file
3
sdk/nodejs/pulumi-langhost-nodejs-exec
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
require("pulumi/cmd/run");
|
||||
|
3
sdk/nodejs/pulumi-langhost-nodejs-exec-test
Executable file
3
sdk/nodejs/pulumi-langhost-nodejs-exec-test
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
require("./bin/cmd/run");
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
export * from "./closure";
|
||||
export * from "./config";
|
||||
export * from "./invoke";
|
||||
export * from "./langhost";
|
||||
export * from "./resource";
|
||||
export * from "./rpc";
|
||||
export * from "./settings";
|
||||
|
|
|
@ -1,210 +0,0 @@
|
|||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import * as childprocess from "child_process";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as runtime from "../runtime";
|
||||
import { version } from "../version";
|
||||
|
||||
const grpc = require("grpc");
|
||||
const langproto = require("../proto/language_pb.js");
|
||||
const langrpc = require("../proto/language_grpc_pb.js");
|
||||
const plugproto = require("../proto/plugin_pb.js");
|
||||
|
||||
/**
|
||||
* monitorAddr is the current resource monitor address.
|
||||
*/
|
||||
let monitorAddr: string | undefined;
|
||||
/**
|
||||
* engineAddr is the current resource engine address, if any.
|
||||
*/
|
||||
let engineAddr: string | undefined;
|
||||
/**
|
||||
* tracingUrl is the current resource engine address, if any.
|
||||
*/
|
||||
let tracingUrl: string | undefined;
|
||||
|
||||
/**
|
||||
* serveLanguageHost spawns a language host that connects to the resource monitor and listens on port.
|
||||
*/
|
||||
export function serveLanguageHost(monitor: string, engine?: string, tracing?: string): { server: any, port: number } {
|
||||
if (monitorAddr) {
|
||||
throw new Error("Already connected to a resource monitor; cannot serve two hosts in one process");
|
||||
}
|
||||
monitorAddr = monitor;
|
||||
engineAddr = engine;
|
||||
tracingUrl = tracing;
|
||||
|
||||
// TODO[pulumi/pulumi#545]: Wire up to OpenTracing. Automatic tracing of gRPC calls themselves is pending
|
||||
// https://github.com/grpc-ecosystem/grpc-opentracing/issues/11 which is pending
|
||||
// https://github.com/grpc/grpc-node/pull/59.
|
||||
|
||||
// Now fire up the gRPC server and begin serving!
|
||||
const server = new grpc.Server();
|
||||
server.addService(langrpc.LanguageRuntimeService, {
|
||||
run: runRPC,
|
||||
getPluginInfo: getPluginInfoRPC,
|
||||
});
|
||||
const port: number = server.bind(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure());
|
||||
|
||||
// Now we're done: the server is started, and gRPC keeps the even loop alive.
|
||||
server.start();
|
||||
return { server: server, port: port }; // return the port for callers.
|
||||
}
|
||||
|
||||
/**
|
||||
* runRPC implements the core "run" logic for both planning and deploying.
|
||||
*/
|
||||
function runRPC(call: any, callback: any): void {
|
||||
// Unpack the request and fire up the program.
|
||||
// IDEA: stick the monitor address in Run's RPC so that it's per invocation.
|
||||
const req: any = call.request;
|
||||
const resp = new langproto.RunResponse();
|
||||
let proc: childprocess.ChildProcess | undefined;
|
||||
try {
|
||||
// Create an args array to pass to spawn, starting with just the run.js program.
|
||||
const args: string[] = [
|
||||
path.join(__filename, "..", "..", "cmd", "run"),
|
||||
];
|
||||
|
||||
// Serialize the config args using an environment variable.
|
||||
const env: {[key: string]: string} = {};
|
||||
const config: any = req.getConfigMap();
|
||||
if (config) {
|
||||
// First flatten the config into a regular (non-RPC) object.
|
||||
const configForEnv: {[key: string]: string} = {};
|
||||
for (const entry of config.entries()) {
|
||||
configForEnv[(entry[0] as string)] = (entry[1] as string);
|
||||
}
|
||||
// Now JSON serialize the config into an environment variable.
|
||||
env[runtime.configEnvKey] = JSON.stringify(configForEnv);
|
||||
}
|
||||
|
||||
const project: string | undefined = req.getProject();
|
||||
if (project) {
|
||||
args.push("--project");
|
||||
args.push(project);
|
||||
}
|
||||
|
||||
const stack: string | undefined = req.getStack();
|
||||
if (stack) {
|
||||
args.push("--stack");
|
||||
args.push(stack);
|
||||
}
|
||||
|
||||
// If this is a dry-run, tell the program so.
|
||||
if (req.getDryrun()) {
|
||||
args.push("--dry-run");
|
||||
}
|
||||
|
||||
// If parallel execution has been requested, propagate it.
|
||||
const parallel: number | undefined = req.getParallel();
|
||||
if (parallel !== undefined) {
|
||||
args.push("--parallel");
|
||||
args.push(parallel.toString());
|
||||
}
|
||||
|
||||
// If a different working directory was requested, make sure to pass it too.
|
||||
const pwd: string | undefined = req.getPwd();
|
||||
if (pwd) {
|
||||
args.push("--pwd");
|
||||
args.push(pwd);
|
||||
}
|
||||
|
||||
// Push the resource monitor address to connect up to.
|
||||
if (!monitorAddr) {
|
||||
throw new Error("No resource monitor known; please ensure the language host is alive");
|
||||
}
|
||||
args.push("--monitor");
|
||||
args.push(monitorAddr);
|
||||
|
||||
// Push the resource engine address, for logging, etc., if there is one.
|
||||
if (engineAddr) {
|
||||
args.push("--engine");
|
||||
args.push(engineAddr);
|
||||
}
|
||||
|
||||
// Push the tracing url, if there is one.
|
||||
if (tracingUrl) {
|
||||
args.push("--tracing");
|
||||
args.push(tracingUrl);
|
||||
}
|
||||
|
||||
// Now get a path to the program.
|
||||
let program: string | undefined = req.getProgram();
|
||||
if (!program) {
|
||||
// If the program path is empty, just use "."; this will cause Node to try to load the default module
|
||||
// file, by default ./index.js, but possibly overridden in the "main" element inside of package.json.
|
||||
program = ".";
|
||||
}
|
||||
args.push(program);
|
||||
|
||||
// Serialize the args plainly, following the program.
|
||||
const argsList: string[] | undefined = req.getArgsList();
|
||||
if (argsList) {
|
||||
for (const arg of argsList) {
|
||||
args.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
// We spawn a new process to run the program. This is required because we don't want the run to complete
|
||||
// until the Node message loop quiesces. It also gives us an extra level of isolation.
|
||||
proc = childprocess.spawn(process.argv[0], args, {
|
||||
env: Object.assign({}, process.env, env),
|
||||
});
|
||||
proc.stdout.on("data", (data: string | Buffer) => {
|
||||
console.log(stripEOL(data));
|
||||
});
|
||||
proc.stderr.on("data", (data: string | Buffer) => {
|
||||
console.error(stripEOL(data));
|
||||
});
|
||||
|
||||
// If we got this far, make sure to communicate completion when the process terminates.
|
||||
proc.on("close", (code: number, signal: string) => {
|
||||
if (callback !== undefined) {
|
||||
if (code !== 0) {
|
||||
if (signal) {
|
||||
resp.setError(`Program exited due to a signal: ${signal}`);
|
||||
}
|
||||
else {
|
||||
resp.setError(`Program exited with non-zero exit code: ${code}`);
|
||||
}
|
||||
}
|
||||
callback(undefined, resp);
|
||||
callback = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
catch (err) {
|
||||
if (callback !== undefined) {
|
||||
resp.setError(err.message);
|
||||
callback(undefined, resp);
|
||||
callback = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stripEOL(data: string | Buffer): string {
|
||||
let dataString: string;
|
||||
if (typeof data === "string") {
|
||||
dataString = data;
|
||||
}
|
||||
else {
|
||||
dataString = data.toString("utf-8");
|
||||
}
|
||||
const eolIndex = dataString.lastIndexOf(os.EOL);
|
||||
if (eolIndex !== -1) {
|
||||
dataString = dataString.substring(0, eolIndex);
|
||||
}
|
||||
return dataString;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPluginInfoRPC implements the RPC interface for plugin introspection.
|
||||
*/
|
||||
function getPluginInfoRPC(call: any, callback: any): void {
|
||||
const resp = new plugproto.PluginInfo();
|
||||
resp.setVersion(version);
|
||||
callback(undefined, resp);
|
||||
}
|
|
@ -481,9 +481,33 @@ function createMockResourceMonitor(
|
|||
}
|
||||
|
||||
function serveLanguageHostProcess(monitorAddr: string): { proc: childProcess.ChildProcess, addr: Promise<string> } {
|
||||
// Spawn the language host in a separate process so that each test case gets an isolated heap, globals, etc.
|
||||
const proc = childProcess.spawn(process.argv[0], [
|
||||
path.join(__filename, "..", "..", "..", "..", "cmd", "langhost", "index.js"),
|
||||
// A quick note about this:
|
||||
//
|
||||
// Normally, pulumi-langhost-nodejs probes the path in order to
|
||||
// find the nodejs executor, pulumi-langhost-nodejs-exec. This works
|
||||
// great in all scenarios other than testing within this file. If the executor
|
||||
// that it founds resides in the Pulumi install dir (which it will, if these tests
|
||||
// are being executed by `make`), then Node will execute it by resolving our relative
|
||||
// path requires to the Pulumi install directory. However, the programs being evaluated
|
||||
// by the language host are using the current directory to resolve relative path requires.
|
||||
//
|
||||
// Normally, this isn't a problem - ostensibly the stuff in the Pulumi install directory
|
||||
// is the same as the stuff that we are currently testing. However, the "settings.ts" module
|
||||
// contains some global state that is expected to be shared between the language host stub
|
||||
// that is connecting to our RPC endpoints (run/index.ts) and the Pulumi program being
|
||||
// evaluated. Node, when resolving the require, will pick different modules to load depending
|
||||
// on which context the require occured; if it happened in run/index.ts, it'll load
|
||||
// from Pulumi install directory, while requires coming from anywhere else will load
|
||||
// from the source directory. Because these are two different files, Node instantiates two
|
||||
// separate module objects and the state that we are expecting to share is not actually shared,
|
||||
// ultimately resulting in extremely wacky errors.
|
||||
//
|
||||
// In order to work around this problem, the langhost is explicitly instructed
|
||||
// (through --use-executor) to use a specific executor which will load modules from
|
||||
// the source directory and not the install directory.
|
||||
const proc = childProcess.spawn("pulumi-langhost-nodejs", [
|
||||
"--use-executor",
|
||||
path.join(__filename, "..", "..", "..", "..", "..", "pulumi-langhost-nodejs-exec-test"),
|
||||
monitorAddr,
|
||||
]);
|
||||
// Hook the first line so we can parse the address. Then we hook the rest to print for debugging purposes, and
|
||||
|
|
|
@ -36,14 +36,12 @@
|
|||
"runtime/config.ts",
|
||||
"runtime/debuggable.ts",
|
||||
"runtime/invoke.ts",
|
||||
"runtime/langhost.ts",
|
||||
"runtime/resource.ts",
|
||||
"runtime/rpc.ts",
|
||||
"runtime/settings.ts",
|
||||
"runtime/stack.ts",
|
||||
|
||||
"cmd/dynamic-provider/index.ts",
|
||||
"cmd/langhost/index.ts",
|
||||
"cmd/run/index.ts",
|
||||
|
||||
"tests/config.spec.ts",
|
||||
|
|
Loading…
Reference in a new issue