Skip to content
This repository has been archived by the owner on Jan 7, 2022. It is now read-only.

Commit

Permalink
fist version
Browse files Browse the repository at this point in the history
  • Loading branch information
tjanczuk committed Jun 4, 2020
1 parent ba4524f commit 5dff1bb
Show file tree
Hide file tree
Showing 3 changed files with 418 additions and 0 deletions.
252 changes: 252 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
const Superagent = require('superagent');
const Url = require('url');

function debug() {
if (process.env.debug) {
console.log.apply(console, arguments);
}
};

function validateReturnTo(ctx) {
if (ctx.query.returnTo) {
const validReturnTo = (ctx.configuration.fusebit_allowed_return_to || '').split(',');
const match = validReturnTo.find(allowed => {
if (allowed === ctx.query.returnTo) {
return true;
}
if (allowed[allowed.length - 1] === '*' && ctx.query.returnTo.indexOf(allowed.substring(0, allowed.length - 1)) === 0) {
return true;
}
return false;
});
if (!match) {
throw {
status: 403,
message: `The specified 'returnTo' URL '${ctx.query.returnTo}' does not match any of the allowed returnTo URLs of the '${
ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component. If this is a valid request, add the specified 'returnTo' URL to the 'fusebit_allowed_return_to' configuration property of the '${
ctx.boundaryId}/${ctx.functionId}' Fusebit Add-On component.`
};
}
}
}

exports.debug = debug;

exports.createSettingsManager = (configure, disableDebug) => {
const { states, initialState } = configure;
return async (ctx) => {
if (!disableDebug) {
debug('DEBUGGING ENABLED. To disable debugging information, comment out the `debug` configuration setting.');
debug('NEW REQUEST', ctx.method, ctx.url, ctx.query, ctx.body);
}
try {
// Configuration request
validateReturnTo(ctx);
let [state, data] = exports.getInputs(ctx, initialState || 'none');
debug('STATE', state);
debug('DATA', data);
if (ctx.query.status === 'error') {
// This is a callback from a subordinate service that resulted in an error; propagate
throw { status: data.status || 500, message: data.message || 'Unspecified error', state };
}
let stateHandler = states[state.configurationState];
if (stateHandler) {
return await stateHandler(ctx, state, data);
}
else {
throw { status: 400, message: `Unsupported configuration state '${state.configurationState}'`, state };
}
}
catch (e) {
return exports.completeWithError(ctx, e);
}
}
};

exports.createLifecycleManager = (options) => {
const { configure, install, uninstall } = options;
return async (ctx) => {
debug('DEBUGGING ENABLED. To disable debugging information, comment out the `debug` configuration setting.');
debug('NEW REQUEST', ctx.method, ctx.url, ctx.query, ctx.body);
const pathSegments = Url.parse(ctx.url).pathname.split('/');
let lastSegment;
do {
lastSegment = pathSegments.pop();
} while (!lastSegment && pathSegments.length > 0);
try {
switch (lastSegment) {
case 'configure': // configuration
if (configure) {
// There is a configuration stage, process the next step in the configuration
validateReturnTo(ctx);
const settingsManager = exports.createSettingsManager(configure, true);
return await settingsManager(ctx);
}
else {
// There is no configuration stage, simply redirect back to the caller with success
validateReturnTo(ctx);
let [state, data] = exports.getInputs(ctx, configure && configure.initialState || 'none');
return exports.completeWithSuccess(state, data);
}
break;
case 'install': // installation
if (!install) {
throw { status: 404, message: 'Not found' };
}
return await install(ctx);
case 'uninstall': // uninstallation
if (!uninstall) {
throw { status: 404, message: 'Not found' };
}
return await uninstall(ctx);
default:
throw { status: 404, message: 'Not found' };
};
}
catch (e) {
return exports.completeWithError(ctx, e);
}
};
}

exports.serializeState = (state) => Buffer.from(JSON.stringify(state)).toString('base64');

exports.deserializeState = (state) => JSON.parse(Buffer.from(state, 'base64').toString());

