diff --git a/CHANGELOG.md b/CHANGELOG.md index 30619c893..22369126a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ CHANGELOG ## HEAD (Unreleased) +- @pulumi/pulumi Now requires Nodejs version 8.13.0 and upwards or 10.10.0 and upwards. + - All data-source invocations are now asynchronous (Promise-returning) by default. - Lock dep ts-node to v8.5.4 [#3733](https://github.com/pulumi/pulumi/pull/3733) diff --git a/pkg/resource/plugin/plugin.go b/pkg/resource/plugin/plugin.go index 0451987db..65631b60a 100644 --- a/pkg/resource/plugin/plugin.go +++ b/pkg/resource/plugin/plugin.go @@ -211,7 +211,7 @@ func newPlugin(ctx *Context, pwd, bin, prefix string, args, env []string) (*plug // The server is unavailable. This is the Linux bug. Wait a little and retry. time.Sleep(time.Millisecond * 10) continue // keep retrying - case codes.Unimplemented, codes.ResourceExhausted: + case codes.Unimplemented, codes.ResourceExhausted, codes.Internal: // Since we sent "" as the method above, this is the expected response. Ready to go. break outer } diff --git a/sdk/nodejs/cmd/dynamic-provider/index.ts b/sdk/nodejs/cmd/dynamic-provider/index.ts index 2fec21231..9dec34c76 100644 --- a/sdk/nodejs/cmd/dynamic-provider/index.ts +++ b/sdk/nodejs/cmd/dynamic-provider/index.ts @@ -15,13 +15,14 @@ import * as minimist from "minimist"; import * as path from "path"; +import * as grpc from "@grpc/grpc-js"; + import * as dynamic from "../../dynamic"; import * as resource from "../../resource"; import * as runtime from "../../runtime"; import { version } from "../../version"; const requireFromString = require("require-from-string"); -const grpc = require("grpc"); const anyproto = require("google-protobuf/google/protobuf/any_pb.js"); const emptyproto = require("google-protobuf/google/protobuf/empty_pb.js"); const structproto = require("google-protobuf/google/protobuf/struct_pb.js"); @@ -32,6 +33,27 @@ const statusproto = require("../../proto/status_pb.js"); const providerKey: string = "__provider"; +// We track all uncaught errors here. If we have any, we will make sure we always have a non-0 exit +// code. +const uncaughtErrors = new Set(); +const uncaughtHandler = (err: Error) => { + if (!uncaughtErrors.has(err)) { + uncaughtErrors.add(err); + console.error(err.stack || err.message || ("" + err)); + } +}; + +process.on("uncaughtException", uncaughtHandler); +// @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so just +// suppress the TS strictness here. +process.on("unhandledRejection", uncaughtHandler); +process.on("exit", (code: number) => { + // If there were any uncaught errors at all, we always want to exit with an error code. + if (code === 0 && uncaughtErrors.size > 0) { + process.exitCode = 1; + } +}); + function getProvider(props: any): dynamic.ResourceProvider { // TODO[pulumi/pulumi#414]: investigate replacing requireFromString with eval return requireFromString(props[providerKey]).handler(); @@ -187,7 +209,8 @@ async function createRPC(call: any, callback: any): Promise { callback(undefined, resp); } catch (e) { - return callback(grpcResponseFromError(e)); + const response = grpcResponseFromError(e); + return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata); } } @@ -238,7 +261,8 @@ async function updateRPC(call: any, callback: any): Promise { callback(undefined, resp); } catch (e) { - return callback(grpcResponseFromError(e)); + const response = grpcResponseFromError(e); + return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata); } } @@ -274,7 +298,7 @@ function resultIncludingProvider(result: any, props: any): any { // rejected the resource, or an initialization error, where the API server has accepted the // resource, but it failed to initialize (e.g., the app code is continually crashing and the // resource has failed to become alive). -function grpcResponseFromError(e: {id: string, properties: any, message: string, reasons?: string[]}): any { +function grpcResponseFromError(e: {id: string, properties: any, message: string, reasons?: string[]}) { // Create response object. const resp = new statusproto.Status(); resp.setCode(grpc.status.UNKNOWN); @@ -308,7 +332,7 @@ function grpcResponseFromError(e: {id: string, properties: any, message: string, }; } -export function main(args: string[]): void { +export async function main(args: string[]) { // The program requires a single argument: the address of the RPC endpoint for the engine. It // optionally also takes a second argument, a reference back to the engine, but this may be missing. if (args.length === 0) { @@ -335,8 +359,15 @@ export function main(args: string[]): void { delete: deleteRPC, getPluginInfo: getPluginInfoRPC, }); - const port: number = server.bind(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure()); - + const port: number = await new Promise((resolve, reject) => { + server.bindAsync(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure(), (err, p) => { + if (err) { + reject(err); + } else { + resolve(p); + } + }); + }); server.start(); // Emit the address so the monitor can read it to connect. The gRPC server will keep the message loop alive. diff --git a/sdk/nodejs/package.json b/sdk/nodejs/package.json index 83af00380..292db4330 100644 --- a/sdk/nodejs/package.json +++ b/sdk/nodejs/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@pulumi/query": "^0.3.0", + "@grpc/grpc-js": "^0.6.15", "deasync": "^0.1.15", "google-protobuf": "^3.5.0", - "grpc": "1.24.2", "minimist": "^1.2.0", "normalize-package-data": "^2.4.0", "protobufjs": "^6.8.6", @@ -34,13 +34,12 @@ "@types/semver": "^5.5.0", "istanbul": "^0.4.5", "mocha": "^3.5.0", - "node-gyp": "^3.6.2", "tslint": "^5.11.0" }, "pulumi": { "comment": "Do not remove. Marks this as as a deployment-time-only package" }, "engines": { - "node": ">=8.0.0" + "node": ">=8.13.0 || >=10.10.0" } } diff --git a/sdk/nodejs/proto/analyzer_grpc_pb.js b/sdk/nodejs/proto/analyzer_grpc_pb.js index 82fba8deb..8ad1bb45d 100644 --- a/sdk/nodejs/proto/analyzer_grpc_pb.js +++ b/sdk/nodejs/proto/analyzer_grpc_pb.js @@ -16,7 +16,7 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); +var grpc = require('@grpc/grpc-js'); var analyzer_pb = require('./analyzer_pb.js'); var plugin_pb = require('./plugin_pb.js'); var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); diff --git a/sdk/nodejs/proto/engine_grpc_pb.js b/sdk/nodejs/proto/engine_grpc_pb.js index 74d91e7d8..4d1337b62 100644 --- a/sdk/nodejs/proto/engine_grpc_pb.js +++ b/sdk/nodejs/proto/engine_grpc_pb.js @@ -16,7 +16,7 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); +var grpc = require('@grpc/grpc-js'); var engine_pb = require('./engine_pb.js'); var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); diff --git a/sdk/nodejs/proto/language_grpc_pb.js b/sdk/nodejs/proto/language_grpc_pb.js index 7021ae302..b4b421f08 100644 --- a/sdk/nodejs/proto/language_grpc_pb.js +++ b/sdk/nodejs/proto/language_grpc_pb.js @@ -16,7 +16,7 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); +var grpc = require('@grpc/grpc-js'); var language_pb = require('./language_pb.js'); var plugin_pb = require('./plugin_pb.js'); var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); diff --git a/sdk/nodejs/proto/provider_grpc_pb.js b/sdk/nodejs/proto/provider_grpc_pb.js index 9cf415999..79e8d1075 100644 --- a/sdk/nodejs/proto/provider_grpc_pb.js +++ b/sdk/nodejs/proto/provider_grpc_pb.js @@ -16,7 +16,7 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); +var grpc = require('@grpc/grpc-js'); var provider_pb = require('./provider_pb.js'); var plugin_pb = require('./plugin_pb.js'); var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); diff --git a/sdk/nodejs/proto/resource_grpc_pb.js b/sdk/nodejs/proto/resource_grpc_pb.js index de97c946d..72b285fde 100644 --- a/sdk/nodejs/proto/resource_grpc_pb.js +++ b/sdk/nodejs/proto/resource_grpc_pb.js @@ -16,7 +16,7 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); +var grpc = require('@grpc/grpc-js'); var resource_pb = require('./resource_pb.js'); var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); var google_protobuf_struct_pb = require('google-protobuf/google/protobuf/struct_pb.js'); diff --git a/sdk/nodejs/runtime/invoke.ts b/sdk/nodejs/runtime/invoke.ts index 07121ce12..1a0ba0112 100644 --- a/sdk/nodejs/runtime/invoke.ts +++ b/sdk/nodejs/runtime/invoke.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as grpc from "@grpc/grpc-js"; import * as fs from "fs"; -import * as grpc from "grpc"; import { AsyncIterable } from "@pulumi/query/interfaces"; diff --git a/sdk/nodejs/runtime/resource.ts b/sdk/nodejs/runtime/resource.ts index 84043c96f..e90549768 100644 --- a/sdk/nodejs/runtime/resource.ts +++ b/sdk/nodejs/runtime/resource.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as grpc from "@grpc/grpc-js"; import * as query from "@pulumi/query"; -import * as grpc from "grpc"; import * as log from "../log"; import * as utils from "../utils"; diff --git a/sdk/nodejs/runtime/settings.ts b/sdk/nodejs/runtime/settings.ts index 5484e1bba..145b8f2a3 100644 --- a/sdk/nodejs/runtime/settings.ts +++ b/sdk/nodejs/runtime/settings.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import * as grpc from "@grpc/grpc-js"; import * as fs from "fs"; -import * as grpc from "grpc"; import * as path from "path"; import { RunError } from "../errors"; import * as log from "../log"; diff --git a/sdk/nodejs/tests/deasync.spec.ts b/sdk/nodejs/tests/deasync.spec.ts deleted file mode 100644 index 77209c599..000000000 --- a/sdk/nodejs/tests/deasync.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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. - -// tslint:disable - -import * as assert from "assert"; -import { asyncTest } from "./util"; -import { promiseResult } from "../utils"; - -describe("deasync", () => { - it("handles simple promise", () => { - const actual = 4; - const promise = new Promise((resolve) => { - resolve(actual); - }); - - const result = promiseResult(promise); - assert.equal(result, actual); - }); - - it("handles rejected promise", () => { - const message = "etc"; - const promise = new Promise((resolve, reject) => { - reject(new Error(message)); - }); - - try { - const result = promiseResult(promise); - assert.fail("Should not be able to reach here 1.") - } - catch (err) { - assert.equal(err.message, message); - return; - } - - assert.fail("Should not be able to reach here 2.") - }); - - it("handles pumping", () => { - const actual = 4; - const promise = new Promise((resolve) => { - setTimeout(resolve, 500 /*ms*/, actual); - }); - - const result = promiseResult(promise); - assert.equal(result, actual); - }); -}); \ No newline at end of file diff --git a/sdk/nodejs/tests/runtime/langhost/cases/060.provider_invokes/index.js b/sdk/nodejs/tests/runtime/langhost/cases/060.provider_invokes/index.js index 3181be912..d473370fc 100644 --- a/sdk/nodejs/tests/runtime/langhost/cases/060.provider_invokes/index.js +++ b/sdk/nodejs/tests/runtime/langhost/cases/060.provider_invokes/index.js @@ -20,21 +20,7 @@ let args = { urn: "some-urn", }; -if (semver.lt(process.version, "12.11.0")) { - // These tests hang on runtimes later than 12.10.x due to their use of deasync. - - let result1 = pulumi.runtime.invoke("test:index:echo", args, { provider, async: false }); - for (const key in args) { - assert.deepEqual(result1[key], args[key]); - } - - let result2 = pulumi.runtime.invoke("test:index:echo", args, { provider, async: false }); - result2.then((v) => { - assert.deepEqual(v, args); - }); -} - -let result3 = pulumi.runtime.invoke("test:index:echo", args, { provider }); +let result3 = pulumi.runtime.invoke("test:index:echo", args, { provider, async: true }); result3.then((v) => { assert.deepEqual(v, args); }); diff --git a/sdk/nodejs/tests/runtime/langhost/cases/061.provider_in_parent_invokes/index.js b/sdk/nodejs/tests/runtime/langhost/cases/061.provider_in_parent_invokes/index.js index 03a4bc162..6b3e149b2 100644 --- a/sdk/nodejs/tests/runtime/langhost/cases/061.provider_in_parent_invokes/index.js +++ b/sdk/nodejs/tests/runtime/langhost/cases/061.provider_in_parent_invokes/index.js @@ -27,21 +27,7 @@ let args = { urn: "some-urn", }; -if (semver.lt(process.version, "12.11.0")) { - // These tests hang on runtimes later than 12.10.x due to their use of deasync. - - let result1 = pulumi.runtime.invoke("test:index:echo", args, { parent, async: false }); - for (const key in args) { - assert.deepEqual(result1[key], args[key]); - } - - let result2 = pulumi.runtime.invoke("test:index:echo", args, { parent, async: false }); - result2.then((v) => { - assert.deepEqual(v, args); - }); -} - -let result3 = pulumi.runtime.invoke("test:index:echo", args, { parent }); +let result3 = pulumi.runtime.invoke("test:index:echo", args, { parent, async: true }); result3.then((v) => { assert.deepEqual(v, args); }); diff --git a/sdk/nodejs/tests/runtime/langhost/run.spec.ts b/sdk/nodejs/tests/runtime/langhost/run.spec.ts index 78760bb61..a60ec49af 100644 --- a/sdk/nodejs/tests/runtime/langhost/run.spec.ts +++ b/sdk/nodejs/tests/runtime/langhost/run.spec.ts @@ -19,11 +19,12 @@ import * as path from "path"; import { ID, runtime, URN } from "../../../index"; import { asyncTest } from "../../util"; +import * as grpc from "@grpc/grpc-js"; + const enginerpc = require("../../../proto/engine_grpc_pb.js"); const engineproto = require("../../../proto/engine_pb.js"); const gempty = require("google-protobuf/google/protobuf/empty_pb.js"); const gstruct = require("google-protobuf/google/protobuf/struct_pb.js"); -const grpc = require("grpc"); const langrpc = require("../../../proto/language_grpc_pb.js"); const langproto = require("../../../proto/language_pb.js"); const resrpc = require("../../../proto/resource_grpc_pb.js"); @@ -1154,7 +1155,7 @@ describe("rpc", () => { let rootResource: string | undefined; let regCnt = 0; let logCnt = 0; - const monitor = createMockEngine(opts, + const monitor = await createMockEngineAsync(opts, // Invoke callback (call: any, callback: any) => { const resp = new providerproto.InvokeResponse(); @@ -1331,20 +1332,12 @@ describe("rpc", () => { } // Finally, tear down everything so each test case starts anew. + await new Promise((resolve, reject) => { langHost.proc.kill(); langHost.proc.on("close", () => { resolve(); }); }); - await new Promise((resolve, reject) => { - monitor.server.tryShutdown((err: Error) => { - if (err) { - reject(err); - } - else { - resolve(); - } - }); - }); + monitor.server.forceShutdown(); } })); } @@ -1435,7 +1428,7 @@ function mockRun(langHostClient: any, monitor: string, opts: RunCase, dryrun: bo // Despite the name, the "engine" RPC endpoint is only a logging endpoint. createMockEngine fires up a fake // logging server so tests can assert that certain things get logged. -function createMockEngine( +async function createMockEngineAsync( opts: RunCase, invokeCallback: (call: any, request: any) => any, readResourceCallback: (call: any, request: any) => any, @@ -1444,7 +1437,7 @@ function createMockEngine( logCallback: (call: any, request: any) => any, getRootResourceCallback: (call: any, request: any) => any, setRootResourceCallback: (call: any, request: any) => any, - supportsFeatureCallback: (call: any, request: any) => any): { server: any, addr: string } { + supportsFeatureCallback: (call: any, request: any) => any) { // The resource monitor is hosted in the current process so it can record state, etc. const server = new grpc.Server(); server.addService(resrpc.ResourceMonitorService, { @@ -1456,7 +1449,7 @@ function createMockEngine( registerResourceOutputs: registerResourceOutputsCallback, }); - let engineImpl: Object = { + let engineImpl: grpc.UntypedServiceImplementation = { log: logCallback, }; @@ -1469,8 +1462,19 @@ function createMockEngine( } server.addService(enginerpc.EngineService, engineImpl); - const port = server.bind("0.0.0.0:0", grpc.ServerCredentials.createInsecure()); + + const port = await new Promise((resolve, reject) => { + server.bindAsync("0.0.0.0:0", grpc.ServerCredentials.createInsecure(), (err, p) => { + if (err) { + reject(err); + } else { + resolve(p); + } + }); + }); + server.start(); + return { server: server, addr: `0.0.0.0:${port}` }; } diff --git a/sdk/nodejs/tests/runtime/tsClosureCases.ts b/sdk/nodejs/tests/runtime/tsClosureCases.ts index 3ea84fa97..f755cb5b5 100644 --- a/sdk/nodejs/tests/runtime/tsClosureCases.ts +++ b/sdk/nodejs/tests/runtime/tsClosureCases.ts @@ -5990,22 +5990,22 @@ return function () { typescript.parseCommandLine([""]); }; }); } - { - cases.push({ - title: "Fail to capture non-deployment module due to native code", - func: function () { console.log(pulumi); }, - error: `Error serializing function 'func': tsClosureCases.js(0,0) +// { +// cases.push({ +// title: "Fail to capture non-deployment module due to native code", +// func: function () { console.log(pulumi); }, +// error: `Error serializing function 'func': tsClosureCases.js(0,0) -function 'func':(...) - module './bin/index.js' which indirectly referenced - function 'debug':(...) -(...) -Function code: - function (...)() { [native code] } +// function 'func':(...) +// module './bin/index.js' which indirectly referenced +// function 'debug':(...) +// (...) +// Function code: +// function (...)() { [native code] } -Module './bin/index.js' is a 'deployment only' module. In general these cannot be captured inside a 'run time' function.` - }); - } +// Module './bin/index.js' is a 'deployment only' module. In general these cannot be captured inside a 'run time' function.` +// }); +// } { // Used just to validate that if we capture a Config object we see these values serialized over. diff --git a/sdk/nodejs/tsconfig.json b/sdk/nodejs/tsconfig.json index f82f531da..cf2f3b947 100644 --- a/sdk/nodejs/tsconfig.json +++ b/sdk/nodejs/tsconfig.json @@ -63,7 +63,6 @@ "cmd/run-policy-pack/run.ts", "tests/config.spec.ts", - "tests/deasync.spec.ts", "tests/init.spec.ts", "tests/iterable.spec.ts", "tests/options.spec.ts", diff --git a/sdk/proto/generate.sh b/sdk/proto/generate.sh index 1b0380b8e..7510b6bbb 100755 --- a/sdk/proto/generate.sh +++ b/sdk/proto/generate.sh @@ -55,6 +55,7 @@ $DOCKER_RUN /bin/bash -c 'set -x && JS_PULUMIRPC=/nodejs/proto && \ protoc --js_out=$JS_PROTOFLAGS:$JS_PULUMIRPC --grpc_out=minimum_node_version=6:$JS_PULUMIRPC --plugin=protoc-gen-grpc=/usr/local/bin/grpc_tools_node_protoc_plugin status.proto && \ protoc --js_out=$JS_PROTOFLAGS:$TEMP_DIR --grpc_out=minimum_node_version=6:$TEMP_DIR --plugin=protoc-gen-grpc=/usr/local/bin/grpc_tools_node_protoc_plugin $JS_HACK_PROTOS && \ sed -i "s/^var global = .*;/var proto = { pulumirpc: {} }, global = proto;/" "$TEMP_DIR"/*.js && \ + sed -i "s/^var grpc = require(.*);/var grpc = require('\''@grpc\/grpc-js'\'');/" "$TEMP_DIR"/*.js && \ cp "$TEMP_DIR"/*.js "$JS_PULUMIRPC"' function on_exit() {