Change to readonly config value

Signed-off-by: Crevil <bjoern.soerensen@gmail.com>
This commit is contained in:
Crevil
2021-03-25 14:36:02 +01:00
parent 131ac14d5e
commit ddf37387b2
5 changed files with 33 additions and 34 deletions
+2 -3
View File
@@ -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`.
+8 -10
View File
@@ -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
@@ -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<EntitiesCatalog>;
let locationsCatalog: jest.Mocked<LocationsCatalog>;
let higherOrderOperation: jest.Mocked<HigherOrderOperation>;
@@ -356,7 +356,7 @@ describe('createRouter readwrite mode', () => {
});
});
describe('createRouter readonly mode', () => {
describe('createRouter readonly enabled', () => {
let entitiesCatalog: jest.Mocked<EntitiesCatalog>;
let locationsCatalog: jest.Mocked<LocationsCatalog>;
let higherOrderOperation: jest.Mocked<HigherOrderOperation>;
@@ -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 () => {
+14 -11
View File
@@ -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);
+3 -4
View File
@@ -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<unknown> {
const contentType = req.header('content-type');
@@ -56,8 +55,8 @@ export async function validateRequestBody<T>(
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');
}
}