-
Notifications
You must be signed in to change notification settings - Fork 97
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
Handy configuration #10
Comments
Why do you use flatten helper method in MPLModule.groovy file ? Why not provide the parameter cfg directly like this ? By this way, I think config parameters will be accessible like this in module scripts : Sorry for my question, I don't understand the usefulness of flattened the config map. |
Hi @blazmanx34, So there is a number of reasons to do that. What I can remember right now is:
So this ticket is exactly about the issue you see - we can't find a better solution to use all the benefits of groovy maps, but here we are. |
Thanks for your answer @sparshev I confess I do not understand why is so important for you to use configuration without exceptions. // Follow this link to execution this script by clicking on 'Edit in console'
// http://groovyconsole.appspot.com/script/5191130064879616
def CFG = [
name: "myname",
options: [
version: "2",
location: "/var"
]
]
println "CFG.name : " + CFG.name
println "CFG.options: " + CFG.options
println "CFG.options.version: " + CFG.options.version
println "CFG.nonExistingOption: " + CFG.nonExistingOption
println "CFG.nonExistingOption?.bar : " + CFG.nonExistingOption?.bar
println "Bar default value 2 = " + CFG.get('nonExistingOption', [:]).get('bar', 'defaultbarvalue') I admit the syntax is a little more complex but it allow to keep the original structure of config map object. The most important reason for me (I feel that is your third reason) is : prevent conflict between the global config and the module config. public Map moduleConfig(String name) {
def globalConfig = [global : config.subMap(config.keySet()-'modules')]
config.modules ? Helper.mergeMaps(globalConfig, (config.modules[name] ?: [:])) : [:]
} or if using the term 'global' as a reserved word bothers you, you can imagine provides 2 parameters to script modules : CFG (the module params) and GLOBAL (the pipeline config without modules config). What do you think of my proposals @sparshev ? |
Hi @blazmanx34 Yeah, when I developed this way to access the configuration I also thought about the others like using "?." everywhere, but I don't like that we can't properly find where the vars is used in the modules by usual grep (means save the 'key.key' path separated by dot) - so I think right now it's more the interface issue, neither than an usability issue. Another issue - we need to support the old interface (backward-compatibility) - so right now we have no simple way to change this configuration interface as you suggested. I think the only way right now - is to prepare a special config object based on groovy map, that will provide 2 interfaces: deprecated one |
So the thing about CFG interface overall - I think it's readability. I dan't want to make it hard to read or understand - modules should save simplicity to be read by almost anyone who even don't know Groovy. Because I think complexity is one of the main issues with the shared libraries in Jenkins Pipeline Engine. |
You did not consider Readability and backward-compatibility in your first message. |
You can think about them as "by default" requirements) Noone want to get broken modules with another MPL upgrade and noone want to read bad-looking configs that looks like someone put a cat on the keyboard :) |
In any case, thanks you for your answers. I tried to search a solution (extends Groovy NullObject to change the behavior of getProperty method) but in vain... The only solution I have found is this class which should allow to use config map or the flatten config map class MPLConfig implements GroovyInterceptable
{
def config = [:]
def MPLConfig(Map config = [:]) {
this.config = config
}
def getProperty(String key) {
String[] keylist = key.tokenize(".")
def value = config
keylist.each {
if (value != null) {
value = value?.it ?: null
}
}
return value
}
void setProperty(String key, val) {
config[key] = val
}
def invokeMethod(String name, args) {
def configMetaClass = config.getMetaClass()
def configMetaMethod = configMetaClass.getMetaMethod(name, args)
def result = configMetaMethod.invoke(config, args)
return result
}
} |
Hi @blazmanx34, Thank you for the source code, but I think it's quite not ready. But yeah - overall it's something like that but with much more complex logic for processing something like I think I can test the config interface like this (with some improvements) - but not soon, unfortunately too busy with the current projects. So if anyone could help - that will be really awesome :) |
I think there is another way to solve this issue and not break the backward-compatibility with minimal code changes: during flatten operation we can leave the original levels of configuration as is and just use links to the values from this maps. Probably this could work like that: CFG = [
first1_level: [
second1_level: [
third1_value: 5,
third2_value: "something1",
],
second2_value: 312,
],
first2_level: [
second3_value: "something2",
],
]
flatten(CFG)
groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(CFG)) And result is: {
"first1_level": {
"second1_level": {
"third1_value": 5,
"third2_value": "something1"
},
"second2_value": 312
},
"first1_level.second1_level": {
"third1_value": 5,
"third2_value": "something1"
},
"first1_level.second1_level.third1_value": 5,
"first1_level.second1_level.third2_value": "something1",
"first1_level.second2_value": 312,
"first2_level": {
"second3_value": "something2"
},
"first2_level.second3_value": "something2"
} So we achieving both simple way to call the required values without exceptions and a way to access the maps if we need to by a cost of slightly increased memory usage and multiple keys in the first level of the CFG map. What do you think guys? Could this work for you? |
Or probably it will be better to still use Config object and provide one-level access (like This interface will promise:
So both parties will get what they need - access to the maps and save the old access interface as the main one. |
Hi @sparshev , Map mergeMaps(Map base, Map overlay) {
if( ! (base in Map) ) base = [:]
if( ! (overlay in Map) ) return overlay
overlay.each { key, val ->
base[key] = base[key] in Map ? mergeMaps(base[key], val) : val
}
base
}
Map flatten(Map data, String separator = '.') {
mergeMaps(data, data.collectEntries { k, v ->
v instanceof Map ? flatten(v, separator).collectEntries { q, r -> [(k + separator + q): r] } : [(k):v]
})
}
CFG = [
first1_level: [
second1_level: [
third1_value: 5,
third2_value: "something1",
third3_value: ["el1", "el2", "el3"],
],
second2_value: 312,
],
first2_level: [
second3_value: "something2",
],
]
flatten(CFG)
println groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(CFG)) The result is
However, I think it would be essential to provide 2 parameters to script modules : CFG (the module params as it is provided today for backward-compatibility) and GLOBAL (the pipeline config without modules config), in order to prevent conflict between the global config and the module config. |
About your 2nd proposition, I admit I do not imagine what you think. class MPLConfig
{
def config = [:]
def MPLConfig(Map config = [:]) {
this.config = config
}
def getProperty(String key) {
String[] keylist = key.tokenize(".")
def value = config
keylist.each {
if (value != null) {
value = value?.it ?: null
}
}
return value
}
void setProperty(String key, val) {
config[key] = val
}
def getConfigMap() {
return config
}
} |
Hi @blazmanx34 , So it's very like your previous offer - I just removed some requirements I had in my head (like the access without exception to not existing key in manner like |
Preparing the implementation right now - yeah, even with this quite simple interface - there is alot of corner points... Hopefully unit tests will help to make sure I did not miss anything. |
Thanks, I can now more understand your proposition. For exemple, if we have a pipeline config map like that : [
agent_label: '',
param: 'value 1',
modules: [
Checkout: [:],
Build: [
param: 'value 2',
],
Deploy: [:],
Test: [:]
]
] The Build module config will be : [
agent_label: '',
param: 'value 2',
] and we have losen the global param value. Maybe, it's not a problem... Is it stupid for a module to use a pipeline config parameter ? |
Is it possible to implement GroovyInterceptable Interface in your MPLConfig class, and add the following method : def invokeMethod(String name, args) {
def configMetaClass = config.getMetaClass()
def configMetaMethod = configMetaClass.getMetaMethod(name, args)
def result = configMetaMethod.invoke(config, args)
return result
} By this way, we could use the Map methods too (it could be useful). For example : CFG = MPLConfig.create([...])
CFG.get('firstx_level','default') |
Hi @blazmanx34
Probably you missing the point of Probably this Overall splitting the configuration to
Don't really like how this is looks like - it just creates another way for providing default through the function without elvis operator. But elvis is much more powerful than this get interface, for example: CFG.'required.config.value' ?: error('Required variable is not set') So I don't think we will provide such alternative if it gives nothing but a way to write the same get config in a different way. |
OK I think I don't understand the global library philosophy.... :-( If I understand, the modules section in the pipeline config map is used to enable or disable a module and we can specify a parameter for a particular module to override the global config. I'm right ? |
Yeah, right) |
In theory that was a good approach - but practice as usual corrected the plans: Security plugin denies to access Trying to find workarounds - this approach is too good, but right now see no good ways... |
I got some progress with using |
Great news - I found a way to workaround the Security Plugin restriction by using Map as an interface, so the PR was modified and ready for review. |
Description
Current flatten configuration is not so clear:
So I think there could be a way to handle this bad things and make configuration more durable.
Map with default
But it has at least one issue:
[:]
- and it's true for any Map stored in the config. So there is no morenull
values by default.MPLConfig object
Specially prepared class that stores the pipeline configuration and handling improper gets - it should determine how much levels we need and only for the last one return null by default... Probably could be handled by groovy, but who know...
The text was updated successfully, but these errors were encountered: