Patrick
Patrick

Reputation: 5994

How can I isolate my Jenkins pipeline Groovy shared library classloader?

I have a Groovy library made available as a Global shared library:

package com.example

@Grab(group="org.apache.httpcomponents", module="httpclient", version="[4.5.3,)")
import org.apache.http.HttpHost
import org.apache.http.impl.client.HttpClients

class MyClass implements Serializable {
  static def run() {
    return HttpClients.custom()
    .setProxy(new HttpHost("proxy.example.com", 3128))
    .build()
  }

  static def debug() {
    return ("""
      this: ${this.classLoader.class.toString()} ${this.classLoader.hashCode().toString()}
      HttpHost: ${HttpHost.class.classLoader.class.toString()} ${HttpHost.class.classLoader.hashCode()}
      HttpClients: ${HttpClients.class.classLoader.class.toString()} ${HttpClients.class.classLoader.hashCode()}
    """)
  }
}

And a Jenkins scripted pipeline job using this library:

@Library('example') _
node {
  echo "${com.example.MyClass.debug()}"
  com.example.MyClass.run()
}

When the job is run, I get the following output from debug(), followed by an error from run():

  this: class org.jenkinsci.plugins.workflow.cps.CpsGroovyShell$CleanGroovyClassLoader 765101363
  HttpHost: class hudson.ClassicPluginStrategy$AntClassLoader2 804623541
  HttpClients: class hudson.ClassicPluginStrategy$AntClassLoader2 1870591909

hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: org.apache.http.impl.client.HttpClientBuilder.setProxy() is applicable for argument types: (org.apache.http.HttpHost) values: [http://proxy.example.com:3128]
Possible solutions: setProxy(org.apache.http.HttpHost)
The following classes appear as argument class and as parameter class, but are defined by different class loader

It is clear to me that some Jenkins plugin already has a dependency on httpcomponents, and the following seem true:

  1. My @Grab annotation caused the requested version of httpclient to be downloaded (as observed in ~/.groovy/grapes).
  2. But, that version is not being loaded or used by the Groovy library, but some other version that is a dependency of some Jenkins plugin.
  3. And, even more annoyingly, HttpHost and HttpClients are being loaded from different classloaders such that I cannot even use the plugin's version that leaked into my Groovy code's classloader.

Versions

Plugins Versions

Is there a way to run my Groovy in a classloader that is isolated from that of Jenkins plugins? How do Jenkins and the Groovy shared library code organize the classloaders? Is this leaking of classes pulled in by plugins intentional?

Is this a bug or am I doing something wrong? I realize I am several versions behind on Jenkins, so that's one thing to try.

As is, the system is unusable unless I'm lucky enough to have dependencies that no other plugin has, or lucky to be compatible with whatever version the classloader happens to find.

Upvotes: 13

Views: 1985

Answers (1)

lax1089
lax1089

Reputation: 3473

@Grab does not work when libraries are scoped to folders. Using @Grab in a library only works when the library is configured in the global settings. This is intentional and not a bug with Jenkins according to the plugin structure documentation.

The documentation goes on to say:

If you want to have your own libaries loaded before these (e.g. you want a newer version of velocity or an other library), you can configure your plugin to use a different classloader strategy by telling the hpi plugin in your pom.xml

Personally, I would upgrade Jenkins (and Pipelines) to the latest version available before trying the above.

Upvotes: 0

Related Questions