Skip to content

Commit

Permalink
Add support to expose functions on a public domain
Browse files Browse the repository at this point in the history
  • Loading branch information
wpjunior committed Mar 16, 2018
1 parent fbd4bd9 commit 41b402a
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 94 deletions.
2 changes: 2 additions & 0 deletions lib/domain/storage/redis.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ class StorageRedis extends Storage {

if (code.exposed === true) {
data.exposed = 'true';
} else if (code.exposed === false) {
data.exposed = 'false';
}

this.setNamespaceMember(namespace, id);
Expand Down
16 changes: 16 additions & 0 deletions lib/http/routers/FunctionsPublicRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const Router = require('express').Router;
const bodyParser = require('body-parser');

const functionRunHandler = require('./functionRunHandler');

const router = new Router();
const { bodyParserLimit } = require('../../support/config');


router.all(
'/:namespace/:id',
bodyParser.json({ limit: bodyParserLimit }),
(req, res) => functionRunHandler(req, res, { exposed: true })
);

module.exports = router;
96 changes: 11 additions & 85 deletions lib/http/routers/FunctionsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ const Router = require('express').Router;
const bodyParser = require('body-parser');
const Validator = require('jsonschema').Validator;

const functionRunHandler = require('./functionRunHandler');
const FunctionsRequest = require('../FunctionsRequest');
const SchemaResponse = require('../SchemaResponse');

const log = require('../../support/log');
const schemas = require('../../domain/schemas');
const Pipeline = require('../../domain/Pipeline');
const ErrorTracker = require('../../domain/ErrorTracker');
const { StdoutLogStorage, DefaultLogStorage } = require('../../domain/LogStorage');
const FunctionsRequest = require('../FunctionsRequest');
const Metric = require('../../domain/Metric');
const SchemaResponse = require('../SchemaResponse');
const { StdoutLogStorage } = require('../../domain/LogStorage');

const router = new Router();
const { bodyParserLimit } = require('../../support/config');
Expand Down Expand Up @@ -140,7 +140,7 @@ router.put('/:namespace/:id', bodyParser.json({ limit: bodyParserLimit }), async
data.env = env;
}

if (exposed) {
if (exposed !== undefined) {
data.exposed = exposed;
}

Expand Down Expand Up @@ -246,85 +246,11 @@ router.delete('/:namespace/:id', async (req, res) => {
});


router.all('/:namespace/:id/run', bodyParser.json({ limit: bodyParserLimit }), async (req, res) => {
const { namespace, id } = req.params;
const memoryStorage = req.app.get('memoryStorage');
const sandbox = req.app.get('sandbox');
const filename = codeFileName(namespace, id);
const metric = new Metric('function-run');
const logStorage = new DefaultLogStorage(namespace, id, req);

let code;

try {
code = await memoryStorage.getCodeByCache(namespace, id, {
preCache: (preCode) => {
preCode.script = sandbox.compileCode(filename, preCode.code);
return preCode;
},
});

if (!code) {
const error = new Error(`Code '${namespace}/${id}' is not found`);
error.statusCode = 404;
throw error;
}
} catch (err) {
res.status(err.statusCode || 500).json({ error: err.message });
return;
}

try {
const options = {
console: logStorage.console,
env: code.env,
};
const result = await sandbox.runScript(code.script, req, options);

res.set(result.headers);
res.status(result.status);
res.json(result.body);

const spent = metric.finish({
filename,
status: result.status,
});

logStorage.flush({
status: result.status,
requestTime: spent,
});
} catch (err) {
logStorage.console.error(`Failed to run function: ${err}`);
logStorage.console.error(err.stack);
const status = err.statusCode || 500;
res.status(status).json({ error: err.message });

const spent = metric.finish({
filename,
status,
error: err.message,
});

const logResult = logStorage.flush({
status,
requestTime: spent,
});

const { namespaceSettings } = code;
const { sentryDSN } = namespaceSettings || {};

const extra = Object.assign({ body: req.body }, logResult || {});
const errTracker = new ErrorTracker({
sentryDSN,
filename,
extra,
tags: { codeHash: code.hash },
code: code.code,
});
errTracker.notify(err);
}
});
router.all(
'/:namespace/:id/run',
bodyParser.json({ limit: bodyParserLimit }),
(req, res) => functionRunHandler(req, res, { exposed: false })
);


router.put('/pipeline', bodyParser.json({ limit: bodyParserLimit }), async (req, res) => {
Expand Down
96 changes: 96 additions & 0 deletions lib/http/routers/functionRunHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const ErrorTracker = require('../../domain/ErrorTracker');
const Metric = require('../../domain/Metric');
const { DefaultLogStorage } = require('../../domain/LogStorage');


function codeFileName(namespace, codeId) {
return `${namespace}/${codeId}.js`;
}


async function functionRunHandler(req, res, { exposed }) {
const { namespace, id } = req.params;
const memoryStorage = req.app.get('memoryStorage');
const sandbox = req.app.get('sandbox');
const filename = codeFileName(namespace, id);
const metric = new Metric('function-run');
const logStorage = new DefaultLogStorage(namespace, id, req);

let code;

try {
code = await memoryStorage.getCodeByCache(namespace, id, {
preCache: (preCode) => {
preCode.script = sandbox.compileCode(filename, preCode.code);
return preCode;
},
});

if (!code) {
const error = new Error(`Code '${namespace}/${id}' is not found`);
error.statusCode = 404;
throw error;
}
if (exposed && !code.exposed) {
const error = new Error('Unauthorized');
error.statusCode = 403;
throw error;
}
} catch (err) {
res.status(err.statusCode || 500).json({ error: err.message });
return;
}

try {
const options = {
console: logStorage.console,
env: code.env,
};
const result = await sandbox.runScript(code.script, req, options);

res.set(result.headers);
res.status(result.status);
res.json(result.body);

const spent = metric.finish({
filename,
status: result.status,
});

logStorage.flush({
status: result.status,
requestTime: spent,
});
} catch (err) {
logStorage.console.error(`Failed to run function: ${err}`);
logStorage.console.error(err.stack);
const status = err.statusCode || 500;
res.status(status).json({ error: err.message });

const spent = metric.finish({
filename,
status,
error: err.message,
});

const logResult = logStorage.flush({
status,
requestTime: spent,
});

const { namespaceSettings } = code;
const { sentryDSN } = namespaceSettings || {};

const extra = Object.assign({ body: req.body }, logResult || {});
const errTracker = new ErrorTracker({
sentryDSN,
filename,
extra,
tags: { codeHash: code.hash },
code: code.code,
});
errTracker.notify(err);
}
}

module.exports = functionRunHandler;
36 changes: 27 additions & 9 deletions lib/http/routes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const express = require('express');
const vhost = require('vhost');
const morgan = require('morgan');

const Sandbox = require('@globocom/backstage-functions-sandbox');
Expand All @@ -10,6 +11,7 @@ const StatusRouter = require('./routers/StatusRouter');
const DebugRouter = require('./routers/DebugRouter');
const NamespacesRouter = require('./routers/NamespacesRouter');
const FunctionsRouter = require('./routers/FunctionsRouter');
const FunctionsPublicRouter = require('./routers/FunctionsPublicRouter');
const RedisStorage = require('../domain/storage/redis');
const parseExposeEnv = require('../support/parseExposeEnv');
const config = require('../support/config');
Expand All @@ -26,19 +28,24 @@ morgan.token('response-sectime', (req, res) => {
return secs.toFixed(3);
});

const app = express();

app.use(morgan(config.log.morganFormat));
app.disable('x-powered-by');
app.enable('trust proxy');

app.set('memoryStorage', new RedisStorage());
app.set('sandbox', new Sandbox({
const memoryStorage = new RedisStorage();
const sandbox = new Sandbox({
env: parseExposeEnv(),
globalModules: config.defaultGlobalModules,
asyncTimeout: config.asyncTimeout,
syncTimeout: config.syncTimeout,
}));
});

function setupApp(app) {
app.use(morgan(config.log.morganFormat));
app.disable('x-powered-by');
app.enable('trust proxy');
app.set('memoryStorage', memoryStorage);
app.set('sandbox', sandbox);
}

const app = express();
setupApp(app);

app.get('/', (req, res) => {
const backstageRequest = new FunctionsRequest(req);
Expand All @@ -49,6 +56,17 @@ app.get('/', (req, res) => {
});
});

if (config.exposed.host) {
const publicApp = express();
setupApp(publicApp);
publicApp.use(FunctionsPublicRouter);
publicApp.use((req, res) => {
res.send({ error: 'Not found' });
});

app.use(vhost(config.exposed.host, publicApp));
}

app.use('/healthcheck', HealthcheckRouter);
app.use('/status', StatusRouter);
app.use('/_debug', DebugRouter);
Expand Down
3 changes: 3 additions & 0 deletions lib/support/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ module.exports = {
useCertFile: ConfigDiscovery.getBool('FUNCTIONS_USE_SSL_CERT_FILE', false),
certFile: process.env.SSL_CERT_FILE,
},
exposed: {
host: process.env.EXPOSED_HOST,
},
};
5 changes: 5 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"request": "^2.81.0",
"stack-trace": "^0.0.9",
"uuid": "^3.0.1",
"vhost": "^3.0.2",
"winston": "^2.2.0"
},
"devDependencies": {
Expand Down

0 comments on commit 41b402a

Please sign in to comment.