From 174b6950eb7a4f4ad1d5f321940310178297122e Mon Sep 17 00:00:00 2001 From: Fabian Reinbold Date: Thu, 1 Aug 2024 21:09:30 +0200 Subject: [PATCH 1/3] [FEATURE] Add support to autofill env-vars in config - Environment variables should be autofilled when the config is loaded Fixes: #935 --- lib/graph/Module.js | 49 +++++++++++++++++++++++++++++++++++ test/lib/graph/Module.js | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 9609a49d4..52c7bedfc 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -27,6 +27,16 @@ function clone(obj) { * @alias @ui5/project/graph/Module */ class Module { + /** + * Regular expression to identify environment variables in strings. + * Environment variables have to be prefixed with "env:", e.g. "env:Path". + * + * @private + * @static + * @readonly + */ + static _ENVIRONMENT_VARIABLE_REGEX = /env:\S+/g; + /** * @param {object} parameters Module parameters * @param {string} parameters.id Unique ID for the module @@ -403,9 +413,48 @@ class Module { if (!config.kind) { config.kind = "project"; // default } + for (const key of Object.keys(config)) { + this._normalizeConfigValue(config, key); + } return config; } + /** + * Normalizes the config value at object[key]. If the config value is an object / array, + * this method will descend depth-first on its properties / elements. If the config value is a string + * and contains any environment variables, they will be filled in at this point. + * + * @private + * @param {(object|any[])} object An object or array + * @param {(string|number)} key A key of the object or an index of the array + */ + _normalizeConfigValue(object, key) { + let value = object[key]; + switch (typeof value) { + case "string": { + value = value.replace(Module._ENVIRONMENT_VARIABLE_REGEX, (substring) => { + const envVarName = substring.slice(4); + return process.env[envVarName] || substring; + }); + object[key] = value; + break; + } + case "object": { + if (value === null) break; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + this._normalizeConfigValue(value, i); + } + } else { + for (const key of Object.keys(value)) { + this._normalizeConfigValue(value, key); + } + } + break; + } + } + } + /** * Resource Access */ diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index c87aac185..eff608f00 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -408,6 +408,62 @@ test("Legacy patches are applied", async (t) => { .map(testLegacyLibrary)); }); +test("Environment variables in configuration", async (t) => { + const testEnvVars = ["testEnvVarForString", "testEnvVarForObject", "testEnvVarForArray"].map( + (testEnvVar, index) => { + const wrapper = { + name: testEnvVar, + oldValue: process.env[testEnvVar], + newValue: `testValue${index}`, + }; + process.env[testEnvVar] = wrapper.newValue; + return wrapper; + } + ); + try { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configuration: { + specVersion: "2.6", + type: "application", + metadata: { + name: "application.a-object", + }, + customConfiguration: { + stringWithEnvVar: `env:${testEnvVars[0].name}`, + objectWithEnvVar: { + someKey: `env:${testEnvVars[1].name}`, + }, + arrayWithEnvVar: ["a", `env:${testEnvVars[2].name}`, "c"] + }, + }, + }); + const {project} = await ui5Module.getSpecifications(); + t.deepEqual( + project.getCustomConfiguration(), + { + stringWithEnvVar: testEnvVars[0].newValue, + objectWithEnvVar: { + someKey: testEnvVars[1].newValue, + }, + arrayWithEnvVar: ["a", testEnvVars[2].newValue, "c"], + }, + "Environment variable is filled in" + ); + } finally { + // Reset all env vars back to their value previous to testing + testEnvVars.forEach((wrapper) => { + if (wrapper.oldValue === undefined) { + delete process.env[wrapper.name]; + } else { + process.env[wrapper.name] = wrapper.oldValue; + } + }); + } +}); + test("Invalid configuration in file", async (t) => { const ui5Module = new Module({ id: "application.a.id", From ea07ad7473e1e0979a9e670a7309889549f55734 Mon Sep 17 00:00:00 2001 From: Fabian Reinbold <32802058+menof36go@users.noreply.github.com> Date: Sun, 1 Sep 2024 19:46:36 +0200 Subject: [PATCH 2/3] Update test/lib/graph/Module.js Co-authored-by: Merlin Beutlberger --- test/lib/graph/Module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index eff608f00..7fc2d8c51 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -408,7 +408,7 @@ test("Legacy patches are applied", async (t) => { .map(testLegacyLibrary)); }); -test("Environment variables in configuration", async (t) => { +test.serial("Environment variables in configuration", async (t) => { const testEnvVars = ["testEnvVarForString", "testEnvVarForObject", "testEnvVarForArray"].map( (testEnvVar, index) => { const wrapper = { From 1f8f496eee059f87a9fa072544406a8d0c629dc7 Mon Sep 17 00:00:00 2001 From: Fabian Reinbold Date: Mon, 30 Sep 2024 22:56:25 +0200 Subject: [PATCH 3/3] [FEATURE] Only transform env vars in customConfiguration, customMW, customBuilder --- lib/graph/Module.js | 27 ++++++-- test/fixtures/application.a/ui5-test-env.yaml | 25 +++++++ test/lib/graph/Module.js | 67 ++++++++++++------- 3 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 test/fixtures/application.a/ui5-test-env.yaml diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 52c7bedfc..d0de6e5ae 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -349,6 +349,25 @@ class Module { return configs; } + try { + for (const config of configs) { + if (config.customConfiguration !== undefined) { + this._normalizeConfigValue(config, "customConfiguration"); + } + if (config.builder?.customTasks !== undefined) { + this._normalizeConfigValue(config.builder, "customTasks"); + } + if (config.server?.customMiddleware !== undefined) { + this._normalizeConfigValue(config.server, "customMiddleware"); + } + } + } catch (err) { + throw new Error( + "Failed to parse configuration for project " + + `${this.getId()} at '${configPath}'\nError: ${err.message}` + ); + } + // Validate found configurations with schema // Validation is done again in the Specification class. But here we can reference the YAML file // which adds helpful information like the line number @@ -413,9 +432,6 @@ class Module { if (!config.kind) { config.kind = "project"; // default } - for (const key of Object.keys(config)) { - this._normalizeConfigValue(config, key); - } return config; } @@ -434,7 +450,10 @@ class Module { case "string": { value = value.replace(Module._ENVIRONMENT_VARIABLE_REGEX, (substring) => { const envVarName = substring.slice(4); - return process.env[envVarName] || substring; + if (process.env[envVarName] === undefined) { + throw new Error(`The environment variable '${envVarName}' is not defined`); + } + return process.env[envVarName]; }); object[key] = value; break; diff --git a/test/fixtures/application.a/ui5-test-env.yaml b/test/fixtures/application.a/ui5-test-env.yaml new file mode 100644 index 000000000..51ac6924d --- /dev/null +++ b/test/fixtures/application.a/ui5-test-env.yaml @@ -0,0 +1,25 @@ +--- +specVersion: "3.0" +type: application +metadata: + name: application.a +customConfiguration: + stringWithEnvVar: env:testEnvVarForString + objectWithEnvVar: + someKey: env:testEnvVarForObject + arrayWithEnvVar: + - a + - env:testEnvVarForArray + - c +builder: + customTasks: + - name: foo + afterTask: replaceVersion + configuration: + builderWithEnvVar: env:testEnvVarForBuilder +server: + customMiddleware: + - name: bar + afterMiddleware: compression + configuration: + serverWithEnvVar: env:testEnvVarForServer diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index 7fc2d8c51..d9a5824dd 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -409,36 +409,27 @@ test("Legacy patches are applied", async (t) => { }); test.serial("Environment variables in configuration", async (t) => { - const testEnvVars = ["testEnvVarForString", "testEnvVarForObject", "testEnvVarForArray"].map( - (testEnvVar, index) => { - const wrapper = { - name: testEnvVar, - oldValue: process.env[testEnvVar], - newValue: `testValue${index}`, - }; - process.env[testEnvVar] = wrapper.newValue; - return wrapper; - } - ); + const testEnvVars = [ + "testEnvVarForString", + "testEnvVarForObject", + "testEnvVarForArray", + "testEnvVarForBuilder", + "testEnvVarForServer", + ].map((testEnvVar, index) => { + const wrapper = { + name: testEnvVar, + oldValue: process.env[testEnvVar], + newValue: `testValue${index}`, + }; + process.env[testEnvVar] = wrapper.newValue; + return wrapper; + }); try { const ui5Module = new Module({ id: "application.a.id", version: "1.0.0", modulePath: applicationAPath, - configuration: { - specVersion: "2.6", - type: "application", - metadata: { - name: "application.a-object", - }, - customConfiguration: { - stringWithEnvVar: `env:${testEnvVars[0].name}`, - objectWithEnvVar: { - someKey: `env:${testEnvVars[1].name}`, - }, - arrayWithEnvVar: ["a", `env:${testEnvVars[2].name}`, "c"] - }, - }, + configPath: "ui5-test-env.yaml", }); const {project} = await ui5Module.getSpecifications(); t.deepEqual( @@ -450,7 +441,21 @@ test.serial("Environment variables in configuration", async (t) => { }, arrayWithEnvVar: ["a", testEnvVars[2].newValue, "c"], }, - "Environment variable is filled in" + "Environment variables in custom configuration are filled in" + ); + t.deepEqual( + project.getCustomTasks()?.[0]?.configuration, + { + builderWithEnvVar: testEnvVars[3].newValue, + }, + "Environment variable in builder custom tasks is filled in" + ); + t.deepEqual( + project.getCustomMiddleware()?.[0]?.configuration, + { + serverWithEnvVar: testEnvVars[4].newValue, + }, + "Environment variable in server custom middleware is filled in" ); } finally { // Reset all env vars back to their value previous to testing @@ -464,6 +469,16 @@ test.serial("Environment variables in configuration", async (t) => { } }); +test.serial("Undefined environment variables in configuration", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-env.yaml", + }); + await t.throwsAsync(() => ui5Module.getSpecifications()); +}); + test("Invalid configuration in file", async (t) => { const ui5Module = new Module({ id: "application.a.id",