diff --git a/packages/config-loader/src/lib/reader.test.ts b/packages/config-loader/src/lib/reader.test.ts index 719939ad7f..238a556061 100644 --- a/packages/config-loader/src/lib/reader.test.ts +++ b/packages/config-loader/src/lib/reader.test.ts @@ -26,6 +26,13 @@ function memoryFiles(files: { [path: string]: string }) { }; } +const mockContext: ReaderContext = { + env: {}, + skip: () => false, + readFile: jest.fn(), + readSecret: jest.fn(), +}; + describe('readConfigFile', () => { it('should read a plain config file', async () => { const readFile = memoryFiles({ @@ -34,8 +41,9 @@ describe('readConfigFile', () => { }); const config = readConfigFile('./app-config.yaml', { + ...mockContext, readFile, - } as ReaderContext); + }); await expect(config).resolves.toEqual({ data: { @@ -55,8 +63,9 @@ describe('readConfigFile', () => { }); const config = readConfigFile('./app-config.yaml', { + ...mockContext, readFile, - } as ReaderContext); + }); await expect(config).rejects.toThrow('Flow map contains an unexpected ]'); }); @@ -67,8 +76,9 @@ describe('readConfigFile', () => { }); const config = readConfigFile('./app-config.yaml', { + ...mockContext, readFile, - } as ReaderContext); + }); await expect(config).rejects.toThrow('Expected object at config root'); }); @@ -80,7 +90,7 @@ describe('readConfigFile', () => { const readSecret = jest.fn().mockResolvedValue('secret'); const config = readConfigFile('./app-config.yaml', { - env: {}, + ...mockContext, readFile, readSecret: readSecret as ReadSecretFunc, }); @@ -91,7 +101,9 @@ describe('readConfigFile', () => { }, context: 'app-config.yaml', }); - expect(readSecret).toHaveBeenCalledWith({ file: './my-secret' }); + expect(readSecret).toHaveBeenCalledWith('.app', { + file: './my-secret', + }); }); it('should require secrets to be objects', async () => { @@ -101,7 +113,7 @@ describe('readConfigFile', () => { const readSecret = jest.fn().mockResolvedValue('secret'); const config = readConfigFile('./app-config.yaml', { - env: {}, + ...mockContext, readFile, readSecret: readSecret as ReadSecretFunc, }); @@ -119,11 +131,29 @@ describe('readConfigFile', () => { const readSecret = jest.fn().mockRejectedValue(new Error('NOPE')); const config = readConfigFile('./app-config.yaml', { - env: {}, + ...mockContext, readFile, readSecret: readSecret as ReadSecretFunc, }); await expect(config).rejects.toThrow('Invalid secret at .app: NOPE'); }); + + it('should omit skipped values', async () => { + const readFile = memoryFiles({ + './app-config.yaml': 'app: { title: skip, name: include }', + }); + + const config = readConfigFile('./app-config.yaml', { + ...mockContext, + readFile, + skip: (path: string) => path === '.app.title', + readSecret: jest.fn() as ReadSecretFunc, + }); + + await expect(config).resolves.toEqual({ + context: 'app-config.yaml', + data: { app: { name: 'include' } }, + }); + }); }); diff --git a/packages/config-loader/src/lib/reader.ts b/packages/config-loader/src/lib/reader.ts index 584d0e0591..e2c9bebdf3 100644 --- a/packages/config-loader/src/lib/reader.ts +++ b/packages/config-loader/src/lib/reader.ts @@ -35,6 +35,10 @@ export async function readConfigFile( obj: JsonValue, path: string, ): Promise { + if (ctx.skip(path)) { + return undefined; + } + if (typeof obj !== 'object') { return obj; } else if (obj === null) { @@ -58,7 +62,7 @@ export async function readConfigFile( } try { - return await ctx.readSecret(obj.$secret); + return await ctx.readSecret(path, obj.$secret); } catch (error) { throw new Error(`Invalid secret at ${path}: ${error.message}`); } diff --git a/packages/config-loader/src/lib/secrets.test.ts b/packages/config-loader/src/lib/secrets.test.ts index e7fe997a58..fd95e527af 100644 --- a/packages/config-loader/src/lib/secrets.test.ts +++ b/packages/config-loader/src/lib/secrets.test.ts @@ -21,6 +21,7 @@ const ctx: ReaderContext = { env: { SECRET: 'my-secret', }, + skip: () => false, readSecret: jest.fn(), async readFile(path) { const content = ({ diff --git a/packages/config-loader/src/lib/types.ts b/packages/config-loader/src/lib/types.ts index c0f8b99dcf..02b8a9d053 100644 --- a/packages/config-loader/src/lib/types.ts +++ b/packages/config-loader/src/lib/types.ts @@ -17,13 +17,18 @@ import { JsonObject } from '@backstage/config'; export type ReadFileFunc = (path: string) => Promise; -export type ReadSecretFunc = (desc: JsonObject) => Promise; +export type ReadSecretFunc = ( + path: string, + desc: JsonObject, +) => Promise; +export type SkipFunc = (path: string) => boolean; /** * Common context that provides all the necessary hooks for reading configuration files. */ export type ReaderContext = { env: { [name in string]?: string }; + skip: SkipFunc; readFile: ReadFileFunc; readSecret: ReadSecretFunc; }; diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 0cc8ac29f5..679ab71e45 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -38,6 +38,7 @@ export type LoadConfigOptions = { class Context { constructor( private readonly options: { + secretPaths: Set; env: { [name in string]?: string }; rootPath: string; shouldReadSecrets: boolean; @@ -48,11 +49,22 @@ class Context { return this.options.env; } + skip(path: string): boolean { + if (this.options.shouldReadSecrets) { + return false; + } + return this.options.secretPaths.has(path); + } + async readFile(path: string): Promise { return fs.readFile(resolvePath(this.options.rootPath, path), 'utf8'); } - async readSecret(desc: JsonObject): Promise { + async readSecret( + path: string, + desc: JsonObject, + ): Promise { + this.options.secretPaths.add(path); if (!this.options.shouldReadSecrets) { return undefined; } @@ -69,10 +81,13 @@ export async function loadConfig( const configPaths = await resolveStaticConfig(options); try { + const secretPaths = new Set(); + for (const configPath of configPaths) { const config = await readConfigFile( configPath, new Context({ + secretPaths, env: process.env, rootPath: dirname(configPath), shouldReadSecrets: Boolean(options.shouldReadSecrets),