-
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 1 commit
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 | ||
|
@@ -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; | ||
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. Not sure whether we should do this fallback in case the environment variable is empty or not set. I would expect that we always replace the env-pattern, since the pattern itself is probably never the correct value for whatever you are passing it to. An empty value seems more likely to trigger checks and result in a reasonable error for the user (i.e. "parameter xyz is empty" from a custom task). I'll have to check again, but I think that would be in line with other tools like Docker as well.
I'm also wondering whether we should differentiate between empty environment variables and variables that are not set at all. I'm leaning towards treating them the same, but we should check how other tools are handling this. 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. Hmm, I agree that the current fallback is bad.
In case 2, we are fine, the task will (hopefully) throw an error. But what about case 1? For this reason, I think it would be best if we take responsibility and throw a reasonable error ourselves instead of delegating it to the task. At the very least we should warn the user imo. 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 agree, we can't tell whether an empty string is a legal value for a given configuration option. Are you proposing to raise an error in case an env-var is not set at all, or also in case it is set to an empty string? By now I think I'm more in favor of the first one, since empty strings may be valid configuration options. I understand that we both agree the name of the environment variable should never be used as a 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.
Yes, exactly, I think it is the best way to avoid unexpected behavior
Yes, I agree 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. After discussing this with some colleagues, I'm not sure whether raising an expection will work well with some of the expectations from SAP/ui5-tooling#935. Especially when environment variables are used in (transitive-)dependencies (e.g. a reuse-library), you would have to make sure to set all those variables when building the root project, as otherwise you will face an error.
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 agree, environment variables inside your dependencies yamls sound like a hot mess, I will gladly give that up (see comment below). If we do however restrict the feature to the root project, would you then agree that an exception is the way to go? The only way for an exception to pop up in that case is if the user messed up their own config and updates to dependencies should no longer be a concern |
||
}); | ||
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 |
---|---|---|
|
@@ -408,6 +408,62 @@ test("Legacy patches are applied", async (t) => { | |
.map(testLegacyLibrary)); | ||
}); | ||
|
||
test("Environment variables in configuration", async (t) => { | ||
menof36go marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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}`, | ||
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. Please also add a case where the pattern is inside a string (see my comment on the regex) |
||
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", | ||
|
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