From 8d4735767dbada61ac99000ec4e126f5d090c292 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 17 May 2024 15:17:26 +0200 Subject: [PATCH] backend-app-api: move healthchecks to healthService Signed-off-by: Vincenzo Scamporlino --- .../health/createHealthRouter.test.ts | 81 +++++++++++++++++++ .../health/createHealthRouter.ts | 46 +++++++++++ .../health/healthServiceFactory.ts | 34 ++++++++ .../services/implementations/health/index.ts | 17 ++++ .../createHealthcheck.test.ts | 0 .../createHealthcheck.ts | 0 .../src/services/definitions/HealthService.ts | 20 +++++ .../services/definitions/HttpRouterService.ts | 5 -- .../src/services/definitions/coreServices.ts | 7 ++ .../src/services/definitions/index.ts | 2 +- .../src/next/services/mockServices.ts | 1 - 11 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/health/createHealthRouter.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/health/createHealthRouter.ts create mode 100644 packages/backend-app-api/src/services/implementations/health/healthServiceFactory.ts create mode 100644 packages/backend-app-api/src/services/implementations/health/index.ts rename packages/backend-app-api/src/services/implementations/{httpRouter => rootHttpRouter}/createHealthcheck.test.ts (100%) rename packages/backend-app-api/src/services/implementations/{httpRouter => rootHttpRouter}/createHealthcheck.ts (100%) create mode 100644 packages/backend-plugin-api/src/services/definitions/HealthService.ts diff --git a/packages/backend-app-api/src/services/implementations/health/createHealthRouter.test.ts b/packages/backend-app-api/src/services/implementations/health/createHealthRouter.test.ts new file mode 100644 index 0000000000..f8c51ee08d --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/health/createHealthRouter.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +import { mockServices } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import { createHealthRouter } from './createHealthRouter'; +import express from 'express'; + +describe('createHealthRouter', () => { + describe('readiness', () => { + it(`should return a 500 response if the server hasn't started yet`, async () => { + const hc = createHealthRouter({ + lifecycle: mockServices.rootLifecycle.mock(), + }); + const app = express().use(hc); + + const response = await request(app).get('/v1/readiness'); + expect(response.status).toBe(500); + }); + + it('should return 200 if the server has started', async () => { + const lifecycle = mockServices.rootLifecycle.mock(); + let mockServerStartedFn = () => {}; + lifecycle.addStartupHook.mockImplementation( + fn => (mockServerStartedFn = fn), + ); + + const hc = createHealthRouter({ lifecycle }); + const app = express().use(hc); + + mockServerStartedFn(); + const response = await request(app).get('/v1/readiness').expect(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + + it(`should return a 500 response if the server has stopped`, async () => { + const lifecycle = mockServices.rootLifecycle.mock(); + let mockServerStartedFn = () => {}; + let mockServerStoppedFn = () => {}; + lifecycle.addStartupHook.mockImplementation( + fn => (mockServerStartedFn = fn), + ); + lifecycle.addShutdownHook.mockImplementation( + fn => (mockServerStoppedFn = fn), + ); + + const hc = createHealthRouter({ lifecycle }); + const app = express().use(hc); + + mockServerStartedFn(); + mockServerStoppedFn(); + const response = await request(app).get('/v1/readiness'); + expect(response.status).toBe(500); + }); + }); + + describe('liveness', () => { + it('should return 200 if the server has started', async () => { + const lifecycle = mockServices.rootLifecycle.mock(); + + const hc = createHealthRouter({ lifecycle }); + const app = express().use(hc); + + const response = await request(app).get('/v1/liveness').expect(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/packages/backend-app-api/src/services/implementations/health/createHealthRouter.ts b/packages/backend-app-api/src/services/implementations/health/createHealthRouter.ts new file mode 100644 index 0000000000..f2ff2ecd73 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/health/createHealthRouter.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +import Router from 'express-promise-router'; +import { Request, Response } from 'express'; +import { RootLifecycleService } from '@backstage/backend-plugin-api'; + +export function createHealthRouter(options: { + lifecycle: RootLifecycleService; +}) { + const router = Router(); + + let isRunning = false; + options.lifecycle.addStartupHook(() => { + isRunning = true; + }); + options.lifecycle.addShutdownHook(() => { + isRunning = false; + }); + + router.get('/v1/readiness', async (_request: Request, response: Response) => { + if (!isRunning) { + throw new Error('Backend has not started yet'); + } + response.json({ status: 'ok' }); + }); + + router.get('/v1/liveness', async (_request: Request, response: Response) => { + response.json({ status: 'ok' }); + }); + + return router; +} diff --git a/packages/backend-app-api/src/services/implementations/health/healthServiceFactory.ts b/packages/backend-app-api/src/services/implementations/health/healthServiceFactory.ts new file mode 100644 index 0000000000..831f4fa6e8 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/health/healthServiceFactory.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +import { + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { createHealthRouter } from './createHealthRouter'; + +export const healthServiceFactory = createServiceFactory({ + service: coreServices.health, + deps: { + rootHttpRouter: coreServices.rootHttpRouter, + lifecycle: coreServices.rootLifecycle, + }, + async factory({ lifecycle, rootHttpRouter }) { + rootHttpRouter.use('.backstage/health', createHealthRouter({ lifecycle })); + + return {}; + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/health/index.ts b/packages/backend-app-api/src/services/implementations/health/index.ts new file mode 100644 index 0000000000..4aeb4b7979 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/health/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +export { healthServiceFactory } from './healthServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createHealthcheck.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/createHealthcheck.test.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createHealthcheck.test.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/createHealthcheck.test.ts diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createHealthcheck.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/createHealthcheck.ts similarity index 100% rename from packages/backend-app-api/src/services/implementations/httpRouter/createHealthcheck.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/createHealthcheck.ts diff --git a/packages/backend-plugin-api/src/services/definitions/HealthService.ts b/packages/backend-plugin-api/src/services/definitions/HealthService.ts new file mode 100644 index 0000000000..d04a8a6133 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/HealthService.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +/** + * @public + */ +export interface HealthService {} diff --git a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts index 2cb5c74649..30c4fb31c8 100644 --- a/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HttpRouterService.ts @@ -26,11 +26,6 @@ export interface HttpRouterServiceAuthPolicy { allow: 'unauthenticated' | 'user-cookie'; } -/** @public */ -export interface HttpRouterHealthCheckConfig { - handler: () => Promise; -} - /** * Allows plugins to register HTTP routes. * diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index b0518fb9d4..36ec0151c5 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -102,6 +102,13 @@ export namespace coreServices { import('./DiscoveryService').DiscoveryService >({ id: 'core.discovery' }); + /** + * The service reference for the plugin scoped {@link RootHealthService}. + */ + export const health = createServiceRef< + import('./RootHealthService').RootHealthService + >({ id: 'core.health', scope: 'root' }); + /** * Authentication of HTTP requests. * diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 57cfd404c1..af0d991f58 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -32,10 +32,10 @@ export type { export type { RootConfigService } from './RootConfigService'; export type { DatabaseService } from './DatabaseService'; export type { DiscoveryService } from './DiscoveryService'; +export type { HealthService } from './HealthService'; export type { HttpRouterService, HttpRouterServiceAuthPolicy, - HttpRouterHealthCheckConfig, } from './HttpRouterService'; export type { HttpAuthService } from './HttpAuthService'; export type { diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 379e9473ca..24571b634c 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -348,7 +348,6 @@ export namespace mockServices { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ use: jest.fn(), - healthCheckConfig: jest.fn(), addAuthPolicy: jest.fn(), })); }