diff --git a/.changeset/metal-ravens-hammer.md b/.changeset/metal-ravens-hammer.md new file mode 100644 index 0000000000..e8a97f412c --- /dev/null +++ b/.changeset/metal-ravens-hammer.md @@ -0,0 +1,6 @@ +--- +'@backstage/config-loader': patch +'@backstage/backend-defaults': patch +--- + +Exclude `@backstage/backend-common` from schema collection if `@backstage/backend-defaults` is present diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index e2a748f85d..aa97313921 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -86,6 +86,19 @@ export interface Config { * Options used by the default auth, httpAuth and userInfo services. */ auth?: { + /** + * Keys shared by all backends for signing and validating backend tokens. + * @deprecated this will be removed when the backwards compatibility is no longer needed with backend-common + */ + keys?: { + /** + * Secret for generating tokens. Should be a base64 string, recommended + * length is 24 bytes. + * + * @visibility secret + */ + secret: string; + }[]; /** * This disables the otherwise default auth policy, which requires all * requests to be authenticated with either user or service credentials. diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index 8c6d1c6b90..53db5c6da7 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -68,6 +68,7 @@ describe('collectConfigSchemas', () => { { path: path.join('node_modules', 'a', 'package.json'), value: mockSchema, + packageName: 'a', }, ]); }); @@ -89,6 +90,62 @@ describe('collectConfigSchemas', () => { { path: path.join('root', 'package.json'), value: mockSchema, + packageName: 'root', + }, + ]); + }); + + it('should not include schemas for backend-common if theres a backend-defaults package', async () => { + mockDir.setContent({ + root: { + 'package.json': JSON.stringify({ + name: 'root', + dependencies: { + '@backstage/backend-common': '1', + '@backstage/backend-defaults': '1', + }, + configSchema: { ...mockSchema, title: 'root' }, + }), + }, + node_modules: { + '@backstage': { + 'backend-common': { + 'package.json': JSON.stringify({ + name: '@backstage/backend-common', + version: '1', + configSchema: { ...mockSchema, title: 'backend-common' }, + }), + }, + 'backend-defaults': { + 'package.json': JSON.stringify({ + name: '@backstage/backend-defaults', + version: '1', + configSchema: { ...mockSchema, title: 'backend-defaults' }, + }), + }, + }, + }, + }); + + process.chdir(mockDir.path); + + await expect( + collectConfigSchemas(['root'], [path.join('root', 'package.json')]), + ).resolves.toEqual([ + { + path: path.join('root', 'package.json'), + value: { ...mockSchema, title: 'root' }, + packageName: 'root', + }, + { + path: path.join( + 'node_modules', + '@backstage', + 'backend-defaults', + 'package.json', + ), + value: { ...mockSchema, title: 'backend-defaults' }, + packageName: '@backstage/backend-defaults', }, ]); }); @@ -159,18 +216,22 @@ describe('collectConfigSchemas', () => { { path: path.join('node_modules', 'b', 'package.json'), value: { ...mockSchema, title: 'b' }, + packageName: 'b', }, { path: path.join('node_modules', 'c1', 'package.json'), value: { ...mockSchema, title: 'c1' }, + packageName: 'c1', }, { path: path.join('node_modules', 'd1', 'package.json'), value: { ...mockSchema, title: 'd1' }, + packageName: 'd1', }, { path: path.join('root', 'package.json'), value: { ...mockSchema, title: 'root' }, + packageName: 'root', }, ]), ); @@ -213,10 +274,12 @@ describe('collectConfigSchemas', () => { { path: path.join('node_modules', 'a', 'package.json'), value: { ...mockSchema, title: 'inline' }, + packageName: 'a', }, { path: path.join('node_modules', 'b', 'schema.json'), value: { ...mockSchema, title: 'external' }, + packageName: 'b', }, { path: path.join('node_modules', 'c', 'schema.d.ts'), @@ -231,6 +294,7 @@ describe('collectConfigSchemas', () => { }, required: ['tsKey'], }, + packageName: 'c', }, ]), ); @@ -284,14 +348,17 @@ describe('collectConfigSchemas', () => { { path: path.join('node_modules', 'a', 'package.json'), value: mockSchema, + packageName: 'a', }, { path: path.join('node_modules', 'b', 'package.json'), value: { ...mockSchema, title: 'b' }, + packageName: 'b', }, { path: path.join('node_modules', 'c', 'package.json'), value: { ...mockSchema, title: 'c1' }, + packageName: 'c', }, { path: path.join( @@ -302,6 +369,7 @@ describe('collectConfigSchemas', () => { 'package.json', ), value: { ...mockSchema, title: 'c2' }, + packageName: 'c', }, ]), ); diff --git a/packages/config-loader/src/schema/collect.ts b/packages/config-loader/src/schema/collect.ts index 7120c12d90..c7af64e0f3 100644 --- a/packages/config-loader/src/schema/collect.ts +++ b/packages/config-loader/src/schema/collect.ts @@ -45,7 +45,7 @@ export async function collectConfigSchemas( packagePaths: string[], ): Promise { const schemas = new Array(); - const tsSchemaPaths = new Array(); + const tsSchemaPaths = new Array<{ packageName: string; path: string }>(); const visitedPackageVersions = new Map>(); // pkgName: [versions...] const currentDir = await fs.realpath(process.cwd()); @@ -115,22 +115,25 @@ export async function collectConfigSchemas( ); } if (isDts) { - tsSchemaPaths.push( - relativePath( + tsSchemaPaths.push({ + path: relativePath( currentDir, resolvePath(dirname(pkgPath), pkg.configSchema), ), - ); + packageName: pkg.name, + }); } else { const path = resolvePath(dirname(pkgPath), pkg.configSchema); const value = await fs.readJson(path); schemas.push({ + packageName: pkg.name, value, path: relativePath(currentDir, path), }); } } else { schemas.push({ + packageName: pkg.name, value: pkg.configSchema, path: relativePath(currentDir, pkgPath), }); @@ -150,15 +153,31 @@ export async function collectConfigSchemas( ]); const tsSchemas = await compileTsSchemas(tsSchemaPaths); + const allSchemas = schemas.concat(tsSchemas); - return schemas.concat(tsSchemas); + const hasBackendDefaults = allSchemas.some( + ({ packageName }) => packageName === '@backstage/backend-defaults', + ); + + if (hasBackendDefaults) { + // We filter out backend-common schemas here to avoid issues with + // schema merging over different versions of the same schema. + // led to issues such as https://github.com/backstage/backstage/issues/28170 + return allSchemas.filter( + ({ packageName }) => packageName !== '@backstage/backend-common', + ); + } + + return allSchemas; } // This handles the support of TypeScript .d.ts config schema declarations. // We collect all typescript schema definition and compile them all in one go. // This is much faster than compiling them separately. -async function compileTsSchemas(paths: string[]) { - if (paths.length === 0) { +async function compileTsSchemas( + entries: { path: string; packageName: string }[], +) { + if (entries.length === 0) { return []; } @@ -168,20 +187,23 @@ async function compileTsSchemas(paths: string[]) { 'typescript-json-schema' ); - const program = getProgramFromFiles(paths, { - incremental: false, - isolatedModules: true, - lib: ['ES5'], // Skipping most libs speeds processing up a lot, we just need the primitive types anyway - noEmit: true, - noResolve: true, - skipLibCheck: true, // Skipping lib checks speeds things up - skipDefaultLibCheck: true, - strict: true, - typeRoots: [], // Do not include any additional types - types: [], - }); + const program = getProgramFromFiles( + entries.map(({ path }) => path), + { + incremental: false, + isolatedModules: true, + lib: ['ES5'], // Skipping most libs speeds processing up a lot, we just need the primitive types anyway + noEmit: true, + noResolve: true, + skipLibCheck: true, // Skipping lib checks speeds things up + skipDefaultLibCheck: true, + strict: true, + typeRoots: [], // Do not include any additional types + types: [], + }, + ); - const tsSchemas = paths.map(path => { + const tsSchemas = entries.map(({ path, packageName }) => { let value; try { const generator = buildGenerator( @@ -228,7 +250,7 @@ async function compileTsSchemas(paths: string[]) { if (!value) { throw new Error(`Invalid schema in ${path}, missing Config export`); } - return { path, value }; + return { path, value, packageName }; }); return tsSchemas; diff --git a/packages/config-loader/src/schema/compile.test.ts b/packages/config-loader/src/schema/compile.test.ts index ff0a9543aa..b39d2537d1 100644 --- a/packages/config-loader/src/schema/compile.test.ts +++ b/packages/config-loader/src/schema/compile.test.ts @@ -21,10 +21,12 @@ describe('compileConfigSchemas', () => { const validate = compileConfigSchemas([ { path: 'a', + packageName: 'a', value: { type: 'object', properties: { a: { type: 'string' } } }, }, { path: 'b', + packageName: 'b', value: { type: 'object', properties: { b: { type: 'number' } } }, }, ]); @@ -64,6 +66,7 @@ describe('compileConfigSchemas', () => { const validate = compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -80,6 +83,7 @@ describe('compileConfigSchemas', () => { }, { path: 'a2', + packageName: 'a2', value: { type: 'object', properties: { @@ -126,6 +130,7 @@ describe('compileConfigSchemas', () => { compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { a: { type: 'string', visibility: 'frontend' } }, @@ -133,6 +138,7 @@ describe('compileConfigSchemas', () => { }, { path: 'a2', + packageName: 'a2', value: { type: 'object', properties: { a: { type: 'string', visibility: 'secret' } }, @@ -148,6 +154,7 @@ describe('compileConfigSchemas', () => { const validate = compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -179,6 +186,7 @@ describe('compileConfigSchemas', () => { const validate = compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -242,6 +250,7 @@ describe('deepVisibility', () => { const validate = compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -257,6 +266,7 @@ describe('deepVisibility', () => { }, { path: 'a2', + packageName: 'a2', value: { type: 'object', deepVisibility: 'secret', @@ -305,6 +315,7 @@ describe('deepVisibility', () => { compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -320,6 +331,7 @@ describe('deepVisibility', () => { }, { path: 'a2', + packageName: 'a2', value: { type: 'object', deepVisibility: 'secret', @@ -346,6 +358,7 @@ describe('deepVisibility', () => { compileConfigSchemas([ { path: 'a2', + packageName: 'a2', value: { type: 'object', deepVisibility: 'secret', @@ -376,6 +389,7 @@ describe('deepVisibility', () => { compileConfigSchemas([ { path: 'a2', + packageName: 'a2', value: { type: 'object', properties: { @@ -398,6 +412,7 @@ describe('deepVisibility', () => { compileConfigSchemas([ { path: 'a1', + packageName: 'a1', value: { type: 'object', properties: { @@ -418,6 +433,7 @@ describe('deepVisibility', () => { }, { path: 'a2', + packageName: 'a2', value: { type: 'object', deepVisibility: 'secret', diff --git a/packages/config-loader/src/schema/types.ts b/packages/config-loader/src/schema/types.ts index 8f679c93fc..4ba65245da 100644 --- a/packages/config-loader/src/schema/types.ts +++ b/packages/config-loader/src/schema/types.ts @@ -29,6 +29,10 @@ export type ConfigSchemaPackageEntry = { * The relative path that the configuration schema was discovered at. */ path: string; + /** + * The package name for the package this belongs to + */ + packageName: string; }; /**