Harden error paths and improve messages

This commit is contained in:
joeduffy 2017-09-06 09:36:28 -07:00
parent f0389799d8
commit d8d94d1df0

View file

@ -2,7 +2,7 @@
import * as asset from "../asset";
import { Computed, MaybeComputed } from "../computed";
import { Resource, URN } from "../resource";
import { ID, Resource, URN } from "../resource";
import { Log } from "./log";
import { isInsideMapValueCallback, Property } from "./property";
import { getMonitor, isDryRun } from "./settings";
@ -15,7 +15,7 @@ let gstruct = require("google-protobuf/google/protobuf/struct_pb.js");
// objects that the registration operation will resolve at the right time (or remain unresolved for deployments).
export function registerResource(
res: Resource, t: string, name: string, props: {[key: string]: MaybeComputed<any> | undefined}): void {
Log.debug(`Registering resource: t=${t}, name=${name}, props=${props}`);
Log.debug(`Registering resource: t=${t}, name=${name}, props=${JSON.stringify(Object.keys(props))}`);
if (isInsideMapValueCallback()) {
throw new Error(
`Illegal attempt to create a conditional resource '${name}' (type ${t}) inside a mapValue callback`);
@ -27,54 +27,60 @@ export function registerResource(
return;
}
// Create a resource URN and an ID that will get populated after deployment.
let urn = new Property<URN>();
let id = new Property<string>();
// Store these properties, plus all of those passed in, on the resource object. Note that we do these using
// Store a URN and ID property, plus any passed in, on the resource object. Note that we do these using
// any casts because they are typically readonly and this function is in cahoots with the initialization process.
let transfer: Promise<any> = transferProperties(res, props);
(<any>res).urn = urn;
(<any>res).id = id;
let urn = (<any>res).urn = new Property<URN>();
let id = (<any>res).id = new Property<ID>();
let transfer: Promise<any> = transferProperties(res, t, name, props);
// During a real deployment, the transfer operation may take some time to settle (we may need to wait on
// other in-flight operations. As a result, we can't launch the RPC request until they are done. At the same
// time, we want to give the illusion of non-blocking code, so we return immediately.
transfer.then((obj: any) => {
Log.debug(`Resource RPC prepared: t=${t}, name=${name}, obj=${obj}`);
transfer.then(
(obj: any) => {
Log.debug(`Resource RPC prepared: t=${t}, name=${name}, obj=${JSON.stringify(obj)}`);
// Fire off an RPC to the monitor to register the resource. If/when it resolves, we will blit the properties.
let req = new langproto.NewResourceRequest();
req.setType(t);
req.setName(name);
req.setObject(obj);
monitor.newResource(req, (err: Error, resp: any) => {
Log.debug(`Resource RPC finished: t=${t}, name=${name}; err: ${err}, resp: ${resp}`);
if (err) {
throw new Error(`Failed to register new resource with monitor: ${err}`);
}
else {
// The resolution will always have a valid URN, even during planning, and it is final (doesn't change).
urn.setOutput(resp.getUrn(), true, false);
// If an ID is present, then it's safe to say it's final, because the resource planner wouldn't hand
// it back to us otherwise (e.g., if the resource was being replaced, it would be missing).
let idOutput: string | undefined = resp.getId();
if (idOutput) {
id.setOutput(idOutput, true, false);
// Fire off an RPC to the monitor to register the resource. If/when it resolves, we will blit the properties.
let req = new langproto.NewResourceRequest();
req.setType(t);
req.setName(name);
req.setObject(obj);
monitor.newResource(req, (err: Error, resp: any) => {
Log.debug(`Resource RPC finished: t=${t}, name=${name}; err: ${err}, resp: ${resp}`);
if (err) {
Log.error(`Failed to register new resource ${name}[${t}]: ${err}`);
}
else {
// The resolution will always have a valid URN, even during planning, and it is final (doesn't change).
urn.setOutput(resp.getUrn(), true, false);
// Finally propagate any other properties that were given to us as outputs.
resolveProperties(res, resp.getObject(), resp.getStable());
}
});
});
// If an ID is present, then it's safe to say it's final, because the resource planner wouldn't hand
// it back to us otherwise (e.g., if the resource was being replaced, it would be missing).
let idOutput: string | undefined = resp.getId();
if (idOutput) {
id.setOutput(idOutput, true, false);
}
// Finally propagate any other properties that were given to us as outputs.
try {
resolveProperties(res, t, name, resp.getObject(), resp.getStable());
}
catch (err) {
Log.error(`Failed to propagate resource provider properties to ${name}[${t}]: ${err}`);
}
}
});
},
(err: Error) => {
Log.error(`An unhandled error occurred during resource ${name}[${t}] creation: ${err}`);
},
);
}
// transferProperties stores the properties on the resource object and returns a gRPC serializable
// proto.google.protobuf.Struct out of a resource's properties.
function transferProperties(
res: Resource, props: {[key: string]: MaybeComputed<any> | undefined}): Promise<any> {
res: Resource, t: string, name: string, props: {[key: string]: MaybeComputed<any> | undefined}): Promise<any> {
let resbag: any = res;
let obj: any = {}; // this will eventually hold the serialized object properties.
let eventuals: Promise<void>[] = []; // this contains all promises outstanding for assignments.
@ -87,7 +93,7 @@ function transferProperties(
// Create a property to wrap the value and store it on the resource.
if (resbag[k]) {
throw new Error(`Property '${k}' is already initialized on this resource object`);
throw new Error(`Property '${k}' is already initialized on resource ${name}[${t}]`);
}
let p = resbag[k] = new Property<any>(props[k]);
@ -103,20 +109,12 @@ function transferProperties(
// Now return a promise that resolves when all assignments above have settled. Note that we do not
// use await here, because we don't actually want to block the above assignments of properties.
return Promise.all(eventuals).then(() => {
try {
return gstruct.Struct.fromJavaScript(obj);
}
catch (err) {
Log.debug(`Failed to marshal gstruct from JSON: ${JSON.stringify(obj)}`);
throw err;
}
});
return Promise.all(eventuals).then(() => gstruct.Struct.fromJavaScript(obj));
}
// resolveProperties takes as input a gRPC serialized proto.google.protobuf.Struct and resolves all of the
// resource's matching properties to the values inside.
function resolveProperties(res: Resource, propsStruct: any, stable: boolean): void {
function resolveProperties(res: Resource, t: string, name: string, propsStruct: any, stable: boolean): void {
// First set any properties present in the output object.
if (propsStruct) {
let resany: any = <any>res;
@ -136,14 +134,16 @@ function resolveProperties(res: Resource, propsStruct: any, stable: boolean): vo
resany[k] = p = new Property<any>();
}
else if (!(p instanceof Property)) {
throw new Error(`Unable to set resource property '${k}' because it is not a Property<T>`);
throw new Error(
`Unable to set property '${k}' on resource ${name}[${t}] because it is not a Property<T>`);
}
try {
(p as Property<any>).setOutput(
deserializeProperty(props[k]), !isDryRun() || stable, false);
}
catch (err) {
throw new Error(`Unable to set resource property '${k}'; error: ${err}`);
throw new Error(
`Unable to set property '${k}' on resource ${name}[${t}; error: ${err}`);
}
}
}
@ -172,7 +172,7 @@ export const specialArchiveSig = "0def7320c3a5731c473e5ecbe6d01bc7";
async function serializeProperty(prop: any): Promise<any> {
if (prop === undefined) {
if (!isDryRun()) {
Log.debug("Unexpected unknown property during deployment");
throw new Error("Unexpected unknown property during deployment");
}
return unknownPropertyValue;
}