diff --git a/app/routes/index.js b/app/routes/index.js index ebc1a5e7..72493863 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -48,9 +48,11 @@ router.get('/v1/health', (req, res)=>{ }); }); -router.use('/kube', Kube); router.use(createExpressLogger('razeedash-api/api')); +// Respond to /api/kube/[liveness|readiness|startup] +router.use('/kube', Kube); + router.use(asyncHandler(async (req, res, next) => { const db = req.app.get('db'); req.db = db; diff --git a/app/routes/kube/kube.js b/app/routes/kube/kube.js index 8a9a5909..3892ef80 100644 --- a/app/routes/kube/kube.js +++ b/app/routes/kube/kube.js @@ -19,7 +19,7 @@ const probeUtil = require('../../utils/probes'); const router = express.Router(); -router.get('/startup', asyncHandler(async (req, res) => { +const startupHandler = asyncHandler(async (req, res) => { try { const payload = await probeUtil.getStartupPayload(req); return res.status(200).send(payload); @@ -27,9 +27,10 @@ router.get('/startup', asyncHandler(async (req, res) => { catch (e) { return res.status(503).send('service unavailable'); } -})); +}); +router.get('/startup', startupHandler); -router.get('/readiness', asyncHandler(async (req, res) => { +const readinessHandler = asyncHandler(async (req, res) => { try { const payload = await probeUtil.getReadinessPayload(req); return res.status(200).send(payload); @@ -37,9 +38,10 @@ router.get('/readiness', asyncHandler(async (req, res) => { catch (e) { return res.status(503).send('service unavailable'); } -})); +}); +router.get('/readiness', readinessHandler); -router.get('/liveness', asyncHandler(async(req, res) => { +const livenessHandler = asyncHandler(async(req, res) => { try { const payload = await probeUtil.getLivenessPayload(req); return res.status(200).send(payload); @@ -47,6 +49,7 @@ router.get('/liveness', asyncHandler(async(req, res) => { catch (e) { return res.status(503).send('service unavailable'); } -})); +}); +router.get('/liveness', livenessHandler); module.exports = router; diff --git a/app/routes/kube/kube.tests.js b/app/routes/kube/kube.tests.js new file mode 100644 index 00000000..f41a51f5 --- /dev/null +++ b/app/routes/kube/kube.tests.js @@ -0,0 +1,103 @@ +/* eslint-env node, mocha */ +/** + * Copyright 2024 IBM Corp. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const assert = require('assert'); +const mongodb = require('mongo-mock'); +var httpMocks = require('node-mocks-http'); +const log = require('../../log').log; + +const probeUtil = require('../../utils/probes'); +const defaultProbe = require('../../utils/probes/probe-default.js'); + +const rewire = require('rewire'); +let kube = rewire('./kube'); +let db = {}; + +describe('probes', () => { + + before(async function () { + mongodb.max_delay = 0; + const MongoClient = mongodb.MongoClient; + db = await MongoClient.connect('someconnectstring', {}); + db.collection('orgs'); + }); + + after(function () { + db.close(); + }); + + describe('startupProbe', () => { + it('should pass the default startup probe after setStartupComplete is called', async () => { + const startupHandler = kube.__get__('startupHandler'); + + const request = httpMocks.createRequest({ + method: 'GET', + url: '/startup', + params: {}, + log: log + }); + const response = httpMocks.createResponse(); + + // Default impl returns failure before 'setStartupComplete' is called + await startupHandler(request, response); + assert.equal(response.statusCode, 503); + + defaultProbe.setStartupComplete(true); + + // Default impl returns success after 'setStartupComplete' is called + await startupHandler(request, response); + assert.equal(response.statusCode, 200); + }); + + it('should fail if the custom startup probe fails', async () => { + const startupHandler = kube.__get__('startupHandler'); + + const request = httpMocks.createRequest({ + method: 'GET', + url: '/startup', + params: {}, + log: log + }); + const response = httpMocks.createResponse(); + + // Note: default probe setStartupComplete has already been called by earlier test + + probeUtil.setImpl('./probe-testFailure.js'); + await startupHandler(request, response); + + assert.equal(response.statusCode, 503); + }); + + it('should succeed if the custom startup probe succeeds', async () => { + const startupHandler = kube.__get__('startupHandler'); + + const request = httpMocks.createRequest({ + method: 'GET', + url: '/startup', + params: {}, + log: log + }); + const response = httpMocks.createResponse(); + + // Note: default probe setStartupComplete has already been called by earlier test + + probeUtil.setImpl('./probe-testSuccess.js'); + await startupHandler(request, response); + + assert.equal(response.statusCode, 200); + }); + }); +}); diff --git a/app/utils/probes/index.js b/app/utils/probes/index.js index 6cd01c6e..f4a2fcfd 100644 --- a/app/utils/probes/index.js +++ b/app/utils/probes/index.js @@ -15,7 +15,7 @@ */ const PROBE_DEFAULT_IMPL = require( './probe-default.js' ); -const PROBE_CUSTOM_IMPL = require( process.env.PROBE_IMPL || './probe-none.js' ); +let PROBE_CUSTOM_IMPL = require( process.env.PROBE_IMPL || './probe-none.js' ); /* Return an impl for each of the probe types: @@ -26,29 +26,32 @@ Return an impl for each of the probe types: Return the custom payload, or the default payload if there is none. */ const PROBE_IMPL = { - getStartupPayload: async function( context ) { + getStartupPayload: async function( req ) { const method = 'getStartupPayload'; - const defaultPayload = await PROBE_DEFAULT_IMPL[method](context); - if( !Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { - return( PROBE_DEFAULT_IMPL[method](context) ); + const defaultPayload = await PROBE_DEFAULT_IMPL[method](req); + if( Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { + return( await PROBE_CUSTOM_IMPL[method](req) ); } return defaultPayload; }, - getReadinessPayload: async function( context ) { + getReadinessPayload: async function( req ) { const method = 'getReadinessPayload'; - const defaultPayload = await PROBE_DEFAULT_IMPL[method](context); - if( !Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { - return( PROBE_DEFAULT_IMPL[method](context) ); + const defaultPayload = await PROBE_DEFAULT_IMPL[method](req); + if( Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { + return( await PROBE_CUSTOM_IMPL[method](req) ); } return defaultPayload; }, - getLivenessPayload: async function( context ) { + getLivenessPayload: async function( req ) { const method = 'getLivenessPayload'; - const defaultPayload = await PROBE_DEFAULT_IMPL[method](context); - if( !Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { - return( PROBE_DEFAULT_IMPL[method](context) ); + const defaultPayload = await PROBE_DEFAULT_IMPL[method](req); + if( Object.prototype.hasOwnProperty.call(PROBE_CUSTOM_IMPL, method) ) { + return( await PROBE_CUSTOM_IMPL[method](req) ); } return defaultPayload; + }, + setImpl: function( newImpl ) { + PROBE_CUSTOM_IMPL = require( newImpl || './probe-none.js' ); } }; diff --git a/app/utils/probes/probe-testFailure.js b/app/utils/probes/probe-testFailure.js new file mode 100644 index 00000000..bd99fc64 --- /dev/null +++ b/app/utils/probes/probe-testFailure.js @@ -0,0 +1,35 @@ +/** + * Copyright 2024 IBM Corp. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/* +This sample shows how the startup/liveness/readiness probes can be customized to always fail. +It is used by automated unit testing. +*/ + +async function getStartupPayload() { + throw new Error('probe failure for testing'); +} + +async function getReadinessPayload() { + throw new Error('probe failure for testing'); +} + +async function getLivenessPayload() { + throw new Error('probe failure for testing'); +} + +module.exports = { getLivenessPayload, getReadinessPayload, getStartupPayload }; diff --git a/app/utils/probes/probe-testSuccess.js b/app/utils/probes/probe-testSuccess.js new file mode 100644 index 00000000..b502a9fc --- /dev/null +++ b/app/utils/probes/probe-testSuccess.js @@ -0,0 +1,35 @@ +/** + * Copyright 2024 IBM Corp. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/* +This sample shows how the startup/liveness/readiness probes can be customized to always succeed. +It is used by automated unit testing. +*/ + +async function getStartupPayload() { + return('probe success for testing'); +} + +async function getReadinessPayload() { + return('probe success for testing'); +} + +async function getLivenessPayload() { + return('probe success for testing'); +} + +module.exports = { getLivenessPayload, getReadinessPayload, getStartupPayload };