screwdriver-api
Version:
API server for the Screwdriver.cd service
243 lines (209 loc) • 10 kB
JavaScript
;
const Hapi = require('@hapi/hapi');
const logger = require('screwdriver-logger');
const registerPlugins = require('./registerPlugins');
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled Rejection at: Promise', p, 'reason:', reason); /* eslint-disable-line no-console */
});
/**
* If we're throwing errors, let's have them say a little more than just 500
* @method prettyPrintErrors
* @param {Hapi.Request} request Hapi Request object
* @param {Hapi.h} h Hapi Response Toolkit
*/
function prettyPrintErrors(request, h) {
const { response } = request;
const { release } = request.server.app;
if (release && release.cookieName && request.state && !request.state[release.cookieName]) {
h.state(release.cookieName, release.cookieValue);
}
if (!response.isBoom) {
return h.continue;
}
const err = response;
const errName = err.output.payload.error;
const errMessage = err.message;
const { statusCode } = err.output.payload;
const stack = err.stack || errMessage;
if (statusCode === 500) {
request.log(['server', 'error'], stack);
}
const res = {
statusCode,
error: errName,
message: errMessage
};
if (err.data) {
res.data = err.data;
}
return h.response(res).code(statusCode);
}
/**
* Configures & starts up a HapiJS server
* @method
* @param {Object} config
* @param {Object} config.httpd
* @param {Integer} config.httpd.port Port number to listen to
* @param {String} config.httpd.host Host to listen on
* @param {String} config.httpd.uri Public routable address
* @param {Object} config.httpd.tls TLS Configuration
* @param {Object} config.webhooks Webhooks settings
* @param {String} config.webhooks.restrictPR Restrict PR setting
* @param {Boolean} config.webhooks.chainPR Chain PR flag
* @param {Object} config.ecosystem List of hosts in the ecosystem
* @param {Object} config.ecosystem.ui URL for the User Interface
* @param {Factory} config.pipelineFactory Pipeline Factory instance
* @param {Factory} config.jobFactory Job Factory instance
* @param {Factory} config.userFactory User Factory instance
* @param {Factory} config.bannerFactory Banner Factory instance
* @param {Factory} config.buildFactory Build Factory instance
* @param {Factory} config.buildClusterFactory Build Cluster Factory instance
* @param {Factory} config.stepFactory Step Factory instance
* @param {Factory} config.secretFactory Secret Factory instance
* @param {Factory} config.tokenFactory Token Factory instance
* @param {Factory} config.eventFactory Event Factory instance
* @param {Factory} config.collectionFactory Collection Factory instance
* @param {Factory} config.stageFactory Stage Factory instance
* @param {Factory} config.stageBuildFactory Stage Build Factory instance
* @param {Factory} config.triggerFactory Trigger Factory instance
* @param {Object} config.builds Config to include for builds plugin
* @param {Object} config.builds.ecosystem List of hosts in the ecosystem
* @param {Object} config.builds.authConfig Configuration for auth
* @param {Object} config.builds.externalJoin Flag to allow external join
* @param {Object} config.unzipArtifactsEnabled Flag to allow unzip artifacts
* @param {Object} config.artifactsMaxDownloadSize Maximum download size for artifacts
* @param {Function} callback Callback to invoke when server has started.
* @return {http.Server} A listener: NodeJS http.Server object
*/
module.exports = async config => {
try {
// Hapi Cross-origin resource sharing configuration
// See http://hapijs.com/api for available options
let corsOrigins = [config.ecosystem.ui];
if (Array.isArray(config.ecosystem.allowCors)) {
corsOrigins = corsOrigins.concat(config.ecosystem.allowCors);
}
const cors = {
origin: corsOrigins,
additionalExposedHeaders: ['x-more-data'],
credentials: true
};
// Create a server with a host and port
const server = new Hapi.Server({
port: config.httpd.port,
host: config.httpd.host,
uri: config.httpd.uri,
routes: {
cors,
log: { collect: true }
},
state: {
strictHeader: false
},
router: {
stripTrailingSlash: true
}
});
// Set the factories within server.app
// Instantiating the server with the factories will apply a shallow copy
server.app = {
commandFactory: config.commandFactory,
commandTagFactory: config.commandTagFactory,
templateFactory: config.templateFactory,
templateTagFactory: config.templateTagFactory,
pipelineTemplateFactory: config.pipelineTemplateFactory,
pipelineTemplateVersionFactory: config.pipelineTemplateVersionFactory,
jobTemplateTagFactory: config.jobTemplateTagFactory,
pipelineTemplateTagFactory: config.pipelineTemplateTagFactory,
stageFactory: config.stageFactory,
stageBuildFactory: config.stageBuildFactory,
triggerFactory: config.triggerFactory,
pipelineFactory: config.pipelineFactory,
jobFactory: config.jobFactory,
userFactory: config.userFactory,
buildFactory: config.buildFactory,
stepFactory: config.stepFactory,
bannerFactory: config.bannerFactory,
secretFactory: config.secretFactory,
tokenFactory: config.tokenFactory,
eventFactory: config.eventFactory,
collectionFactory: config.collectionFactory,
buildClusterFactory: config.buildClusterFactory,
ecosystem: config.ecosystem,
release: config.release,
queueWebhook: config.queueWebhook,
unzipArtifacts: config.unzipArtifactsEnabled
};
const bellConfigs = await config.auth.scm.getBellConfiguration();
config.auth.bell = bellConfigs;
if (config.release && config.release.cookieName) {
server.state(config.release.cookieName, {
path: '/',
ttl: config.release.cookieTimeout * 60 * 1000, // (2 mins)
isSecure: config.auth.https,
isHttpOnly: !config.auth.https
});
}
// Write prettier errors
server.ext('onPreResponse', prettyPrintErrors);
// Audit log
if (config.log && config.log.audit.enabled) {
server.ext('onCredentials', (request, h) => {
const { username, scope, pipelineId } = request.auth.credentials;
if (scope) {
const validScope = config.log.audit.scope.filter(s => scope.includes(s));
if (Array.isArray(validScope) && validScope.length > 0) {
let context;
if (validScope.includes('admin')) {
context = `Admin ${username}`;
} else if (validScope.includes('user')) {
context = `User ${username}`;
} else if (validScope.includes('build') || validScope.includes('temporal')) {
context = `Build ${username}`;
} else if (validScope.includes('pipeline')) {
context = `Pipeline ${pipelineId}`;
} else {
context = `Guest ${username}`;
}
logger.info(`[Login] ${context} ${request.method} ${request.path}`);
}
}
return h.continue;
});
}
// Register events for notifications plugin
server.event(['build_status', 'job_status']);
// Register plugins
await registerPlugins(server, config);
// Initialize common data in buildFactory and jobFactory
server.app.buildFactory.apiUri = server.info.uri;
server.app.buildFactory.tokenGen = (buildId, metadata, scmContext, expiresIn, scope = ['temporal']) =>
server.plugins.auth.generateToken(
server.plugins.auth.generateProfile({ username: buildId, scmContext, scope, metadata }),
expiresIn
);
server.app.buildFactory.executor.tokenGen = server.app.buildFactory.tokenGen;
server.app.buildFactory.maxDownloadSize = parseInt(config.artifactsMaxDownloadSize, 10) * 1024 * 1024 * 1024;
server.app.jobFactory.apiUri = server.info.uri;
server.app.jobFactory.tokenGen = (username, metadata, scmContext, scope = ['user']) =>
server.plugins.auth.generateToken(
server.plugins.auth.generateProfile({ username, scmContext, scope, metadata })
);
server.app.jobFactory.executor.userTokenGen = server.app.jobFactory.tokenGen;
if (server.plugins.shutdown) {
server.plugins.shutdown.handler({
taskname: 'executor-queue-cleanup',
task: async () => {
await server.app.jobFactory.cleanUp();
logger.info('completed clean up tasks');
}
});
}
// Start the server
await server.start();
return server;
} catch (err) {
logger.error('Failed to start server', err);
throw err;
}
};