-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEATURE] Add support to autofill env-vars in config #764
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -339,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 | ||
|
@@ -406,6 +435,45 @@ class Module { | |
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); | ||
if (process.env[envVarName] === undefined) { | ||
throw new Error(`The environment variable '${envVarName}' is not defined`); | ||
} | ||
return process.env[envVarName]; | ||
}); | ||
object[key] = value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder whether this requires additional checks to prevent prototype pollution, since the source of the configuration might not be fully trusted. In case the configuration object has been loaded from a YAML, the |
||
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 | ||
*/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Match the start and end of the string to prevent partial substitution like "env:should" in
this env:should not be replaced
.At least I think that would be unwanted/unexpected by most users. Or did you expect this to work as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered this aswell, but figured this would actually be expected behavior since environment variables behave like this in batch and powershell script string interpolation.
Consider a custom task that takes a configuration value in a specific format, e.g. something like
With the current implementation, the user could inject their environment variables however they might like, e.g. something like
This approach allows a more fine-grained configuration IMO. Now one might argue that the dev should not have implemented their custom configuration like this in the first place (and I agree, at least for this example a yaml array would have sufficed), but then again the goal is to move the responsibility away from the dev.
I think it's very unlikely that a user might have a substring in a config value that coincidentally matches the environment variable regex given a sufficiently unique env var format, so the additional control and flexibility the user gets in return seems worth it.
I'll gladly change it if you want me to, though