diff --git a/.changeset/short-geese-double.md b/.changeset/short-geese-double.md new file mode 100644 index 0000000000..1c63321be7 --- /dev/null +++ b/.changeset/short-geese-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +New config command to export the configuration schema. When running backstage-cli with yarn, consider using `yarn --silent backstage-cli config:schema` to get a clean output on `stdout`. diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 5a68690615..46cca46f78 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -46,6 +46,7 @@ remove-plugin Removes plugin in the current repository config:print Print the app configuration for the current package config:check Validate that the given configuration loads and matches schema +config:schema Dump the app configuration schema versions:bump Bump Backstage packages to the latest versions versions:check Check Backstage package versioning @@ -484,6 +485,27 @@ Options: -h, --help display help for command ``` +## config:schema + +Scope: `root` + +Dump the configuration schema that was collected from all local packages in the +repo. + +Note: when run by `yarn`, supply the yarn option `--silent` if you are using the +output in a command line pipe to avoid non schema output in the pipeline. + +```text +Usage: backstage-cli config:schema [options] + +Print configuration schema + +Options: + --package <name> Only output config schema that applies to the given package + -h, --help display help for command + +``` + ## versions:bump Scope: `root` diff --git a/packages/cli/package.json b/packages/cli/package.json index d0f78c90a2..d34bd967f4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -82,6 +82,7 @@ "inquirer": "^7.0.4", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", + "json-schema": "^0.2.5", "lodash": "^4.17.19", "mini-css-extract-plugin": "^0.9.0", "ora": "^4.0.3", diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/commands/config/schema.ts new file mode 100644 index 0000000000..63c1524789 --- /dev/null +++ b/packages/cli/src/commands/config/schema.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Command } from 'commander'; +import { JSONSchema7 as JSONSchema } from 'json-schema'; +import { stringify as stringifyYaml } from 'yaml'; +import { loadCliConfig } from '../../lib/config'; +import { JsonObject } from '@backstage/config'; +import { mergeConfigSchemas } from '@backstage/config-loader'; + +export default async (cmd: Command) => { + const { schema } = await loadCliConfig({ + args: [], + fromPackage: cmd.package, + mockEnv: true, + }); + + const merged = mergeConfigSchemas( + (schema.serialize().schemas as JsonObject[]).map( + _ => _.value as JSONSchema, + ), + ); + + merged.title = 'Application Configuration Schema'; + merged.description = + 'This is the schema describing the structure of the app-config.yaml configuration file.'; + + if (cmd.format === 'json') { + process.stdout.write(`${JSON.stringify(merged, null, 2)}\n`); + } else { + process.stdout.write(`${stringifyYaml(merged)}\n`); + } +}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index e058aaf696..3a8ef36a97 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -170,6 +170,15 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./config/validate').then(m => m.default))); + program + .command('config:schema') + .option( + '--package ', + 'Only output config schema that applies to the given package', + ) + .description('Print configuration schema') + .action(lazy(() => import('./config/schema').then(m => m.default))); + program .command('versions:bump') .description('Bump Backstage packages to the latest versions') diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index b49b644e25..de6bd6353d 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -48,8 +48,10 @@ export async function loadCliConfig(options: Options) { configPaths, }); - console.log( - `Loaded config from ${appConfigs.map(c => c.context).join(', ')}`, + // printing to stderr to not clobber stdout in case the cli command + // outputs structured data (e.g. as config:schema does) + process.stderr.write( + `Loaded config from ${appConfigs.map(c => c.context).join(', ')}\n`, ); try { diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index b905d3e8a4..07fd22d7ba 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -32,6 +32,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.1", + "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", "fs-extra": "^9.0.0", "json-schema": "^0.2.5", @@ -42,7 +43,6 @@ }, "devDependencies": { "@types/jest": "^26.0.7", - "@types/json-schema": "^7.0.6", "@types/json-schema-merge-allof": "^0.6.0", "@types/mock-fs": "^4.10.0", "@types/node": "^12.0.0", diff --git a/packages/config-loader/src/index.ts b/packages/config-loader/src/index.ts index 9ad54c5f18..d9e5ae1350 100644 --- a/packages/config-loader/src/index.ts +++ b/packages/config-loader/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { readEnvConfig, loadConfigSchema } from './lib'; +export { readEnvConfig, loadConfigSchema, mergeConfigSchemas } from './lib'; export type { ConfigSchema, ConfigVisibility } from './lib'; export { loadConfig } from './loader'; export type { LoadConfigOptions } from './loader'; diff --git a/packages/config-loader/src/lib/schema/compile.ts b/packages/config-loader/src/lib/schema/compile.ts index e340d775a1..e85b6023cc 100644 --- a/packages/config-loader/src/lib/schema/compile.ts +++ b/packages/config-loader/src/lib/schema/compile.ts @@ -77,8 +77,41 @@ export function compileConfigSchemas( } } + const merged = mergeConfigSchemas(schemas.map(_ => _.value)); + const validate = ajv.compile(merged); + + return configs => { + const config = ConfigReader.fromConfigs(configs).get(); + + visibilityByPath.clear(); + + const valid = validate(config); + if (!valid) { + const errors = validate.errors ?? []; + return { + errors: errors.map(({ dataPath, message, params }) => { + const paramStr = Object.entries(params) + .map(([name, value]) => `${name}=${value}`) + .join(' '); + return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; + }), + visibilityByPath: new Map(), + }; + } + + return { + visibilityByPath: new Map(visibilityByPath), + }; + }; +} + +/** + * Given a list of configuration schemas from packages, merge them + * into a single json schema. + */ +export function mergeConfigSchemas(schemas: JSONSchema[]): JSONSchema { const merged = mergeAllOf( - { allOf: schemas.map(_ => _.value) }, + { allOf: schemas }, { // JSONSchema is typically subtractive, as in it always reduces the set of allowed // inputs through constraints. This changes the object property merging to be additive @@ -107,30 +140,5 @@ export function compileConfigSchemas( } as Partial>, }, ); - - const validate = ajv.compile(merged); - - return configs => { - const config = ConfigReader.fromConfigs(configs).get(); - - visibilityByPath.clear(); - - const valid = validate(config); - if (!valid) { - const errors = validate.errors ?? []; - return { - errors: errors.map(({ dataPath, message, params }) => { - const paramStr = Object.entries(params) - .map(([name, value]) => `${name}=${value}`) - .join(' '); - return `Config ${message || ''} { ${paramStr} } at ${dataPath}`; - }), - visibilityByPath: new Map(), - }; - } - - return { - visibilityByPath: new Map(visibilityByPath), - }; - }; + return merged; } diff --git a/packages/config-loader/src/lib/schema/index.ts b/packages/config-loader/src/lib/schema/index.ts index 8cefb93b3c..00bb5c7d10 100644 --- a/packages/config-loader/src/lib/schema/index.ts +++ b/packages/config-loader/src/lib/schema/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export { mergeConfigSchemas } from './compile'; export { loadConfigSchema } from './load'; export type { ConfigSchema, ConfigVisibility } from './types';