diff --git a/.changeset/rich-birds-reply.md b/.changeset/rich-birds-reply.md index fb1337dace..6e4755ca86 100644 --- a/.changeset/rich-birds-reply.md +++ b/.changeset/rich-birds-reply.md @@ -1,14 +1,13 @@ --- -'example-backend': minor '@backstage/plugin-catalog-backend': minor --- Add `readonly` mode to catalog backend -This change adds a `catalog.mode` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibility of adding new components to the catalog after startup. +This change adds a `catalog.readonly` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibility of adding new components to the catalog after startup. When in `readonly` mode only locations configured in `catalog.locations` are loaded and served. -By default the mode is `readwrite` which represents the current functionality where locations can be added at run-time. +By default `readonly` is disabled which represents the current functionality where locations can be added at run-time. This change requires the config API in the router which requires a change to `createRouter`. diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index aced6c3856..564dd7511e 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -16,8 +16,6 @@ import { JsonValue } from '@backstage/config'; -export type Mode = 'readonly' | 'readwrite'; - export interface Config { /** * Configuration options for the catalog plugin. @@ -45,18 +43,18 @@ export interface Config { }>; /** - * Mode defines the overall behaviour mode of the catalog. + * Readonly defines whether the catalog allows writes after startup. * - * Setting the mode to 'readwrite' you allow users to register their own - * components. This is the default value. + * Setting 'readonly=false' allows users to register their own components. + * This is the default value. * - * Setting the mode to 'readonly' configures catalog to only allow reads. - * This can be used in combination with static locations to only serve - * operator provided locations. Effectively this removes the ability to - * register new components to a running backstage instance. + * Setting 'readonly=true' configures catalog to only allow reads. This can + * be used in combination with static locations to only serve operator + * provided locations. Effectively this removes the ability to register new + * components to a running backstage instance. * */ - mode?: Mode; + readonly?: boolean; /** * A set of static locations that the catalog shall always keep itself diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 72ce61a126..eb072f04d2 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -26,7 +26,7 @@ import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; import { basicEntityFilter } from './request'; -describe('createRouter readwrite mode', () => { +describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let higherOrderOperation: jest.Mocked; @@ -356,7 +356,7 @@ describe('createRouter readwrite mode', () => { }); }); -describe('createRouter readonly mode', () => { +describe('createRouter readonly enabled', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let higherOrderOperation: jest.Mocked; @@ -388,7 +388,7 @@ describe('createRouter readonly mode', () => { logger: getVoidLogger(), config: new ConfigReader({ catalog: { - mode: 'readonly', + readonly: true, }, }), }); @@ -435,7 +435,7 @@ describe('createRouter readonly mode', () => { expect(entitiesCatalog.batchAddOrUpdateEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); }); @@ -444,7 +444,7 @@ describe('createRouter readonly mode', () => { const response = await request(app).delete('/entities/by-uid/apa'); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); }); @@ -476,7 +476,7 @@ describe('createRouter readonly mode', () => { expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); it('supports dry run', async () => { diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 51b1c20012..4c93b27640 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -28,7 +28,6 @@ import { Logger } from 'winston'; import yn from 'yn'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { HigherOrderOperation, LocationAnalyzer } from '../ingestion/types'; -import { Mode } from '../../config'; import { basicEntityFilter, parseEntityFilterParams, @@ -36,7 +35,7 @@ import { parseEntityTransformParams, } from './request'; import { - requireReadWriteMode, + disallowReadonlyMode, requireRequestBody, validateRequestBody, } from './util'; @@ -65,9 +64,11 @@ export async function createRouter( const router = Router(); router.use(express.json()); - const mode: Mode = - (config.getOptionalString('catalog.mode') as Mode) || 'readwrite'; - logger.info(`Catalog is running in ${mode} mode`); + const readonlyEnabled = + config.getOptionalBoolean('catalog.readonly') || false; + if (readonlyEnabled) { + logger.info('Catalog is running in readonly mode'); + } if (entitiesCatalog) { router @@ -100,7 +101,7 @@ export async function createRouter( * It stays around in the service for the time being, but may be * removed or change semantics at any time without prior notice. */ - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const body = await requireRequestBody(req); const [result] = await entitiesCatalog.batchAddOrUpdateEntities([ @@ -122,7 +123,7 @@ export async function createRouter( res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); @@ -151,9 +152,11 @@ export async function createRouter( const input = await validateRequestBody(req, locationSpecSchema); const dryRun = yn(req.query.dryRun, { default: false }); - // when in dryRun addLocation is effectively a read operation so when in - // dryRun we override mode to readwrite to allow the operation - requireReadWriteMode(dryRun ? 'readwrite' : mode); + // when in dryRun addLocation is effectively a read operation so we don't + // need to disallow readonly + if (!dryRun) { + disallowReadonlyMode(readonlyEnabled); + } const output = await higherOrderOperation.addLocation(input, { dryRun }); res.status(201).json(output); @@ -177,7 +180,7 @@ export async function createRouter( res.status(200).json(output); }) .delete('/locations/:id', async (req, res) => { - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const { id } = req.params; await locationsCatalog.removeLocation(id); diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 60e8c48984..9cc5ba1b52 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -18,7 +18,6 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { Request } from 'express'; import lodash from 'lodash'; import yup from 'yup'; -import { Mode } from '../../config'; export async function requireRequestBody(req: Request): Promise { const contentType = req.header('content-type'); @@ -56,8 +55,8 @@ export async function validateRequestBody( return (body as unknown) as T; } -export function requireReadWriteMode(mode: Mode) { - if (mode !== 'readwrite') { - throw new NotAllowedError('This operation requires readwrite mode'); +export function disallowReadonlyMode(readonly: boolean) { + if (readonly) { + throw new NotAllowedError('This operation not allowed in readonly mode'); } }