2017-10-14 02:48:54 +02:00
|
|
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
|
|
|
|
import * as minimist from "minimist";
|
|
|
|
import * as path from "path";
|
|
|
|
|
|
|
|
import * as dynamic from "../../dynamic";
|
|
|
|
import * as resource from "../../resource";
|
2017-12-01 22:50:32 +01:00
|
|
|
import { version } from "../../version";
|
2017-10-14 02:48:54 +02:00
|
|
|
|
|
|
|
const requireFromString = require("require-from-string");
|
|
|
|
const grpc = require("grpc");
|
|
|
|
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");
|
2017-12-01 22:50:32 +01:00
|
|
|
const plugproto = require("../../proto/plugin_pb.js");
|
2017-10-14 02:48:54 +02:00
|
|
|
|
2017-10-14 23:29:49 +02:00
|
|
|
const providerKey: string = "__provider";
|
2017-10-14 02:48:54 +02:00
|
|
|
|
2017-10-14 23:29:49 +02:00
|
|
|
function getProvider(props: any): dynamic.ResourceProvider {
|
|
|
|
// TODO[pulumi/pulumi#414]: investigate replacing requireFromString with eval
|
|
|
|
return requireFromString(props[providerKey]).handler();
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
|
|
|
|
2017-10-17 07:58:34 +02:00
|
|
|
// 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:
|
2017-10-17 07:51:26 +02:00
|
|
|
// 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
|
2017-10-17 07:58:34 +02:00
|
|
|
// 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.
|
2017-10-17 07:51:26 +02:00
|
|
|
|
2017-10-14 02:48:54 +02:00
|
|
|
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();
|
|
|
|
|
2017-12-03 01:34:16 +01:00
|
|
|
const olds = req.getOlds().toJavaScript();
|
|
|
|
const news = req.getNews().toJavaScript();
|
|
|
|
const provider = getProvider(news);
|
|
|
|
|
2017-12-06 04:14:28 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-03 01:34:16 +01:00
|
|
|
inputs[providerKey] = news[providerKey];
|
|
|
|
resp.setInputs(structproto.Struct.fromJavaScript(inputs));
|
2017-10-14 02:48:54 +02:00
|
|
|
|
2017-12-06 04:14:28 +01:00
|
|
|
if (failures.length !== 0) {
|
|
|
|
const failureList = [];
|
|
|
|
for (const f of failures) {
|
2017-10-14 02:48:54 +02:00
|
|
|
const failure = new provproto.CheckFailure();
|
|
|
|
failure.setProperty(f.property);
|
|
|
|
failure.setReason(f.reason);
|
2017-12-06 04:14:28 +01:00
|
|
|
failureList.push(failure);
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
2017-12-06 04:14:28 +01:00
|
|
|
resp.setFailuresList(failureList);
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
callback(undefined, resp);
|
|
|
|
} catch (e) {
|
2017-10-17 07:51:26 +02:00
|
|
|
console.error(`${e}: ${e.stack}`);
|
2017-10-14 02:48:54 +02:00
|
|
|
callback(e, undefined);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function diffRPC(call: any, callback: any): Promise<void> {
|
|
|
|
try {
|
|
|
|
const req: any = call.request;
|
|
|
|
const resp = new provproto.DiffResponse();
|
|
|
|
|
2017-10-17 07:58:34 +02:00
|
|
|
// 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.
|
2017-10-14 02:48:54 +02:00
|
|
|
const olds = req.getOlds().toJavaScript();
|
|
|
|
const news = req.getNews().toJavaScript();
|
2017-10-14 23:29:49 +02:00
|
|
|
if (olds[providerKey] !== news[providerKey]) {
|
|
|
|
resp.setReplacesList([ providerKey ]);
|
2017-10-14 02:48:54 +02:00
|
|
|
} else {
|
2017-10-14 23:29:49 +02:00
|
|
|
const provider = getProvider(olds);
|
2017-12-06 04:14:28 +01:00
|
|
|
if (provider.diff) {
|
|
|
|
const result: any = await provider.diff(req.getId(), olds, news);
|
|
|
|
if (result.replaces && result.replaces.length !== 0) {
|
|
|
|
resp.setReplacesList(result.replaces);
|
|
|
|
}
|
2017-12-11 18:22:04 +01:00
|
|
|
if (result.deleteBeforeReplace) {
|
|
|
|
resp.setDeleteBeforeReplace(result.deleteBeforeReplace);
|
|
|
|
}
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(undefined, resp);
|
|
|
|
} catch (e) {
|
2017-10-17 07:51:26 +02:00
|
|
|
console.error(`${e}: ${e.stack}`);
|
2017-10-14 02:48:54 +02:00
|
|
|
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();
|
2017-10-14 23:29:49 +02:00
|
|
|
const provider = getProvider(props);
|
2017-10-14 02:48:54 +02:00
|
|
|
|
2017-10-14 23:29:49 +02:00
|
|
|
const result = await provider.create(props);
|
2017-10-14 02:48:54 +02:00
|
|
|
resp.setId(result.id);
|
|
|
|
if (result.outs) {
|
|
|
|
resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(undefined, resp);
|
|
|
|
} catch (e) {
|
2017-10-17 07:51:26 +02:00
|
|
|
console.error(`${e}: ${e.stack}`);
|
2017-10-14 02:48:54 +02:00
|
|
|
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();
|
2017-10-14 23:29:49 +02:00
|
|
|
if (olds[providerKey] !== news[providerKey]) {
|
|
|
|
throw new Error("changes to provider should require replacement");
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
|
|
|
|
2017-12-06 04:14:28 +01:00
|
|
|
const provider = getProvider(olds);
|
|
|
|
if (provider.update) {
|
|
|
|
const result: any = await provider.update(req.getId(), olds, news);
|
|
|
|
if (result.outs) {
|
|
|
|
resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
|
|
|
|
}
|
2017-10-14 02:48:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
callback(undefined, resp);
|
|
|
|
} catch (e) {
|
2017-10-17 07:51:26 +02:00
|
|
|
console.error(`${e}: ${e.stack}`);
|
2017-10-14 02:48:54 +02:00
|
|
|
callback(e, undefined);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteRPC(call: any, callback: any): Promise<void> {
|
|
|
|
try {
|
|
|
|
const req: any = call.request;
|
|
|
|
const props: any = req.getProperties().toJavaScript();
|
2017-12-06 04:14:28 +01:00
|
|
|
const provider: any = await getProvider(props);
|
|
|
|
if (provider.delete) {
|
2017-12-29 02:47:10 +01:00
|
|
|
await provider.delete(req.getId(), props);
|
2017-12-06 04:14:28 +01:00
|
|
|
}
|
2017-10-14 02:48:54 +02:00
|
|
|
callback(undefined, new emptyproto.Empty());
|
|
|
|
} catch (e) {
|
2017-10-17 07:51:26 +02:00
|
|
|
console.error(`${e}: ${e.stack}`);
|
2017-10-14 02:48:54 +02:00
|
|
|
callback(e, undefined);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-01 22:50:32 +01:00
|
|
|
async function getPluginInfoRPC(call: any, callback: any): Promise<void> {
|
|
|
|
const resp: any = new plugproto.PluginInfo();
|
|
|
|
resp.setVersion(version);
|
|
|
|
callback(undefined, resp);
|
|
|
|
}
|
|
|
|
|
2017-10-14 02:48:54 +02:00
|
|
|
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,
|
|
|
|
update: updateRPC,
|
|
|
|
delete: deleteRPC,
|
2017-12-01 22:50:32 +01:00
|
|
|
getPluginInfo: getPluginInfoRPC,
|
2017-10-14 02:48:54 +02:00
|
|
|
});
|
|
|
|
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));
|