cloudworker-proxy
Version:
An api gateway for cloudflare workers
385 lines (341 loc) • 11.1 kB
JavaScript
;
/*
* SERVERLESS PLATFORM SDK: INSTANCE
*/
const path = require('path');
const exec = require('child_process').exec;
const utils = require('./utils');
const api = require('./api');
/**
* Correctly formats an instanceId
* @param {*} orgUid
* @param {*} stageName
* @param {*} appUid
* @param {*} instanceName
*/
const generateId = (orgUid, stageName, appUid, instanceName) => {
// Validate
if (!orgUid || !stageName || !appUid || !instanceName) {
throw new Error("'orgUid' 'stageName' 'appUid' and 'instanceName' are required");
}
return `${orgUid}.${stageName}.${appUid}.${instanceName}`;
};
/**
* Create a new Instance
* @param {*} orgName
* @param {*} stageName
* @param {*} appName
* @param {*} instanceName
*/
const create = (orgName = null, stageName = null, appName = null, instanceName = null) => {
// Validate
if (!orgName || !stageName || !appName || !instanceName) {
throw new Error("'orgName' 'stageName' 'appName' and 'instanceName' are required");
}
// Instance
const instance = {};
instance.orgName = orgName;
instance.appName = appName;
instance.stageName = stageName;
instance.instanceName = instanceName;
instance.componentName = null;
instance.componentVersion = null;
instance.inputs = {};
instance.outputs = {};
instance.state = {};
instance.description = null;
// Status
instance.instanceStatus = 'inactive';
instance.deploymentError = null;
instance.deploymentErrorStack = null;
instance.lastAction = null;
// Dates
instance.createdAt = Date.now();
instance.updatedAt = Date.now();
instance.lastDeployedAt = null;
instance.lastActionAt = null;
// Metrics
instance.instanceMetrics = {};
instance.instanceMetrics.actions = 0;
instance.instanceMetrics.deployments = 0;
instance.instanceMetrics.removes = 0;
instance.instanceMetrics.errors = 0;
return instance;
};
/**
* Validates and (re)formats the component instance properties
*/
const validateAndFormat = (rawInstance) => {
// Copy input object, otherwise the inputter will have unintended data modifications
const instance = Object.assign({}, rawInstance);
// Format Helper - If shortened properties are used, replace them with full properties
if (instance.org) {
instance.orgName = instance.org;
delete instance.org;
}
if (instance.stage) {
instance.stageName = instance.stage;
delete instance.stage;
}
if (instance.app) {
instance.appName = instance.app;
delete instance.app;
}
if (instance.name) {
instance.instanceName = instance.name;
delete instance.name;
}
// Ensure all required properties exist
if (!instance.orgName || !instance.stageName || !instance.appName || !instance.instanceName) {
throw new Error("'orgName' 'stageName' 'appName' and 'instanceName' are required");
}
// Format - If shortened component syntax is used, expand into full syntax
if (instance.component) {
instance.componentName = instance.component.includes('@')
? instance.component.split('@')[0]
: instance.component;
instance.componentVersion = instance.component.includes('@')
? instance.component.split('@')[1]
: '';
delete instance.component;
}
// Ensure all required component properties exist
if (!instance.componentName) {
throw new Error("'componentName' is required");
}
// Ensure an inputs object exists
if (!instance.inputs) {
instance.inputs = {};
}
return instance;
};
/**
* Create or update an Instance
*/
const save = async (sdk, instance = {}) => {
// Validate
instance = validateAndFormat(instance);
// Send request
return api.instance.save(sdk, instance);
};
/**
* Get an Instance record by name
*/
const getByName = async (
sdk,
orgName = null,
stageName = null,
appName = null,
instanceName = null,
options = null
) => {
return api.instance.get(sdk, orgName, appName, stageName, instanceName, options);
};
/**
* List Instance records by Org
*/
const listByOrgName = async (sdk, orgName = null, orgUid = null) => {
return api.instance.list(sdk, orgUid, orgName);
};
/*
* Run a "src" hook, if one is specified
*/
const preRunSrcHook = async (src) => {
if (typeof src === 'object' && src.hook && src.dist) {
// First run the build hook, if "hook" and "dist" are specified
const options = { cwd: src.src };
return new Promise((resolve, reject) => {
exec(src.hook, options, (err) => {
if (err) {
return reject(
new Error(`Failed running "src.hook": "${src.hook}" due to the following error: ${err}`)
);
}
return resolve(path.resolve(process.cwd(), src.dist));
});
});
} else if (typeof src === 'object' && src.src) {
src = path.resolve(src.src);
} else if (typeof src === 'string') {
src = path.resolve(src);
}
return src;
};
/*
* Uploads the component instance src input reference if available
*/
const preRunSrcUpload = async (sdk, orgName, inputs, options) => {
const { srcOriginal } = inputs;
let { src } = inputs;
let exclude = [];
let include = [];
if (typeof srcOriginal === 'object') {
exclude = srcOriginal.exclude || [];
include = srcOriginal.include || [];
options.targetDir = srcOriginal.targetDir;
}
const packageUrls = await api.instance.getUploadUrls(sdk, orgName);
const uploadPromise = utils.zipAndUpload(
src,
null,
exclude,
include,
packageUrls.upload,
options
);
// replace the src input to point to the download url so that it's download in the lambda runtime
src = packageUrls.download;
return { src, uploadPromise, cosInfo: packageUrls.cosInfo };
};
/**
* Run a method. "inputs" override serverless.yml inputs and should only be provided when using custom methods.
*/
const run = async (sdk, method = {}, instanceData = {}, credentials = {}, options = {}) => {
let instance = JSON.parse(JSON.stringify(instanceData));
// Validate method
if (!method) {
throw new Error('A "method" argument is required');
}
// Validate instance
instance = validateAndFormat(instance);
// Run source hook and upload source, if "src" input is used...
// Save original src to present neatly in Dashboard/clients, or this will be replaced by the S3 location by the functions below
if (instance.inputs.src) {
instance.inputs.srcOriginal = JSON.parse(JSON.stringify(instance.inputs.src));
}
instance.inputs.src = await preRunSrcHook(instance.inputs.src);
let size;
let uploadTask;
if (instance.inputs.src && typeof instance.inputs.src === 'string') {
// Process source code, or skip if not provided...
const { src: source, uploadPromise: task, cosInfo: sourceCosInfo } = await preRunSrcUpload(
sdk,
instance.orgName,
instance.inputs,
options
);
if (options.debug) {
console.debug(`the code will be uploaded to ${source}`);
}
instance.inputs.src = source;
instance.inputs.sourceCosInfo = sourceCosInfo;
uploadTask = task;
}
if (uploadTask) {
await uploadTask;
await api.instance.run(sdk, instance, method, credentials, options, size);
} else {
return api.instance.run(sdk, instance, method, credentials, options, size);
}
return null;
};
/**
* Finish a run
*/
const runFinish = async (sdk, method = null, instance = {}) => {
return api.instance.runFinish(sdk, instance, method);
};
/**
* Run a deployment
* @param {object} instance The Instance definition.
* @param {object} credentials The credentials of the cloud providers required by the Instance.
* @param {object} options Any options you wish to supply the Instance method.
* @param {number} size The size in bytes of the source code, so that the Instance method can validate it beforehand.
*/
const deploy = async (sdk, instance, credentials, options) => {
// Set defaults
options.timeout = options.timeout || 600000; // 600000 = 10 minutes
options.interval = options.interval || 500;
// Set timer
const startedAt = Date.now();
// Perform Run
await run(sdk, 'deploy', instance, credentials, options);
if (options.dev && utils.isChinaUser()) {
// in tencent case, we'll setup polling in component CLI after each deployment
return null;
}
// Set up polling
// Poll function calls the instance constantly to check its status
const poll = async () => {
// Check if timed out
if (Date.now() - options.timeout > startedAt) {
throw new Error(`Request timed out after ${options.timeout / 60000} minutes`);
}
// Fetch instance
let instanceRecord = await getByName(
sdk,
instance.orgName || instance.org,
instance.stageName || instance.stage,
instance.appName || instance.app,
instance.instanceName || instance.name
);
instanceRecord = instanceRecord.instance;
// Sleep before calling again
if (instanceRecord.instanceStatus === 'active') {
return instanceRecord;
} else if (instanceRecord.instanceStatus === 'error') {
const error = new Error(instanceRecord.deploymentError);
error.stack = instanceRecord.deploymentErrorStack;
throw error;
}
await utils.sleep(options.interval);
return poll();
};
return poll();
};
/**
* Run a removal
* @param {object} instance The Instance definition.
* @param {object} credentials The credentials of the cloud providers required by the Instance.
* @param {object} options Any options you wish to supply the Instance method.
* @param {number} size The size in bytes of the source code, so that the Instance method can validate it beforehand.
*/
const remove = async (sdk, instance, credentials, options) => {
// Set defaults
options.timeout = options.timeout || 600000; // 600000 = 10 minutes
options.interval = options.interval || 500;
// Set timer
const startedAt = Date.now();
// Perform Run
await run(sdk, 'remove', instance, credentials, options);
// Set up polling
// Poll function calls the instance constantly to check its status
const poll = async () => {
// Check if timed out
if (Date.now() - options.timeout > startedAt) {
throw new Error(`Request timed out after ${options.timeout / 1000} seconds`);
}
// Fetch instance
let instanceRecord = await getByName(
sdk,
instance.orgName || instance.org,
instance.stageName || instance.stage,
instance.appName || instance.app,
instance.instanceName || instance.name
);
instanceRecord = instanceRecord.instance;
// Sleep before calling again
if (instanceRecord.instanceStatus === 'inactive') {
return instanceRecord;
} else if (instanceRecord.instanceStatus === 'error') {
const error = new Error(instanceRecord.deploymentError);
error.stack = instanceRecord.deploymentErrorStack;
throw error;
}
await utils.sleep(options.interval);
return poll();
};
return poll();
};
module.exports = {
generateId,
validateAndFormat,
create,
save,
getByName,
listByOrgName,
run,
runFinish,
deploy,
remove,
};