From 47952780392b522827707c6ffbcf5a58771c89c2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Nov 2020 14:05:56 +0100 Subject: [PATCH] config-loader: add config filtering by visibility --- packages/config-loader/src/lib/schema.ts | 97 +++++++++++++++++++++--- packages/config-loader/src/loader.ts | 2 +- 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/packages/config-loader/src/lib/schema.ts b/packages/config-loader/src/lib/schema.ts index c3441bce38..0d820ab173 100644 --- a/packages/config-loader/src/lib/schema.ts +++ b/packages/config-loader/src/lib/schema.ts @@ -19,14 +19,24 @@ import { resolve as resolvePath, dirname } from 'path'; import Ajv from 'ajv'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import mergeAllOf, { Resolvers } from 'json-schema-merge-allof'; -import { AppConfig, ConfigReader } from '@backstage/config'; +import { + AppConfig, + ConfigReader, + JsonObject, + JsonValue, +} from '@backstage/config'; const CONFIG_VISIBILITIES = ['frontend', 'backend', 'secret'] as const; type ConfigVisibility = typeof CONFIG_VISIBILITIES[number]; +const DEFAULT_CONFIG_VISIBILITY: ConfigVisibility = 'backend'; + type ConfigSchema = { - load(appConfigs: AppConfig[]): AppConfig[]; + process( + appConfigs: AppConfig[], + options?: ConfigProcessingOptions, + ): AppConfig[]; }; type ConfigSchemaPackageEntry = { @@ -42,11 +52,61 @@ type ValidationError = string; type ValidationResult = { errors?: ValidationError[]; - visibilities: Map; + visibilityByPath: Map; }; type ValidationFunc = (configs: AppConfig[]) => ValidationResult; +type ConfigProcessingOptions = { + visibilities?: ConfigVisibility[]; +}; + +export function filterByVisibility( + data: JsonObject, + includeVisibilities: ConfigVisibility[], + visibilityByPath: Map, +): JsonObject { + function transform(jsonVal: JsonValue, path: string): JsonValue | undefined { + if (typeof jsonVal !== 'object') { + const visibility = + visibilityByPath.get(path) ?? DEFAULT_CONFIG_VISIBILITY; + if (includeVisibilities.includes(visibility)) { + return jsonVal; + } + return undefined; + } else if (jsonVal === null) { + return undefined; + } else if (Array.isArray(jsonVal)) { + const arr = new Array(); + + for (const [index, value] of jsonVal.entries()) { + const out = transform(value, `${path}/${index}`); + if (out !== undefined) { + arr.push(out); + } + } + + return arr.length === 0 ? undefined : arr; + } + + const outObj: JsonObject = {}; + + for (const [key, value] of Object.entries(jsonVal)) { + if (value === undefined) { + continue; + } + const out = transform(value, `${path}/${key}`); + if (out !== undefined) { + outObj[key] = out; + } + } + + return Object.keys(outObj).length === 0 ? undefined : outObj; + } + + return (transform(data, '') as JsonObject) ?? {}; +} + export async function loadSchema(options: Options): Promise { const start = process.hrtime(); @@ -60,16 +120,28 @@ export async function loadSchema(options: Options): Promise { const validate = compileConfigSchemas(schemas); return { - load(configs: AppConfig[]): AppConfig[] { + process( + configs: AppConfig[], + { visibilities }: ConfigProcessingOptions = {}, + ): AppConfig[] { const result = validate(configs); - console.log('DEBUG: result =', result); if (result.errors) { throw new Error( `Config validation failed, ${result.errors.join('; ')}`, ); } - return configs; + let processedConfigs = configs; + + if (visibilities) { + processedConfigs = processedConfigs.map(({ data, context }) => ({ + context, + data: filterByVisibility(data, visibilities, result.visibilityByPath), + })); + } + console.log('DEBUG: result.visibilityByPath =', result.visibilityByPath); + + return processedConfigs; }, }; } @@ -77,7 +149,7 @@ export async function loadSchema(options: Options): Promise { function compileConfigSchemas( schemas: ConfigSchemaPackageEntry[], ): ValidationFunc { - const visibilities = new Map(); + const visibilityByPath = new Map(); const ajv = new Ajv({ strict: true, @@ -99,8 +171,8 @@ function compileConfigSchemas( if (!ctx) { return false; } - if (visibility === 'frontend') { - visibilities.set(ctx.dataPath, visibility); + if (visibility) { + visibilityByPath.set(ctx.dataPath, visibility); } return true; }; @@ -140,18 +212,19 @@ function compileConfigSchemas( return configs => { const config = ConfigReader.fromConfigs(configs).get(); - visibilities.clear(); + visibilityByPath.clear(); + const valid = validate(config); if (!valid) { const errors = ajv.errorsText(validate.errors); return { errors: [errors], - visibilities: new Map(), + visibilityByPath: new Map(), }; } return { - visibilities: new Map(visibilities), + visibilityByPath: new Map(visibilityByPath), }; }; } diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 318eaa7915..2d3c2afd9c 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -130,5 +130,5 @@ export async function loadConfig( configs.push(...readEnvConfig(process.env)); - return schema.load(configs); + return schema.process(configs, { visibilities: ['frontend', 'secret'] }); }