My First Groovy DSL

I decided to have a go at automating the management of our Jenkins jobs. Basically most of our projects have similar builds, and I want to keep the job configs standard – they tend to diverge when we manage them via the UI. I’ve previously played with the Jenkins API using HttpBuilder to post XML config files. My DSL doesn’t do anything clever to generate the XML, but it makes the Jenkins config very readable:

1jenkins {  
2    url = "http://jenkins.url/"
3    username = "jenkins"
4    apiKey = "asdfadsgdsfshgdsfg"
5 
6    buildGroup {
7        name = "Maven Builds"
8        xml {
9            feature = "release/xml/maven-test-config.xml"
10            develop = "release/xml/maven-install-config.xml"
11        }
12        repos {
13            project1 = "ssh://git.repo/project1.git"
14            project2 = "ssh://git.repo/project2.git"
15        }
16    }
17 
18    buildGroup {
19        name = "Grails Builds"
20        xml {
21            feature = "release/xml/grails-test-config.xml"
22            develop = "release/xml/grails-install-config.xml"
23        }
24        repos {
25            project4 = "ssh://git.repo/project4.git"
26            project5 = "ssh://git.repo/project5.git"
27        }
28    }
29 
30}

I think this is so easy to read.

I’m not really sure if I’ve gone the right way about writing this, but here goes:

First, I have a main method, which parses the file. It binds the “jenkins” method to an instance of my JenkinsDsl class.

1Binding binding = new Binding();
2JenkinsDsl jenkinsDsl = new JenkinsDsl();
3binding.setVariable("jenkins",jenkinsDsl);
4GroovyShell shell = new GroovyShell(binding);
5Script script = shell.parse(file);
6script.run();

The JenkinsDsl class overrides the `call` method, and uses `methodMissing` to define how we handle the buildGroups. The really cool bit is I didn’t have to write anything special to get the url, username and password from the jenkins config file – groovy is automatically calling a ‘setProperty’ method on the delegate.

1class JenkinsDsl {
2 
3    def call(Closure cl) {
4        log.info "Processing Main Configuration"
5 
6        cl.setDelegate(this);
7        cl.setResolveStrategy(Closure.DELEGATE_ONLY)
8        cl.call();
9    }
10 
11    def url
12    def apiKey
13    def username
14 
15    def jenkinsHttp
16 
17    def methodMissing(String methodName, args) {
18 
19        if (methodName.equals("buildGroup")) {
20 
21            Closure closure = args[0]
22            closure.setDelegate(new JenkinsBuildGroup())
23            closure.setResolveStrategy(Closure.DELEGATE_ONLY)
24            closure()
25            closure.delegate.updateJenkins(getJenkinsHttp())
26        } else {
27            log.error "Unsupported option: " + methodName
28        }
29    }
30 
31    def getJenkinsHttp() {
32        if (!jenkinsHttp) {
33            jenkinsHttp = new JenkinsHttp(url, username, apiKey)
34        }
35        jenkinsHttp
36    }  
37}

The JenkinsBuildGroup class is very similar – to save time I’ve used the ‘Expando’ class to collect the xml and repository details.

1class JenkinsBuildGroup {
2 
3    def name
4    def xml
5    def repoList
6 
7    /**
8     * Use methodMissing to load the xml and repos closures
9     * @param methodName
10     * @param args
11     */
12    def methodMissing(String methodName, args) {
13 
14        if (methodName.equals("xml")) {
15            xml = new Expando()
16            Closure closure = args[0]
17            closure.setDelegate(xml)
18            closure.setResolveStrategy(Closure.DELEGATE_ONLY)
19            closure()
20        } else if (methodName.equals("repos")) {
21            repoList = new Expando()
22            Closure closure = args[0]
23            closure.setDelegate(repoList)
24            closure.setResolveStrategy(Closure.DELEGATE_ONLY)
25            closure()
26        } else {
27            log.error "Unsupported option: " + methodName
28        }
29    }
30 
31    def updateJenkins(def jenkinsHttp) {
32        xml.getProperties().each { String jobType, String filePath ->
33 
34            def configXml = new File(filePath).text
35 
36            repoList.getProperties().each { String jobName, String repo ->
37                String jobXml = configXml.replaceAll("REPOSITORY_URL", repo)
38                jenkinsHttp.createOrUpdate(jobName + "-" + jobType, jobXml)
39            }
40        }
41    }
42}

To actually update jenkins, I substitute the correct repository in the template XML file and post it to the Jenkins API. The JenkinsHttp class is just a wrapper for HTTP Builder. It checks if a job exists so that it can correctly create or update.

I’m not sure if this is the right way to go about writing a groovy DSL, but it works! And once I’ve finalised the template XML files, Jenkins is going to be a vision of consistency.