Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4c9bcd8
Add runnable-api-client as dependency
cflynn07 Nov 26, 2015
9e50eb7
Add npm dependencies and logic for super-user client
cflynn07 Nov 26, 2015
74837e6
Add logging utility to project
cflynn07 Nov 26, 2015
2bc920d
Add npm dependencies for testing
cflynn07 Nov 26, 2015
af96a83
Seed test files and package script
cflynn07 Nov 26, 2015
f2555a7
Add sinon to project dependencies
cflynn07 Nov 26, 2015
5e5c9c1
Add super user login precondition to server listen
cflynn07 Nov 26, 2015
e8c3e28
Fetch instance from API middleware
cflynn07 Nov 26, 2015
ab191aa
Log module return child log instance
cflynn07 Nov 26, 2015
db767af
Add trace log
cflynn07 Nov 26, 2015
ab64255
Add start check for required ENV keys
cflynn07 Nov 26, 2015
b140690
Add error page generation logic for instance status
cflynn07 Nov 26, 2015
c815c1d
Fix jshint error
cflynn07 Nov 26, 2015
5e78b79
Add error-cat dep to project and use with invalid request params
cflynn07 Nov 30, 2015
0b64376
Invoke next with error if instance not found
cflynn07 Nov 30, 2015
8b78d96
Use expressjs view engine local vars
cflynn07 Nov 30, 2015
2d42b54
Use assign to extend app.locals
cflynn07 Nov 30, 2015
8972fe4
Reorganize functions into private module properties for unit tests
cflynn07 Nov 30, 2015
25b8f10
Fix missing invoke of next
cflynn07 Nov 30, 2015
fc953de
Update log message prefixes
cflynn07 Nov 30, 2015
b1ed80b
Add if statement block for unresponsive query
cflynn07 Nov 30, 2015
a8f0098
Update README
cflynn07 Nov 30, 2015
b1b83dd
Update src comments
cflynn07 Nov 30, 2015
68764a4
Add trace logs and add list of port numbers to view data
cflynn07 Nov 30, 2015
9e39139
Change API_HOST to API_HOSTNAME for greater consistency
cflynn07 Nov 30, 2015
b45b1e1
Remove else after return
cflynn07 Dec 2, 2015
4e91d82
Replace console logs with bunyan
cflynn07 Dec 2, 2015
b9a3035
Send error message to rollbar
cflynn07 Dec 2, 2015
e7e34bf
Switch if/else to be if/return
cflynn07 Dec 2, 2015
730e20d
Add unit tests app.loginSuperUser
cflynn07 Dec 2, 2015
d08f22b
Add unit tests app._validateRequest'
cflynn07 Dec 2, 2015
a4ea96a
Ignore leaks in tests
cflynn07 Dec 2, 2015
7b45982
Add unit tests app._fetchInstance
cflynn07 Dec 2, 2015
eb33f53
Add unit tests app._processNaviError
cflynn07 Dec 2, 2015
55a5e25
Add circle.yml test file
cflynn07 Dec 2, 2015
a3283df
Remove defaulting assign logic
cflynn07 Dec 2, 2015
9591b25
Add rollbar report wait process exit
cflynn07 Dec 2, 2015
83a6ad7
Remove serializers put extension
cflynn07 Dec 2, 2015
ae7769f
Fix undefined reference
cflynn07 Dec 3, 2015
181f42e
Fix typo in app module
cflynn07 Dec 3, 2015
cf117d1
Add port exception prevent logic
cflynn07 Dec 3, 2015
4c6656c
Refactor code to use instance.getBranchName()
cflynn07 Dec 4, 2015
1606b54
Change format of title in README
cflynn07 Dec 5, 2015
326844a
Temporarily hide links until bug resolved
cflynn07 Dec 5, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# detention
Handles our 404
# Detention

### Navi request general error message response producing service.

Navi will proxy to this service in the event of several types of error scenarios. Detention fetches
the status of an instance from API and produces an error HTML response page.

