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(), })); }