From 159913e6741cb79bd4846eae8e7ebcd24b63e7c1 Mon Sep 17 00:00:00 2001 From: James Seppi Date: Thu, 7 Sep 2017 10:51:22 -0500 Subject: [PATCH] add healthcheck endpoint for monitoring builder health Right now, it only checks to make sure that the builder is able to authenticate with cloud.gov. Other checks can be added in the future. --- src/server.js | 32 ++++++++++++++++++++ test/cloud-foundry-auth-client-test.js | 6 +++- test/nocks/cloud-foundry-oauth-token-nock.js | 10 ++++-- test/server-test.js | 32 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index 8b358310..a16f9227 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,6 @@ const Hapi = require('hapi'); const winston = require('winston'); +const CloudFoundryAuthClient = require('./cloud-foundry-auth-client'); function createServer(cluster) { const server = new Hapi.Server(); @@ -18,6 +19,37 @@ function createServer(cluster) { }, }); + server.route({ + method: 'GET', + path: '/healthcheck', + handler: (request, reply) => { + // Exposes an endpoint to report on server health + + const authClient = new CloudFoundryAuthClient(); + + // Array of promises returned from methods we want included in the healthcheck + const checkPromises = [ + authClient.accessToken(), // make sure we can authenticate with cloud.gov + ]; + + const replyOk = () => reply({ ok: true }); + const replyNotOk = () => reply({ ok: false }); + + Promise.all(checkPromises) + .then(([token]) => { + if (!token) { + replyNotOk(); + } else { + replyOk(); + } + }) + .catch((err) => { + winston.error('Healthcheck error', err); + replyNotOk(); + }); + }, + }); + server.route({ method: 'DELETE', path: '/builds/{buildID}/callback', diff --git a/test/cloud-foundry-auth-client-test.js b/test/cloud-foundry-auth-client-test.js index 263d4e71..3a750164 100644 --- a/test/cloud-foundry-auth-client-test.js +++ b/test/cloud-foundry-auth-client-test.js @@ -1,10 +1,14 @@ const expect = require('chai').expect; const jwt = require('jsonwebtoken'); const nock = require('nock'); + const CloudFoundryAuthClient = require('../src/cloud-foundry-auth-client'); const mockTokenRequest = require('./nocks/cloud-foundry-oauth-token-nock'); -const mockToken = (expiration = (Date.now() / 1000) + 600) => jwt.sign({ exp: expiration }, '123abc'); +const mockToken = (expiration = (Date.now() / 1000) + 600) => ( + jwt.sign({ exp: expiration }, '123abc') +); + describe('CloudFoundryAuthClient', () => { describe('.accessToken()', () => { diff --git a/test/nocks/cloud-foundry-oauth-token-nock.js b/test/nocks/cloud-foundry-oauth-token-nock.js index ce475ed3..79ffb4d0 100644 --- a/test/nocks/cloud-foundry-oauth-token-nock.js +++ b/test/nocks/cloud-foundry-oauth-token-nock.js @@ -4,7 +4,7 @@ const nock = require('nock'); const mockTokenRequest = (token) => { const accessToken = token || jwt.sign({ exp: (Date.now() / 1000) + 600 }, '123abc'); - return nock('https://login.example.com', { + const n = nock('https://login.example.com', { reqheaders: { authorization: `Basic ${Buffer('cf:').toString('Base64')}`, }, @@ -13,7 +13,13 @@ const mockTokenRequest = (token) => { username: 'deploy_user', password: 'deploy_pass', response_type: 'token', - }).reply(200, { + }); + + if (token === 'badtoken') { + return n.reply(401); + } + + return n.reply(200, { access_token: accessToken, }); }; diff --git a/test/server-test.js b/test/server-test.js index b5f2b484..a480ceba 100644 --- a/test/server-test.js +++ b/test/server-test.js @@ -1,5 +1,7 @@ const expect = require('chai').expect; + const server = require('../src/server'); +const mockTokenRequest = require('./nocks/cloud-foundry-oauth-token-nock'); const mockCluster = () => ({ stopBuild: () => {} }); @@ -18,6 +20,36 @@ describe('server', () => { }); }); + describe('GET /healthcheck', () => { + it('should be ok when a valid access token can be retrieved', (done) => { + const testServer = server(mockCluster()); + mockTokenRequest(); + + testServer.inject({ + method: 'GET', + url: '/healthcheck', + }, (response) => { + expect(response.statusCode).to.eq(200); + expect(response.result).to.deep.equal({ ok: true }); + done(); + }); + }); + + it('should not be ok when an access token cannot be retrieved', (done) => { + const testServer = server(mockCluster()); + mockTokenRequest('badtoken'); + + testServer.inject({ + method: 'GET', + url: '/healthcheck', + }, (response) => { + expect(response.statusCode).to.eq(200); + expect(response.result).to.deep.equal({ ok: false }); + done(); + }); + }); + }); + describe('DELETE /builds/{buildID}/callback', () => { it('should respond with a 200', (done) => { const testServer = server(mockCluster());