From 44f97c173bf9eefb3c281928b84aa1f80bc32375 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 5 Jun 2024 13:27:39 +0200 Subject: [PATCH] backend-app-api: flip logic of health service Signed-off-by: Vincenzo Scamporlino --- .../health/healthServiceFactory.test.ts | 14 ++++++- .../health/healthServiceFactory.ts | 1 + .../rootHttpRouter/createHealthRouter.ts | 42 +++++++++++++++++++ .../rootHttpRouterServiceFactory.ts | 9 +++- packages/backend-plugin-api/api-report.md | 13 +++++- .../src/services/definitions/HealthService.ts | 5 ++- .../src/next/services/mockServices.ts | 18 ++++---- 7 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts diff --git a/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.test.ts index 40a41d74f5..7e160cbd82 100644 --- a/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.test.ts @@ -22,7 +22,13 @@ describe('DefaultHealthService', () => { const service = new DefaultHealthService({ lifecycle: mockServices.rootLifecycle.mock(), }); - await expect(service.getReadiness()).resolves.toEqual({ status: 503 }); + await expect(service.getReadiness()).resolves.toEqual({ + status: 503, + payload: { + message: 'Backend has not started yet', + status: 'error', + }, + }); }); it('should return 200 if the server has started', async () => { @@ -33,7 +39,7 @@ describe('DefaultHealthService', () => { ); const service = new DefaultHealthService({ - lifecycle: mockServices.rootLifecycle.mock(), + lifecycle, }); mockServerStartedFn(); @@ -63,6 +69,10 @@ describe('DefaultHealthService', () => { mockServerStoppedFn(); await expect(service.getReadiness()).resolves.toEqual({ status: 503, + payload: { + message: 'Backend has not started yet', + status: 'error', + }, }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.ts index 3f397ce29b..f68451df74 100644 --- a/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/health/healthServiceFactory.ts @@ -37,6 +37,7 @@ export class DefaultHealthService implements HealthService { async getLiveness(): Promise<{ status: number; payload?: any }> { return { status: 200, payload: { status: 'ok' } }; } + async getReadiness(): Promise<{ status: number; payload?: any }> { if (!this.#isRunning) { return { diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts new file mode 100644 index 0000000000..012a268e02 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -0,0 +1,42 @@ +import { HealthService } from '@backstage/backend-plugin-api'; + +/* + * 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'; + +export function createHealthRouter(options: { health: HealthService }) { + const router = Router(); + + router.get( + '.backstage/health/v1/readiness', + async (_request: Request, response: Response) => { + const { status, payload } = await options.health.getReadiness(); + response.status(status).json(payload); + }, + ); + + router.get( + '.backstage/health/v1/liveness', + async (_request: Request, response: Response) => { + const { status, payload } = await options.health.getLiveness(); + response.status(status).json(payload); + }, + ); + + return router; +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index ea3dc42eb7..5f22cf5ced 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -29,6 +29,7 @@ import { readHttpServerOptions, } from './http'; import { DefaultRootHttpRouter } from './DefaultRootHttpRouter'; +import { createHealthRouter } from './createHealthRouter'; /** * @public @@ -41,6 +42,7 @@ export interface RootHttpRouterConfigureContext { config: RootConfigService; logger: LoggerService; lifecycle: LifecycleService; + healthRouter: RequestHandler; applyDefaults: () => void; } @@ -75,8 +77,9 @@ export const rootHttpRouterServiceFactory = createServiceFactory( config: coreServices.rootConfig, rootLogger: coreServices.rootLogger, lifecycle: coreServices.rootLifecycle, + health: coreServices.health, }, - async factory({ config, rootLogger, lifecycle }) { + async factory({ config, rootLogger, lifecycle, health }) { const { indexPath, configure = defaultConfigure } = options ?? {}; const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); @@ -84,6 +87,8 @@ export const rootHttpRouterServiceFactory = createServiceFactory( const router = DefaultRootHttpRouter.create({ indexPath }); const middleware = MiddlewareFactory.create({ config, logger }); const routes = router.handler(); + + const healthRouter = createHealthRouter({ health }); const server = await createHttpServer( app, readHttpServerOptions(config.getOptionalConfig('backend')), @@ -98,11 +103,13 @@ export const rootHttpRouterServiceFactory = createServiceFactory( config, logger, lifecycle, + healthRouter, applyDefaults() { app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); app.use(middleware.logging()); + app.use(healthRouter); app.use(routes); app.use(middleware.notFound()); app.use(middleware.error()); diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 5e02ad1787..ee5aecd63b 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -335,7 +335,18 @@ export type ExtensionPoint = { export type ExtensionPointConfig = CreateExtensionPointOptions; // @public (undocumented) -export interface HealthService {} +export interface HealthService { + // (undocumented) + getLiveness(): Promise<{ + status: number; + payload?: any; + }>; + // (undocumented) + getReadiness(): Promise<{ + status: number; + payload?: any; + }>; +} // @public export interface HttpAuthService { diff --git a/packages/backend-plugin-api/src/services/definitions/HealthService.ts b/packages/backend-plugin-api/src/services/definitions/HealthService.ts index d04a8a6133..b2dc4f415e 100644 --- a/packages/backend-plugin-api/src/services/definitions/HealthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/HealthService.ts @@ -17,4 +17,7 @@ /** * @public */ -export interface HealthService {} +export interface HealthService { + getLiveness(): Promise<{ status: number; payload?: any }>; + getReadiness(): Promise<{ status: number; payload?: any }>; +} diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 22496bd6e1..a59d053b95 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -20,11 +20,11 @@ import { HostDiscovery, discoveryServiceFactory, } from '@backstage/backend-defaults/discovery'; +import { healthServiceFactory } from '@backstage/backend-defaults/health'; import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter'; import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; -import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; @@ -345,6 +345,14 @@ export namespace mockServices { })); } + export namespace health { + export const factory = healthServiceFactory; + export const mock = simpleMock(coreServices.health, () => ({ + getLiveness: jest.fn(), + getReadiness: jest.fn(), + })); + } + export namespace httpRouter { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ @@ -384,14 +392,6 @@ export namespace mockServices { })); } - export namespace rootHealth { - export const factory = rootHealthServiceFactory; - export const mock = simpleMock(coreServices.health, () => ({ - getLiveness: jest.fn(), - getReadiness: jest.fn(), - })); - } - export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; export const mock = simpleMock(coreServices.rootLifecycle, () => ({