What am I doing wrong here trying to write a plugin that defines conventions for another plugin?


(Doug Lethin) #1

I’m seeking some advice on how to write a plugin that extends an existing plugin and automatically configures that plugin’s extension object. I wish I started this work last month so I could have asked some of the Gradleware developers while attending the Gradleware summit (which was great, btw), but hopefully this forum will suffice.

What I’m trying to do is automatically configure the jenkins plugin (found here: https://github.com/ghale/gradle-jenkins-plugin)

The jenkins plugin provides a mechanism for generating jenkins job configuration xml files, as well as tasks for uploading those configurations to a server. The plugin provides a DSL for defining jenkins server and the job definitions.

A build.gradle file using this plugin might look like this:

jenkins {
  servers {
    main {
      url 'http://localhost:7070'
    }
  }
    defaultServer servers.main
    jobs {
    myJob {
      definition {
        xml {
   // xml is a markup builder closure
          doit()
        }
      }
    }
  }
}

What I would like to do is write a plugin that automatically configures the jenkins plugin job definitions based on my own model provided by my plugin.

For example, I’ve written a “base” plugin which defines a ProductDefinition and I attach that as an extension object to the project, allowing me to have my build.gradle file look like this:

apply plugin: 'myPlugin'
  product {
  team = ["dev-team@myCompany.com"]
    branches {
    // main (this is created by default. don't need to put it here
    featureA
    release
  }
}

In the case above, what I would like to do is create a series of jenkin’s job derived from my above model. My model is very simple at this point as I’m just getting started. I have a product. A product is associated with a team and can have a number of branches. Each branch can have it’s own team, or it can use the main project team. By default, my plugin creates a main branch. There can be others.

To simply the use case, for each of these branches, I want to create 1 jenkin’s job and I want my product/branch model to be used to derive the configuration of the jenkin’s job. One example is that I want to configure the notification of the job such that build failures get sent via email to the team associated with the branch being built.

I took some stabs at getting this to work, but have not had any luck. Here is the latest in what I tried to do:

public class ProductJenkinsPlugin implements Plugin<Project> {
    Project project
    public void apply(Project project) {
    this.project = project
      // This is my plugin that defines a 'product' model
    project.getPlugins().apply(ProductBasePlugin.class);
      // This is the plug that provides the 'jenkin's model that I want to automatically configure based on my product model.
    project.getPlugins().apply(JenkinsPlugin.class);
      project.afterEvaluate {
      createJenkinsJobs()
    }
  }
    final void createJenkinsJobs() {
      def jobs = project.container(com.terrafolio.gradle.plugins.jenkins.JenkinsJob)
      project.product.branches.all { ProductBranch branch ->
        def jobName = "${project.product.name}-${branch.name}-commit"
      def job = jobs.create(jobName)
      println "Adding job: ${job.name}"
      // TODO: what is the proper way to configure the this new jenkins job
    }
  }
}

Then my project build.gradle looks like this:

product {
   name = "myproduct"
  team = ["dev-team@myCompany.com"]
    branches {
    // main (this is created by default. don't need to put it here
    featureA
    release
  }
}
  jenkins {
  servers {
    dummy {
      url 'http://localhost:7070'
    }
  }
    defaultServer servers.dummy
    jobs {
    blah {
      definition {
        xml {
          doit()
        }
      }
    }
  }
}
  task doIt << {
  // diagnostic stuff
    println "${product.name} Branches:"
  product.branches.each { branch ->
    println "${branch.name} - team: ${branch.team}"
  }
    println "jenkins plugin jobs:"
  jenkins.jobs.each { job ->
    println "${job.name}"
  }
}

Running the doIt task provides the following output:

Adding job: myproduct-featureA-commit
Adding job: myproduct-release-commit
Adding job: myproduct-trunk-commit
:apps:gpserver:doIt
myproduct Branches:
featureA - team: [dev-team@myCompany.com]
release - team: [dev-team@myCompany.com]
trunk - team: [dev-team@myCompany.com]
jenkins plugin jobs:
blah

So I can see that the code in my plugin is adding jenkin’s job’s, but when I run my diagnostic ‘doIt’ task to iterate over the jenkin’s job, all I see is the one i explicitly defined in my build.gradle file (called blah). The ones I dynamically created from my model are not showing up.

What is the best way to accomplish what I’m trying to do?

Thanks in advance.

Doug


(Doug Lethin) #2

Ok, so after posting this, I continued to play around and think I found a partial solution.

I modified my plugin to look like this:

public class ProductJenkinsPlugin implements Plugin<Project> {
    Project project
    public void apply(Project project) {
    this.project = project
      project.getPlugins().apply(ProductBasePlugin.class);
// This plugin provides support for defining a 'product'
    project.getPlugins().apply(JenkinsPlugin.class);
    // this is the plugin I want to automatically configure
      project.afterEvaluate {
      createJenkinsJobs()
    }
  }
    final void createJenkinsJobs() {
      project.product.branches.all { ProductBranch branch ->
        def jobName = "${project.product.name}-${branch.name}-commit"
        project.jenkins.jobs {
          "${jobName}" {
          definition {
            xml {
blah()}
          }
        }
      }
    }
  }
}

Then running then jenkin plugin’s dumpJenkinsJobs configuration does indeed produce the jenkin’s jobs I injected as seen here:

% gradle clean dumpJenkinsJobs
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:apps:gpserver:clean
:apps:gpserver:dumpJenkinsJobs
  BUILD SUCCESSFUL
  Total time: 1.226 secs
% ls build/jobs/
blah-config.xml
  myproduct-release-commit-config.xml
myproduct-featureA-commit-config.xml myproduct-trunk-commit-config.xml
% cat build/jobs/myproduct-featureA-commit-config.xml
<blah/>
%

Now that I got this working, I can start to flesh out my markup builder closure to generate the real contents of my jenkins job configurations.

I think there is still a problem with my solution, however.

I currently dynamically generate the jenkin’s jobs when the project has finished evaluating:

project.afterEvaluate {
      createJenkinsJobs()
    }

The problem with this approach is that I want to allow users of my plugin to override any of the jenkin’s job confguration in there build.gradle file.

For example, my build.xml could have this:

jenkins {
    jobs {
    "myproduct-featureA-commit" {
      definition {
        xml {
          override()
        }
      }
    }
  }
}

In this case, my build.gradle file wants to override the markup closure used to define the job’s configuration. I would expect after dumping the job configuration’s to see " as my job configuration for my ‘myproduct-featureA-commit’ job, but instead I see it as which is the one my plugin injected.

It seems that there is some clever wiring I need to do to somehow define my jenkin’s job each time a a new branch in my product model is created. However, in my base plugin which defines the model, I create a main branch by default/convention. At that time my baseplugin doesn’t know anything about jenkins.

Not sure if anything has any suggestions on how to resolve this issue.

Thanks.

Doug


(Peter Niederwieser) #3

It’s a tricky problem. Some ideas:

  • Expose a ‘generateJenkinsJobs()’ method that the user can call before overwriting job details. Perhaps ‘afterEvaluate’ would only call that method if it wasn’t called by the user already. * Use ‘NamedDomainObjectCollection.addRule’ (assuming ‘jenkins.jobs’ is of that type) to kick off job generation (in case it hasn’t run already). Again, you’ll need an ‘afterEvaluate’ backup. * Model everything that the user needs to be able to overwrite in your own DSL.