pulumi/sdk/nodejs/cmd/dynamic-provider/index.ts

313 lines
12 KiB
TypeScript

// 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.
import * as minimist from "minimist";
import * as path from "path";
import * as dynamic from "../../dynamic";
import * as resource from "../../resource";
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");
const provproto = require("../../proto/provider_pb.js");
const provrpc = require("../../proto/provider_grpc_pb.js");
const plugproto = require("../../proto/plugin_pb.js");
const statusproto = require("../../proto/status_pb.js");
const providerKey: string = "__provider";
function getProvider(props: any): dynamic.ResourceProvider {
// TODO[pulumi/pulumi#414]: investigate replacing requireFromString with eval
return requireFromString(props[providerKey]).handler();
}
// Each of the *RPC functions below implements a single method of the resource provider gRPC interface. The CRUD
// functions--checkRPC, diffRPC, createRPC, updateRPC, and deleteRPC--all operate in a similar fashion:
// 1. Deserialize the dyanmic provider for the resource on which the function is operating
// 2. Call the dynamic provider's corresponding {check,diff,create,update,delete} method
// 3. Convert and return the results
// In all cases, the dynamic provider is available in its serialized form as a property of the resource;
// getProvider` is responsible for handling its deserialization. In the case of diffRPC, if the provider itself
// has changed, `diff` reports that the resource requires replacement and does not delegate to the dynamic provider.
// This allows the creation of the replacement resource to use the new provider while the deletion of the old
// resource uses the provider with which it was created.
function configureRPC(call: any, callback: any): void {
callback(undefined, new emptyproto.Empty());
}
async function invokeRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
// TODO[pulumi/pulumi#406]: implement this.
callback(new Error(`unknown function ${req.getTok()}`), undefined);
}
async function checkRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.CheckResponse();
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
const provider = getProvider(news);
let inputs: any = {};
let failures: any[] = [];
if (provider.check) {
const result = await provider.check(olds, news);
if (result.inputs) {
inputs = result.inputs;
}
if (result.failures) {
failures = result.failures;
}
}
inputs[providerKey] = news[providerKey];
resp.setInputs(structproto.Struct.fromJavaScript(inputs));
if (failures.length !== 0) {
const failureList = [];
for (const f of failures) {
const failure = new provproto.CheckFailure();
failure.setProperty(f.property);
failure.setReason(f.reason);
failureList.push(failure);
}
resp.setFailuresList(failureList);
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function diffRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.DiffResponse();
// If the provider itself has changed, do not delegate to the dynamic provider. Instead, simply report that the
// resource requires replacement. This allows the new resource to be created using the new provider and the old
// resource to be deleted using the old provider.
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
if (olds[providerKey] !== news[providerKey]) {
resp.setReplacesList([ providerKey ]);
} else {
const provider = getProvider(olds);
if (provider.diff) {
const result: any = await provider.diff(req.getId(), olds, news);
if (result.changes === true) {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_SOME);
} else if (result.changes === false) {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_NONE);
} else {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_UNKNOWN);
}
if (result.replaces && result.replaces.length !== 0) {
resp.setReplacesList(result.replaces);
}
if (result.deleteBeforeReplace) {
resp.setDeletebeforereplace(result.deleteBeforeReplace);
}
}
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function createRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.CreateResponse();
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
const result = await provider.create(props);
const resultProps = resultIncludingProvider(result.outs, props);
resp.setId(result.id);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
callback(undefined, resp);
} catch (e) {
return callback(grpcResponseFromError(e));
}
}
async function readRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.ReadResponse();
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
if (provider.read) {
// If there's a read function, consult the provider. Ensure to propagate the special __provider
// value too, so that the provider's CRUD operations continue to function after a refresh.
const result: any = await provider.read(req.getId(), props);
const resultProps = resultIncludingProvider(result.properties, props);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
} else {
// In the event of a missing read, simply return back the input properties.
resp.setProperties(req.getProperties());
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function updateRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.UpdateResponse();
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
if (olds[providerKey] !== news[providerKey]) {
throw new Error("changes to provider should require replacement");
}
let result: any;
const provider = getProvider(olds);
if (provider.update) {
result = await provider.update(req.getId(), olds, news);
}
if (result.outs) {
const resultProps = resultIncludingProvider(result.outs, news);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
}
callback(undefined, resp);
} catch (e) {
return callback(grpcResponseFromError(e));
}
}
async function deleteRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const props: any = req.getProperties().toJavaScript();
const provider: any = await getProvider(props);
if (provider.delete) {
await provider.delete(req.getId(), props);
}
callback(undefined, new emptyproto.Empty());
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function getPluginInfoRPC(call: any, callback: any): Promise<void> {
const resp: any = new plugproto.PluginInfo();
resp.setVersion(version);
callback(undefined, resp);
}
function resultIncludingProvider(result: any, props: any): any {
return Object.assign(result || {}, {
[providerKey]: props[providerKey],
});
}
// grpcResponseFromError creates a gRPC response representing an error from a dynamic provider's
// resource. This is typically either a creation error, in which the API server has (virtually)
// 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 {
// Create response object.
const resp = new statusproto.Status();
resp.setCode(grpc.status.UNKNOWN);
resp.setMessage(e.message);
const metadata = new grpc.Metadata();
if (e.id) {
// Object created successfully, but failed to initialize. Pack initialization failure into
// details.
const detail = new provproto.ErrorResourceInitFailed();
detail.setId(e.id);
detail.setProperties(structproto.Struct.fromJavaScript(e.properties || {}));
detail.addReasons(e.reasons || []);
const details = new anyproto.Any();
details.pack(detail.serializeBinary(), "pulumirpc.ErrorResourceInitFailed");
// Add details to metadata.
resp.addDetails(details);
// NOTE: `grpc-status-details-bin` is a magic field that allows us to send structured
// protobuf data as an error back through gRPC. This notion of details is a first-class in
// the Go gRPC implementation, and the nodejs implementation has not quite caught up to it,
// which is why it's cumbersome here.
metadata.add("grpc-status-details-bin", Buffer.from(resp.serializeBinary()));
}
return {
code: grpc.status.UNKNOWN,
message: e.message,
metadata: metadata,
};
}
export function main(args: string[]): void {
// 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) {
console.error("fatal: Missing <engine> address");
process.exit(-1);
return;
}
const engineAddr: string = args[0];
// Finally connect up the gRPC client/server and listen for incoming requests.
const server = new grpc.Server();
server.addService(provrpc.ResourceProviderService, {
configure: configureRPC,
invoke: invokeRPC,
check: checkRPC,
diff: diffRPC,
create: createRPC,
read: readRPC,
update: updateRPC,
delete: deleteRPC,
getPluginInfo: getPluginInfoRPC,
});
const port: number = server.bind(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure());
server.start();
// 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.slice(2));