exports.getInputs = (ctx, initialConfigurationState) => {
let data;
try {
data = ctx.query.data ? exports.deserializeState(ctx.query.data) : {};
}
catch (e) {
throw { status: 400, message: `Malformed 'data' parameter` };
}
if (ctx.query.returnTo) {
// Initialization of the add-on component interaction
if (!initialConfigurationState) {
throw { status: 400, message: `State consistency error. Initial configuration state is not specified, and 'state' parameter is missing.` };
}
['baseUrl', 'accountId', 'subscriptionId', 'boundaryId','functionId', 'templateName'].forEach(p => {
if (!data[p]) {
throw { status: 400, message: `Missing 'data.${p}' input parameter`, state: ctx.query.state };
}
});
return [{ configurationState: initialConfigurationState, returnTo: ctx.query.returnTo, returnToState: ctx.query.state}, data];
}
else if (ctx.query.state) {
// Continuation of the add-on component interaction (e.g. form post from a settings manager)
try {
return [JSON.parse(Buffer.from(ctx.query.state, 'base64').toString()), data];
}
catch (e) {
throw { status: 400, message: `Malformed 'state' parameter` };
}
} else {
throw { status: 400, message: `Either the 'returnTo' or 'state' parameter must be present.` };
}
}

exports.completeWithSuccess = (state, data) => {
const location = `${state.returnTo}?status=success&data=${
encodeURIComponent(exports.serializeState(data))
}` + (state.returnToState ? `&state=${encodeURIComponent(state.returnToState)}` : '');
return { status: 302, headers: { location }};
};

exports.completeWithError = (ctx, error) => {
debug('COMPLETE WITH ERROR', error);
let returnTo = error.state && error.state.returnTo || ctx.query.returnTo;
let state = error.state && error.state.returnToState || (ctx.query.returnTo && ctx.query.state);
let body = { status: error.status || 500, message: error.message };
if (returnTo) {
const location = `${returnTo}?status=error&data=${
encodeURIComponent(exports.serializeState(body))
}` + (state ? `&state=${encodeURIComponent(state)}` : '');
return { status: 302, headers: { location }};
}
else {
return { status: body.status, body };
}
};

exports.getSelfUrl = (ctx) => {
const baseUrl = ctx.headers['x-forwarded-proto']
? `${ctx.headers['x-forwarded-proto'].split(',')[0]}://${ctx.headers.host}`
: `${ctx.protocol}://${ctx.headers.host}`;
return `${baseUrl}/v1/run/${ctx.subscriptionId}/${ctx.boundaryId}/${ctx.functionId}`;
};

exports.redirect = (ctx, state, data, redirectUrl, nextConfigurationState) => {

state.configurationState = nextConfigurationState;

const location = `${
redirectUrl
}?returnTo=${
`${exports.getSelfUrl(ctx)}/configure`
}&state=${
encodeURIComponent(exports.serializeState(state))
}&data=${
encodeURIComponent(exports.serializeState(data))
}`;

return { status: 302, headers: { location } };
}

exports.createFunction = async (ctx, functionSpecification) => {
let functionCreated = false;
try {
let url = `${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${
ctx.body.subscriptionId
}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}`;
let response = await Superagent.put(url)
.set("Authorization", ctx.headers['authorization']) // pass-through authorization
.send(functionSpecification);
functionCreated = true;
let attempts = 15;
while (response.status === 201 && attempts > 0) {
response = await Superagent.get(
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${
ctx.body.subscriptionId
}/boundary/${ctx.body.boundaryId}/function/${ctx.body.functionId}/build/${response.body.buildId}`
).set("Authorization", ctx.headers['authorization']);
if (response.status === 200) {
if (response.body.status === "success") {
return;
} else {
throw new Error(
`Failure creating function: ${(response.body.error &&
response.body.error.message) ||
"Unknown error"}`
);
}
}
await new Promise(resolve => setTimeout(resolve, 2000));
attempts--;
}
if (attempts === 0) {
throw new Error(`Timeout creating function`);
}
if (response.status === 204 || (response.body && response.body.status === "success")) {
return;
} else {
throw response.body;
}
}
catch (e) {
if (functionCreated) {
try {
await exports.deleteFunction(ctx);
} catch (_) {}
}
throw e;
}
};

exports.deleteFunction = async (ctx, boundaryId, functionId) => {
await Superagent.delete(
`${ctx.body.baseUrl}/v1/account/${ctx.body.accountId}/subscription/${
ctx.body.subscriptionId
}/boundary/${boundaryId || ctx.body.boundaryId}/function/${functionId || ctx.body.functionId}`
).set("Authorization", ctx.headers['authorization']); // pass-through authorization
};
Loading

0 comments on commit 5dff1bb

Please sign in to comment.