Gotcha When Building Gradle Plugins on Top of docker-java-plugin

Posted by & filed under , .

docker-logoIf you use gradle and docker you must have come across the gradle-docker-plugin. And if you started using it a lot you probably found out that more often than not your builds start using the same boilerplate code to prepare your microservices containers. And if you end up in that situation best thing to avoid boilerplate rubbish code is to write your own gradle plugin I think.

And that’s when things started getting a bit hairy it seems.

Benjamin is very well versed with Gradle as it turns out (and his code for the gradle-docker-plugin shows it) that he ends up overcoming Gradle classloader limitations by operating at classloader level directly in his plugin. This doesn’t sound as much at first site, until you package your plugin, you deploy it and start using it … and then you get the dreaded :

Caused by: org.gradle.internal.UncheckedException: java.lang.ClassNotFoundException: com.github.dockerjava.core.DefaultDockerClientConfig

(See by the way the issue I filed against my own nebula-docker-plugin here: https://github.com/nebula-plugins/nebula-docker-plugin/issues/4.) If you go and check you will find out that the com.github.dockerjava.core.DefaultDockerClientConfig class is part of the docker-java library which gradle-docker-plugin uses and declares it as a dependency, so what gives?

If you look deeper in the code, DockerRemoteApiPlugin has this bit of code:

class DockerRemoteApiPlugin implements Plugin {
    public static final String DOCKER_JAVA_CONFIGURATION_NAME = 'dockerJava'
    public static final String DOCKER_JAVA_DEFAULT_VERSION = '3.0.6'
    public static final String EXTENSION_NAME = 'docker'
    public static final String DEFAULT_TASK_GROUP = 'Docker'
 
    @Override
    void apply(Project project) {
        project.configurations.create(DOCKER_JAVA_CONFIGURATION_NAME)
                .setVisible(false)
                .setTransitive(true)
                .setDescription('The Docker Java libraries to be used for this project.')
 
        // if no repositories were defined fallback to buildscript
        // repositories to resolve dependencies as a last resort
        project.afterEvaluate {
            if (project.repositories.size() == 0) {
                project.repositories.addAll(project.buildscript.repositories.collect())
            }
        }
 
        Configuration config = project.configurations[DOCKER_JAVA_CONFIGURATION_NAME]
        config.defaultDependencies { dependencies ->
            dependencies.add(project.dependencies.create("com.github.docker-java:docker-java:$DockerRemoteApiPlugin.DOCKER_JAVA_DEFAULT_VERSION"))
            dependencies.add(project.dependencies.create('org.slf4j:slf4j-simple:1.7.5'))
            dependencies.add(project.dependencies.create('cglib:cglib:3.2.0'))
        }
 
        DockerExtension extension = project.extensions.create(EXTENSION_NAME, DockerExtension)
        extension.classpath = config
...

Basically here the code is building a classpath with the libraries the docker-java-plugin depends on and then these classes in this classpath are loaded using an instance of DockerThreadContextClassLoader. This serves as a bootclasspath for when the plugin gets executed — and as it turns out, without this trick his plugin will give the above error.

Now when you are building a plugin on top of this, you have to be mindful of the trick he employs in his code … and basically duplicate it 🙂

    void apply(Project project) {
        project.configurations.create('YOUR_CONFIG_NAME_HERE')
                .setVisible(true)
                .setTransitive(true)
                .setDescription('SOME DESCRIPTION HERE.')
        Configuration config = project.configurations['YOUR_CONFIG_NAME_HERE']
        config.defaultDependencies { dependencies ->
            dependencies.add(project.dependencies.create("com.bmuschko:gradle-docker-plugin:3.0.3"))
            dependencies.add(project.dependencies.create("com.github.docker-java:docker-java:3.0.3"))
            dependencies.add(project.dependencies.create('org.slf4j:slf4j-simple:1.7.5'))
            dependencies.add(project.dependencies.create('cglib:cglib:3.2.0'))
        }
        project.configure(project) {
            apply plugin: 'com.bmuschko.docker-java-application'
            project.extensions['docker'].classpath = config
        }
// code for your plugin here
...

This way when your plugin gets executed and applies in turn the docker-java-plugin, it will ensure the libraries this plugin depends on are loaded via his classloader trick.

I am digging into this to see if there is a way to inherit and use the dockerJava configuration his plugin is creating and will update here accordingly. For now, this can save you a lot of headaches if you are building on top of Benjamin’s (most excellent!) plugin.

One Response to “Gotcha When Building Gradle Plugins on Top of docker-java-plugin”

  1. Liv

    I have submitted actually a while back a pull request to the original docker plugin here: https://github.com/bmuschko/gradle-docker-plugin/pull/284
    However, since my plugin was (rightly so I’d say) opinionated, it doesn’t coincide with Benjamin’s approach to this project. As such there are 2 ways to get this functionality:
    * nebula-docker-plugin: https://github.com/nebula-plugins/nebula-docker-plugin
    * checkout my branch from docker-plugin in my own repo here https://github.com/liviutudor/nebula-docker-plugin and build it and use it

    If this turns out to have enough interest I might consider publishing it as a separate plugin but for now I think these 2 ways should suffice.
    As always contact me if you are interested in this.