diff --git a/.changeset/tricky-clouds-wait.md b/.changeset/tricky-clouds-wait.md new file mode 100644 index 0000000000..5a28f94fa7 --- /dev/null +++ b/.changeset/tricky-clouds-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Support parameter substitution for environment variables diff --git a/docs/conf/writing.md b/docs/conf/writing.md index 48104699c7..6e827c075e 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -199,6 +199,18 @@ configuration value will evaluate to `undefined`. The substitution syntax can be escaped using `$${...}`, which will be resolved as `${...}`. +Parameter substitution syntax (e.g. `${MY_VAR:-default-value}`) is also +supported to provide a default or fallback value for a given environment +variable if it is unset, or is declared but has no value. For example: + +```yaml +app: + baseUrl: https://${HOST:-localhost:3000} +``` + +In the above example, when `HOST` is unset or has no value, it will be +substituted with `localhost:3000`. + ## Combining Includes and Environment Variable Substitution The Includes and Environment Variable Substitutions can be combined to do diff --git a/packages/config-loader/src/sources/transform/substitution.test.ts b/packages/config-loader/src/sources/transform/substitution.test.ts index ff1c02174b..f7645c3cc4 100644 --- a/packages/config-loader/src/sources/transform/substitution.test.ts +++ b/packages/config-loader/src/sources/transform/substitution.test.ts @@ -55,6 +55,12 @@ describe('substituteTransform', () => { applied: true, value: 'hello my-secret', }); + await expect( + substituteTransform('hello ${SECRET:-default-secret}', { dir: '/' }), + ).resolves.toEqual({ + applied: true, + value: 'hello my-secret', + }); await expect( substituteTransform('${SECRET } $${} ${TOKEN }', { dir: '/' }), ).resolves.toEqual({ applied: true, value: 'my-secret ${} my-token' }); @@ -64,6 +70,24 @@ describe('substituteTransform', () => { applied: true, value: undefined, }); + await expect( + substituteTransform('foo ${MISSING:-}', { dir: '/' }), + ).resolves.toEqual({ + applied: true, + value: undefined, + }); + await expect( + substituteTransform('foo ${MISSING:-default-value}', { dir: '/' }), + ).resolves.toEqual({ + applied: true, + value: 'foo default-value', + }); + await expect( + substituteTransform('foo ${MISSING:-default:-value}', { dir: '/' }), + ).resolves.toEqual({ + applied: true, + value: 'foo default:-value', + }); await expect( substituteTransform('empty substitute ${}', { dir: '/' }), ).resolves.toEqual({ @@ -73,6 +97,9 @@ describe('substituteTransform', () => { await expect( substituteTransform('foo ${MISSING} ${SECRET}', { dir: '/' }), ).resolves.toEqual({ applied: true, value: undefined }); + await expect( + substituteTransform('foo ${MISSING:-} ${SECRET}', { dir: '/' }), + ).resolves.toEqual({ applied: true, value: undefined }); await expect( substituteTransform('foo ${SECRET} ${SECRET}', { dir: '/' }), ).resolves.toEqual({ applied: true, value: 'foo my-secret my-secret' }); diff --git a/packages/config-loader/src/sources/transform/substitution.ts b/packages/config-loader/src/sources/transform/substitution.ts index f10e5dc8e7..bc13100dd8 100644 --- a/packages/config-loader/src/sources/transform/substitution.ts +++ b/packages/config-loader/src/sources/transform/substitution.ts @@ -21,7 +21,9 @@ import { SubstitutionFunc } 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. + * the entire expression ends up undefined. Additionally, supports parameter substitution + * syntax to provide a default or fallback value for a given environment variable if it is + * unset; e.g. 'token ${MY_TOKEN:-xyz}' transforms to 'token xyz' if MY_TOKEN is unset. */ export function createSubstitutionTransform( env: SubstitutionFunc, @@ -37,7 +39,20 @@ export function createSubstitutionTransform( if (part.startsWith('$$')) { parts[i] = part.slice(1); } else { - parts[i] = await env(part.slice(2, -1).trim()); + const indexOfFallbackSeparator = part.indexOf(':-'); + + if (indexOfFallbackSeparator > -1) { + const envVarValue = await env( + part.slice(2, indexOfFallbackSeparator).trim(), + ); + const fallbackValue = part + .slice(indexOfFallbackSeparator + ':-'.length, -1) + .trim(); + + parts[i] = envVarValue || fallbackValue || undefined; + } else { + parts[i] = await env(part.slice(2, -1).trim()); + } } }