From f79938abe6ef4c86b696c6772a8ea6c81caef48a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 24 Jan 2021 01:05:52 +0100 Subject: [PATCH] config-loader: add substitution transform --- .../config-loader/src/lib/transform/index.ts | 1 + .../src/lib/transform/substitution.test.ts | 68 +++++++++++++++++++ .../src/lib/transform/substitution.ts | 40 +++++++++++ packages/config-loader/src/loader.test.ts | 13 ++++ packages/config-loader/src/loader.ts | 2 + 5 files changed, 124 insertions(+) create mode 100644 packages/config-loader/src/lib/transform/substitution.test.ts create mode 100644 packages/config-loader/src/lib/transform/substitution.ts diff --git a/packages/config-loader/src/lib/transform/index.ts b/packages/config-loader/src/lib/transform/index.ts index 9f1026eea6..cb9f077d43 100644 --- a/packages/config-loader/src/lib/transform/index.ts +++ b/packages/config-loader/src/lib/transform/index.ts @@ -16,3 +16,4 @@ export { applyConfigTransforms } from './apply'; export { createIncludeTransform } from './include'; +export { createSubstitutionTransform } from './substitution'; diff --git a/packages/config-loader/src/lib/transform/substitution.test.ts b/packages/config-loader/src/lib/transform/substitution.test.ts new file mode 100644 index 0000000000..b6795dbfc4 --- /dev/null +++ b/packages/config-loader/src/lib/transform/substitution.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2020 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 { createSubstitutionTransform } from './substitution'; + +const env = jest.fn(async (name: string) => { + return ({ + SECRET: 'my-secret', + TOKEN: 'my-token', + } as { [name: string]: string })[name]; +}); + +const substituteTransform = createSubstitutionTransform(env); + +describe('substituteTransform', () => { + it('should not transform unknown values', async () => { + await expect(substituteTransform(false)).resolves.toEqual([ + false, + expect.anything(), + ]); + await expect(substituteTransform([1])).resolves.toEqual([ + false, + expect.anything(), + ]); + await expect(substituteTransform(1)).resolves.toEqual([ + false, + expect.anything(), + ]); + await expect(substituteTransform({ x: 'y' })).resolves.toEqual([ + false, + expect.anything(), + ]); + await expect(substituteTransform(null)).resolves.toEqual([false, null]); + }); + + it('should substitute env var', async () => { + await expect(substituteTransform('hello ${SECRET}')).resolves.toEqual([ + true, + 'hello my-secret', + ]); + await expect( + substituteTransform('${SECRET } $${} ${TOKEN }'), + ).resolves.toEqual([true, 'my-secret $${} my-token']); + await expect(substituteTransform('foo ${MISSING}')).resolves.toEqual([ + true, + undefined, + ]); + await expect( + substituteTransform('foo ${MISSING} ${SECRET}'), + ).resolves.toEqual([true, undefined]); + await expect( + substituteTransform('foo ${SECRET} ${SECRET}'), + ).resolves.toEqual([true, 'foo my-secret my-secret']); + }); +}); diff --git a/packages/config-loader/src/lib/transform/substitution.ts b/packages/config-loader/src/lib/transform/substitution.ts new file mode 100644 index 0000000000..d1336edd4c --- /dev/null +++ b/packages/config-loader/src/lib/transform/substitution.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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 { JsonValue } from '@backstage/config'; +import { TransformFunc, EnvFunc } from './types'; + +/** + * A environment variable substitution transform that transforms e.g. 'token ${MY_TOKEN}' + * to 'token abc' if MY_TOKEN is 'abc'. If any of the substituted variables are undefined, + * the entire expression ends up undefined. + */ +export function createSubstitutionTransform(env: EnvFunc): TransformFunc { + return async (input: JsonValue) => { + if (typeof input !== 'string') { + return [false, input]; + } + + const parts: (string | undefined)[] = input.split(/(? part === undefined)) { + return [true, undefined]; + } + return [true, parts.join('')]; + }; +} diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index a9857a784f..bb6262ce13 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -19,6 +19,8 @@ import mockFs from 'mock-fs'; describe('loadConfig', () => { beforeAll(() => { + process.env.MY_SECRET = 'is-secret'; + mockFs({ '/root/app-config.yaml': ` app: @@ -29,8 +31,14 @@ describe('loadConfig', () => { '/root/app-config.development.yaml': ` app: sessionKey: development-key + backend: + $include: ./included.yaml `, '/root/secrets/session-key.txt': 'abc123', + '/root/included.yaml': ` + foo: + bar: token \${MY_SECRET} + `, }); }); @@ -104,6 +112,11 @@ describe('loadConfig', () => { app: { sessionKey: 'development-key', }, + backend: { + foo: { + bar: 'token is-secret', + }, + }, }, }, ]); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 71553a3dd7..34437dcf75 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -22,6 +22,7 @@ import { applyConfigTransforms, readEnvConfig, createIncludeTransform, + createSubstitutionTransform, } from './lib'; export type LoadConfigOptions = { @@ -78,6 +79,7 @@ export async function loadConfig( const input = yaml.parse(await readFile(configPath)); const data = await applyConfigTransforms(input, [ createIncludeTransform(env, readFile), + createSubstitutionTransform(env), ]); configs.push({ data, context: basename(configPath) });