script to push it
```
Expand Down
259 changes: 211 additions & 48 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,231 @@
/**
* @module app
*/
'use strict';

var ErrorCat = require('error-cat');
var Runnable = require('runnable');
var assign = require('101/assign');
var bodyParser = require('body-parser');
var express = require('express');
var keypather = require('keypather')();
var path = require('path');
var bodyParser = require('body-parser');
var put = require('101/put');

var version = require('./package.json').version;
var app = express();
var log = app.log = require('./logger')(__filename);
var version = require('./package.json').version;

// valid Detention request types (val for req validation)
var validDetentionTypes = [
'not_running',
'ports',
'signin',
'unresponsive'
];

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

var locals = {
version: version
assign(app.locals, {
localVersion: version,
absoluteUrl: process.env.ABSOLUTE_URL
});

// this is used for hello runnable user so we only have to login once
var superUser = app.superUser = new Runnable(process.env.API_HOSTNAME, {
requestDefaults: {
headers: {
'user-agent': 'detention-root'
},
}
});

/**
* Authenticate with API as super user
* Must invoke before server begins listening
*/
app.loginSuperUser = function (cb) {
var logData = {
tx: true
};
log.info(logData, 'api.loginSuperUser');
superUser.githubLogin(process.env.HELLO_RUNNABLE_GITHUB_TOKEN, function (err) {
if (err) {
log.error(put({
err: err
}, logData), 'loginSuperUser error');
} else {
log.trace(logData, 'loginSuperUser success');
}
cb(err);
});
};

/**
* Validate query parameters
*/
app._validateRequest = function (req, res, next) {
log.info('app._validateRequest');
if (!req.query.shortHash && req.query.type !== 'signin') {
log.trace('_validateRequest !shortHash');
// TODO?: switch to createAndReport
return next(ErrorCat.create(500, 'instance shortHash required'));
}
if (!~validDetentionTypes.indexOf(req.query.type)) {
// only valid occurance if login error
log.trace('_validateRequest !type');
// TODO?: switch to createAndReport
return next(ErrorCat.create(500, 'invalid request type'));
}
log.trace('_validateRequest success');
next();
};

/**
* Fetch Instance resource from API
*/
app._fetchInstance = function (req, res, next) {
log.info({
shortHash: req.query.shortHash
}, 'api._fetchInstance');
if (req.query.type === 'signin') {
log.trace('_fetchInstance signin bypass');
return next();
}
req.instance = superUser.fetchInstance(req.query.shortHash, function (err) {
if (err) {
log.error({
err: err
}, '_fetchInstance superUser.fetchInstance error');
// TODO?: switch to createAndReport
return next(ErrorCat.create(404, 'instance not found'));
}
log.trace('_fetchInstance superUser.fetchInstance success');
next();
});
};
// uncomment after placing your favicon in /public
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

app.route('/*').get(function (req, res, next) {
var options = {
localVersion: version,
absoluteUrl: process.env.ABSOLUTE_URL || 'detention.runnable.io'
};
if (req.query.type) {
var page = req.query.type;
[
'status',
'branchName',
'redirectUrl',
'containerUrl',
'ownerName',
'instanceName'
].forEach(function (option) {
options[option] = req.query[option];
});
if (req.query.ports) {
var value = req.query.ports;
if (!Array.isArray(value)) {
value = [value];
}
options.ports = value;
/**
* Resolve instance status and render + return relevant error message html page
*/
app._processNaviError = function (req, res, next) {
log.info({
query: req.query
}, 'processNaviError');
var options = {};

[
'redirectUrl',
'shortHash'
].forEach(function (option) {
options[option] = req.query[option];
});

if (req.instance) {
options.branchName = req.instance.getBranchName();
// Temp missing pending resolution of SAN-3018
// https://runnable.atlassian.net/browse/SAN-3018
options.instanceName = keypather.get(req.instance, 'attrs.lowerName');
options.ownerName = keypather.get(req.instance, 'attrs.owner.username');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found in my testing that this coming back null (using prod api). I only get the githubId back

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes @Nathan219 this JIRA ticket is tracking that.
https://runnable.atlassian.net/browse/SAN-3018

Not a blocker for deploying the refactored detention as is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye tho finding that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is null, which makes the links (view logs, go to container) not work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see this? #8 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's weird that the comment wasn't on the full change...

var ports = keypather.get(req.instance, 'attrs.container.ports');
if (ports) {
options.ports = Object.keys(ports).map(function (portKey) {
// Ex: '3000/tcp' --> '3000'
return portKey.replace(/\/tcp$/, '');
});
} else {
log.warn({
instance: req.instance
}, '_processNaviError instance !ports');
}
options.headerText = options.status;
if (options.status) {
if (options.status === 'buildFailed') {
options.headerText = 'build failed.';
options.status = 'failed to build';
} else {
options.status = 'is ' + options.status;
}
}

if (req.query.type === 'signin') {
log.trace('processNaviError type signin');
return res.render('pages/signin', options);
}
if (req.query.type === 'not_running') {
log.trace('processNaviError type not_running');

// container state error pages.
// - Not running (building, starting, crashed)
// - Running, but unresponsive
var status = req.instance.status();
log.trace({
status: status,
options: options
}, 'processNaviError instance status');

options.status = status;
switch(status) {
case 'stopped':
case 'crashed':
case 'stopping':
options.headerText = 'is ' + status;
res.render('pages/dead', options);
break;
case 'running':
// The instance could have started after Navi fetched it and proxied to detention.
// Might not be the best idea to trigger a refresh, could easily result in user-unfriendly
// infinite redirect loops. Better to display an error page prompting user to refresh?
options.headerText = 'is running';
res.render('pages/dead', options);
break;
case 'buildFailed':
options.headerText = 'build failed';
res.render('pages/dead', options);
break;
case 'building':
options.headerText = 'is building';
res.render('pages/dead', options);
break;
case 'neverStarted':
case 'starting':
options.headerText = 'is starting';
res.render('pages/dead', options);
break;
case 'unknown':
options.headerText = 'unknown';
res.render('pages/dead', options);
break;
}
res.render('pages/' + page, options);
} else {
res.render('pages/invalid', options);
return;
}
});
if (req.query.type === 'ports'){
log.trace('processNaviError type ports');
/*
* Currently not implemented, might be bundled into 'unresponsive'
*
* Userland hipache will only route to navi if a request is made to an elastic url on a port
* that's explicitly set on the instance (we set hipache redis entries when ports are exposed)
* otherwise userland-hipache will return an error page due to a lack of a redis entry.
*
* Probably could fix by patching Hipache or perhaps reading the manual to see if there's a
* some kind of forward-for-all-ports functionality
*
* Anand if you read this Monday morning lets chat about it at 3pm
*/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't ports work right now? Or have I only been seeing them due to errors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unresponsive page will show you a list of ports. That's probably what you've been seeing. We don't have anything yet that can display a message if you try to access a port that isn't set as exposed on the instance document. We basically need to hack hipache to get that.

return;
}
if (req.query.type === 'unresponsive'){
log.trace('processNaviError type unresponsive');
res.render('pages/unresponsive', options);
return;
}
};

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

app.route('/*').get(
app._validateRequest,
app._fetchInstance,
app._processNaviError
);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
Expand All @@ -66,15 +234,10 @@ app.use(function(req, res, next) {
next(err);
});

// error handlers

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.render('pages/invalid', {
localVersion: version
});
res.render('pages/invalid', {});
});


module.exports = app;
25 changes: 23 additions & 2 deletions bin/www
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,29 @@
* Module dependencies.
*/

var app = require('../app');
var ErrorCat = require('error-cat');
var debug = require('debug')('detention:server');
var hasKeypaths = require('101/has-keypaths');
var http = require('http');

var app = require('../app');
var log = require('../logger')(__filename);

var requiredEnvKeys = [
'HELLO_RUNNABLE_GITHUB_TOKEN',
'API_HOSTNAME'
];
if (!hasKeypaths(process.env, requiredEnvKeys)) {
log.error({
requiredEnvKeys: requiredEnvKeys,
env: process.env
}, 'Missing required ENV keys');
// send to rollbar
ErrorCat.report(new Error('Detention missing required ENV values'), null, function () {
process.exit(1);
});
}

/**
* Get port from environment and store in Express.
*/
Expand All @@ -25,7 +44,9 @@ var server = http.createServer(app);
* Listen on provided port, on all network interfaces.
*/

server.listen(port);
app.loginSuperUser(function () {
server.listen(port);
});
server.on('error', onError);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this above app.login, was a bug before, if listen error sync it will not get called.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also where is onError ?!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh nm, git diff messed me up

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was confused by what you thought you were seeing....

server.on('listening', onListening);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is onListening defined?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 lines down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh nm, git diff messed me up


Expand Down
15 changes: 15 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
machine:
environment:
NODE_ENV: test
LOG_LEVEL_STDOUT: error
dependencies:
override:
- nvm install 4.2.0
- nvm alias default 4.2.0
- npm install -g npm@2.8.3
- npm install
test:
pre:
- ulimit -n 10240
override:
- npm run test
31 changes: 31 additions & 0 deletions logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @module lib/logger
*/
'use strict';

var bunyan = require('bunyan');
var envIs = require('101/env-is');
var path = require('path');
var put = require('101/put');

var logger = bunyan.createLogger({
name: 'detention',
streams: [{
level: process.env.LOG_LEVEL_STDOUT || 'trace',
stream: process.stdout
}],
serializers: bunyan.stdSerializers,
// DO NOT use src in prod, slow
src: !envIs('production'),
environment: process.env.NODE_ENV
});

/**
* Return a new child bunyan instance
* @param {String} namespace
*/
module.exports = function (namespace) {
return logger.child({
module: path.relative(process.cwd(), namespace)
});
}
Loading