I'm trying to make friends with Groovy because Jenkins uses Groovy and I need to be friends with Jenkins. Groovy is a "Java-syntax-compatible" language that runs on the JVM. Not entirely reassuring to see that Wikipedia says its "Typing discipline" is "Dynamic, static, strong, duck," which is a little like saying "north, south, black, white" - two sets of opposite things.
Because of inconsistent naming schemes, I have to deploy to servers that have nothing in the same place: some server sets have one server, some have two. And on those servers, one or two instances may be running. In a small blessing, the deploy folders have the same name on servers in the same environment (although they differ between environments). So we end up with a very complex variable to describe the servers:
def serverSets = [
"qa": [
"physicalServers" : [
'loki': ['loki'],
],
"deployFolder": '/home/loki/htdocs/',
"gitBranch" : 'qa',
],
"dev": [
"physicalServers" : [
'thor': ['asgard'],
],
"deployFolder": '/var/www/dev/',
"gitBranch" : 'dev',
],
"prod" : [
"physicalServers" : [
'frigg': ['freya', 'frey'],
'odin' : ['odin3', 'odin4'],
],
"deployFolder": '/var/www/',
"gitBranch" : 'master',
],
]
That's right: a list inside a map inside a map inside a map. That can't possibly go wrong. Sadly, it's the simplest data setup I could come up with ...
Better tutorials than I could write have been written about iterating over lists and maps (what I think of as "dictionaries" because I'm more familiar with Python, but are most formally known as an Associative Array): I recommend Working with collections from Groovy's own documentation. But what it doesn't tell you is how to deal with those nested complex variables/collections.
The first time I experimented with <list-or-map>.each {} closures, I swear that the ${it} variable didn't behave properly - at the second level it returned its first level value. And now I can't replicate the problem, as this works fine:
def listmap = [
[a: 4, b: 16, c: 64],
[x: 5, y: 25, z: 625],
]
println('Using "it":');
listmap.each {
def newmap = it;
println("first level item: " + newmap);
newmap.each {
println(" ${it.key}: ${it.value}");
}
}
But even if you didn't need to name the iterator variable to save yourself from that problem, named closure variables make the process both simpler and clearer:
def listmap = [
[a: 4, b: 16, c: 64],
[x: 5, y: 25, z: 625],
]
println('Using specific names:');
listmap.each { first ->
println("first level item: " + first);
first.each { second ->
println(" ${second.key} ... ${second.value}");
}
}
So how to deal with that very complex variable from the beginning of the post?
def serverSets = [
"qa": [
"physicalServers" : [
'loki': ['loki'],
],
"deployFolder": '/home/loki/htdocs/',
"gitBranch" : 'qa',
],
"dev": [
"physicalServers" : [
'thor': ['asgard'],
],
"deployFolder": '/var/www/dev/',
"gitBranch" : 'dev',
],
"prod" : [
"physicalServers" : [
'frigg': ['freya', 'frey'],
'odin' : ['odin3', 'odin4'],
],
"deployFolder": '/var/www/',
"gitBranch" : 'master',
],
]
['qa', 'dev', 'prod'].each { env ->
def serverSet = serverSets[env];
print('\nServer set is "' + env + '"');
println(' with deploy folder ' + serverSet["deployFolder"]);
def numServers = serverSet['physicalServers'].size();
println("env ${env} has ${numServers} physical servers:");
serverSet['physicalServers'].each { phyServ ->
println(" " + phyServ.key + ":");
phyServ.value.collect { softServ ->
println(" " + softServ);
}
}
}
The output looks like this:
Server set is "qa" with deploy folder /home/loki/htdocs/ env qa has 1 physical servers: loki: loki Server set is "dev" with deploy folder /var/www/dev/ env dev has 1 physical servers: thor: asgard Server set is "prod" with deploy folder /var/www/ env prod has 2 physical servers: frigg: freya frey odin: odin3 odin4
The code takes a minute to parse out but in the end is more manageable than I expected, and all parts of the complex variable are fairly easily accessible.
Bibliography
- iterating over a nested maps and lists (recursively, to any depth - look at all solutions): https://stackoverflow.com/questions/51488434/how-to-iterate-through-all-values-of-a-nested-map-in-groovy
- changing the implicit parameter on closures: http://groovy-lang.org/closures.html#implicit-it