diff --git a/.changeset/fast-wasps-peel.md b/.changeset/fast-wasps-peel.md new file mode 100644 index 0000000000..075336a506 --- /dev/null +++ b/.changeset/fast-wasps-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Enable strict config checking during `backstage-cli config:check` with the new `--strict` option which will surface schema errors. diff --git a/.changeset/late-lizards-unite.md b/.changeset/late-lizards-unite.md new file mode 100644 index 0000000000..bf0a03de12 --- /dev/null +++ b/.changeset/late-lizards-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config. diff --git a/.changeset/shy-worms-enjoy.md b/.changeset/shy-worms-enjoy.md new file mode 100644 index 0000000000..cbfb2b5f7b --- /dev/null +++ b/.changeset/shy-worms-enjoy.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-user-settings': patch +'@backstage/plugin-auth-backend': patch +--- + +Fix config schema definition. diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index c4c057ffe6..66a1855fca 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -290,7 +290,8 @@ Options: --package Only load config schema that applies to the given package --lax Do not require environment variables to be set --frontend Only validate the frontend configuration - --deprecated List all deprecated configuration settings + --deprecated Output deprecated configuration settings + --strict Ensure that the provided config(s) has no errors and does not contain keys not in the schema. --config Config files to load instead of app-config.yaml (default: []) -h, --help display help for command ``` diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index bad849d283..39bee2ed05 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -58,6 +58,7 @@ Options: --lax --frontend --deprecated + --strict --config -h, --help ``` diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts index c3d05291e0..c81d82832e 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/commands/config/validate.ts @@ -24,5 +24,6 @@ export default async (opts: OptionValues) => { mockEnv: opts.lax, fullVisibility: !opts.frontend, withDeprecatedKeys: opts.deprecated, + strict: opts.strict, }); }; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index ba11a53cfd..351616fc42 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -345,6 +345,10 @@ export function registerCommands(program: Command) { .option('--lax', 'Do not require environment variables to be set') .option('--frontend', 'Only validate the frontend configuration') .option('--deprecated', 'Output deprecated configuration settings') + .option( + '--strict', + 'Enable strict config validation, forbidding errors and unknown errors', + ) .option(...configOption) .description( 'Validate that the given configuration loads and matches schema', diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b34e1280c0..aaf3fcfcb9 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -32,6 +32,7 @@ type Options = { withFilteredKeys?: boolean; withDeprecatedKeys?: boolean; fullVisibility?: boolean; + strict?: boolean; }; export async function loadCliConfig(options: Options) { @@ -70,6 +71,7 @@ export async function loadCliConfig(options: Options) { dependencies: localPackageNames, // Include the package.json in the project root if it exists packagePaths: [paths.resolveTargetRoot('package.json')], + noUndeclaredProperties: options.strict, }); const { appConfigs } = await loadConfig({ @@ -93,6 +95,7 @@ export async function loadCliConfig(options: Options) { : ['frontend'], withFilteredKeys: options.withFilteredKeys, withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, }); const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs); diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 1cc353fd4c..8cd469d4d7 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -180,14 +180,17 @@ export function loadConfigSchema( ): Promise; // @public -export type LoadConfigSchemaOptions = +export type LoadConfigSchemaOptions = ( | { dependencies: string[]; packagePaths?: string[]; } | { serialized: JsonObject; - }; + } +) & { + noUndeclaredProperties?: boolean; +}; // @public export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7; diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts index d193e7fbf2..f9e1a3bf76 100644 --- a/packages/config-loader/src/schema/compile.ts +++ b/packages/config-loader/src/schema/compile.ts @@ -25,6 +25,7 @@ import { CONFIG_VISIBILITIES, ConfigVisibility, } from './types'; +import { SchemaObject } from 'json-schema-traverse'; /** * This takes a collection of Backstage configuration schemas from various @@ -35,6 +36,9 @@ import { */ export function compileConfigSchemas( schemas: ConfigSchemaPackageEntry[], + options?: { + noUndeclaredProperties?: boolean; + }, ): ValidationFunc { // The ajv instance below is stateful and doesn't really allow for additional // output during validation. We work around this by having this extra piece @@ -102,6 +106,18 @@ export function compileConfigSchemas( const merged = mergeConfigSchemas(schemas.map(_ => _.value)); + if (options?.noUndeclaredProperties) { + traverse(merged, (schema: SchemaObject) => { + /** + * The `additionalProperties` key can only be applied to `type: object` in the JSON + * schema. + */ + if (schema?.type === 'object') { + schema.additionalProperties ||= false; + } + }); + } + const validate = ajv.compile(merged); const visibilityBySchemaPath = new Map(); diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts index 0309f64ea0..1a96305ae0 100644 --- a/packages/config-loader/src/schema/load.ts +++ b/packages/config-loader/src/schema/load.ts @@ -32,12 +32,16 @@ import { * @public */ export type LoadConfigSchemaOptions = - | { - dependencies: string[]; - packagePaths?: string[]; - } - | { - serialized: JsonObject; + | ( + | { + dependencies: string[]; + packagePaths?: string[]; + } + | { + serialized: JsonObject; + } + ) & { + noUndeclaredProperties?: boolean; }; function errorsToError(errors: ValidationError[]): Error { @@ -77,7 +81,9 @@ export async function loadConfigSchema( schemas = serialized.schemas as ConfigSchemaPackageEntry[]; } - const validate = compileConfigSchemas(schemas); + const validate = compileConfigSchemas(schemas, { + noUndeclaredProperties: options.noUndeclaredProperties, + }); return { process( diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 8d76ca8bf5..c6579e08f8 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -59,6 +59,7 @@ export interface Config { /** * The available auth-provider options and attributes + * @additionalProperties true */ providers?: { google?: { diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index e3967edc53..98d688bcda 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -78,7 +78,8 @@ "type": "object", "additionalProperties": { "type": "object", - "visibility": "frontend" + "visibility": "frontend", + "additionalProperties": true } } }