Skip to content

Commit

Permalink
Introduce support for slim Jenkinsfile Runner packages (#449)
Browse files Browse the repository at this point in the history
* Save progress

* Get working classloading for slim packaging

* Add Slim packaging POM

* Update filters so that Winstone is retained

* Add Pipeline as YAML to CWP demo

* Update more CWP dependencies to get a stable image

* Update Plugin Installation Manager from 2.2.0 to 2.5.0
  • Loading branch information
oleg-nenashev authored Dec 14, 2020
1 parent e0ac5b9 commit 36ccf9e
Show file tree
Hide file tree
Showing 23 changed files with 401 additions and 58 deletions.
6 changes: 4 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ payload/target
payload-dependencies/target
setup/target
target/
vanilla-package/target/war
vanilla-package/target/plugins
#vanilla-package/target/war
#vanilla-package/target/plugins
vanilla-package/target/maven-archiver
vanilla-package/target/maven-status
vanilla-package/target/jenkinsfile-runner*
vanilla-package/target/surefire-reports
vanilla-package/target/classes
vanilla-package/target/generated-sources
vanilla-package/target/generated-test-sources
packaging-parent-pom/target
packaging-slim-parent-pom/target

# Tests
tests
Expand Down
45 changes: 31 additions & 14 deletions app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>prepare-lib</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.jenkins.jenkinsfile-runner</groupId>
<artifactId>setup</artifactId>
<version>${project.version}</version>
<outputDirectory>${project.build.directory}/appassembler/lib/setup</outputDirectory>
<destFileName>setup.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>io.jenkins.jenkinsfile-runner</groupId>
<artifactId>payload</artifactId>
<version>${project.version}</version>
<outputDirectory>${project.build.directory}/appassembler/lib/payload</outputDirectory>
<destFileName>payload.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- TODO(oleg_nenashev): Standalone JAR generation works by luck: https://github.com/jenkinsci/jenkinsfile-runner/issues/350
META-INF definitions are not merged correctly, and extension annotations from the core might be overridden by plugins.
Custom container description handlers are required to handle META-INF/annotations/hudson.Extension though it might be replaced by META-INF/annotations/hudson.Extension.txt.
Expand Down Expand Up @@ -89,24 +120,10 @@
<artifactId>bootstrap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.jenkinsfile-runner</groupId>
<artifactId>setup</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jenkins.jenkinsfile-runner</groupId>
<artifactId>payload</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>symbol-annotation</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci</groupId>
<artifactId>annotation-indexer</artifactId>
Expand Down
6 changes: 5 additions & 1 deletion bootstrap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<groupId>org.jenkins-ci</groupId>
<artifactId>version-number</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins.lib</groupId>
<artifactId>support-log-formatter</artifactId>
</dependency>

<dependency>
<groupId>junit</groupId>
Expand All @@ -40,7 +44,7 @@
<configuration>
<resources>
<resource>
<directory>${project.build.directory}/../src/main/resources-filtered</directory>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
</resource>
</resources>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;

/**
* @author Kohsuke Kawaguchi
*/
public class ClassLoaderBuilder {
private final ClassLoader parent;
private final List<URL> jars = new ArrayList<>();
private final HashSet<URL> jars = new HashSet<>(48);

public ClassLoaderBuilder(ClassLoader parent) {
this.parent = parent;
Expand All @@ -24,15 +24,20 @@ public ClassLoaderBuilder(ClassLoader parent) {
/**
* Recursively scan a directory to find jars
*/
public ClassLoaderBuilder collectJars(File dir, FileFilter filter) throws IOException {
File[] children = dir.listFiles();
public ClassLoaderBuilder processJars(File dirOrFile, FileFilter filter, JarHandler handler) throws IOException {
if (dirOrFile.isFile() && dirOrFile.getName().endsWith(".jar") && filter.accept(dirOrFile) ) {
handler.accept(dirOrFile);
return this;
}

File[] children = dirOrFile.listFiles();
if (children!=null) {
for (File child : children) {
if (child.isDirectory()) {
collectJars(child, filter);
processJars(child, filter, handler);
} else {
if (child.getName().endsWith(".jar") && filter.accept(child)) {
jars.add(child.toURI().toURL());
handler.accept(child);
}
}
}
Expand All @@ -41,11 +46,19 @@ public ClassLoaderBuilder collectJars(File dir, FileFilter filter) throws IOExce
}

public ClassLoaderBuilder collectJars(File dir) throws IOException {
return collectJars(dir,(File)->true);
return processJars(dir, (File)->true, (jar) -> jars.add(jar.toURI().toURL()));
}

public ClassLoaderBuilder excludeJars(File dir) throws IOException {
return processJars(dir, (File)->true, (jar) -> jars.remove(jar.toURI().toURL()));
}

public ClassLoader make() {
return AccessController.doPrivileged((PrivilegedAction<URLClassLoader>) () -> new URLClassLoader(jars.toArray(
new URL[jars.size()]), parent));
}

public interface JarHandler {
public void accept(File jar) throws MalformedURLException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public static String readJenkinsPomProperty(String key) throws IOException {
return JFR_PROPERTIES.getProperty(key);
}
try (InputStream pomProperties = Bootstrap.class.getResourceAsStream("/jfr.properties")) {
if (pomProperties == null) {
throw new IOException("Cannot find the Jenkinsfile Runner version properties file: /jfr.properties");
}
Properties props = new Properties();
props.load(pomProperties);
JFR_PROPERTIES = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ private void installPlugin(File pluginsDir, String shortname, String version) th
public ClassLoader createJenkinsWarClassLoader() throws PrivilegedActionException {
return AccessController.doPrivileged((PrivilegedExceptionAction<ClassLoader>) () -> new ClassLoaderBuilder(new SideClassLoader(getPlatformClassloader()))
.collectJars(new File(getLauncherOptions().warDir, "WEB-INF/lib"))
// In this mode we also take Jetty from the Jenkins core
.collectJars(new File(getLauncherOptions().warDir, "winstone.jar"))
// servlet API needs to be visible to jenkins.war
.collectJars(new File(getAppRepo(), "javax/servlet"))
.make());
Expand All @@ -171,6 +173,10 @@ public ClassLoader createJenkinsWarClassLoader() throws PrivilegedActionExceptio
public ClassLoader createSetupClassLoader(ClassLoader jenkins) throws IOException {
return new ClassLoaderBuilder(jenkins)
.collectJars(getAppRepo())
.collectJars(getSetupJarDir())
// Payload should be skipped, otherwise it will be loaded with a wrong classloader when no bundled plugin jars
// NOTE: Not relevant for slim JARs
.excludeJars(new File(getAppRepo(), "io/jenkins/jenkinsfile-runner/payload"))
.make();
}

Expand All @@ -181,6 +187,7 @@ public int runJenkinsfileRunnerApp() throws Throwable {
return ((IApp) c.newInstance()).run(this);
}

// Slim packaging (no bundled WAR or plugins)
ClassLoader jenkins = createJenkinsWarClassLoader();
ClassLoader setup = createSetupClassLoader(jenkins);

Expand Down Expand Up @@ -220,13 +227,45 @@ public boolean hasClass(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
} catch (ClassNotFoundException | NoClassDefFoundError e) {
return false;
}
}

// TODO: move elsewhere
/**
* Location of the app repo packaged by AppAssembler.
* Depending on the configuration, it might include full or slim packaging.
* @return Path to the app repo
*/
public File getAppRepo() {
return new File(System.getProperty("app.repo"));
}

/**
* Location of the app repo packaged by AppAssembler.
* Depending on the configuration, it might include full or slim packaging.
* @return Path to the app repo
*/
public File getLibDirectory() {
if (launcherOptions.libPath != null) {
return launcherOptions.libPath;
}
return new File(System.getProperty("app.repo"), "../lib");
}

public File getSetupJarDir() throws IOException {
File setupJar = new File(getLibDirectory(), "setup/");
if (!setupJar.exists()) {
throw new IOException("Setup JAR is missing: " + setupJar);
}
return setupJar;
}

public File getPayloadJarDir() throws IOException {
File payloadJar = new File(getLibDirectory(), "payload/");
if (!payloadJar.exists()) {
throw new IOException("Payload JAR is missing: " + payloadJar);
}
return payloadJar;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ public class JenkinsLauncherOptions {
description = "Path to a directory containing Groovy Init Hooks to copy into init.groovy.d")
public File withInitHooks;

@CheckForNull
@CommandLine.Option(names = "--skipShutdown",
description = "Forces Jenkinsfile Runner to skip the shutdown logic. " +
"It reduces the instance termination time but may lead to unexpected behavior in plugins " +
"which release external resources on clean up synchronous task queues on shutdown.")
public boolean skipShutdown;

@CheckForNull
@CommandLine.Option(names = "--libPath",
description = "When a slim packaging is used, points to the library directory which contains payload.jar and setup.jar files")
public File libPath;


public String getMirrorURL(String url) {
if (this.mirror == null || "".equals(this.mirror.trim())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.jenkins.jenkinsfile.runner.bootstrap.util;

/**
* Classloader, which might be added to the instance post-factum during the execution.
* In Jenkinsfile Runner it is used to set the plugin classloader once the Jenkins instance is initialized.
*/
public class OptionalClassLoader extends ClassLoader {

private final ClassLoader parent;
private ClassLoader optional;

public OptionalClassLoader(ClassLoader parent) {
this.parent = parent;
}

public void set(ClassLoader optional) {
this.optional = optional;
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
ClassNotFoundException parentException;
try {
return parent.loadClass(name);
} catch (ClassNotFoundException ex) {
parentException = ex;
}

ClassLoader _opt = optional;
if (_opt != null) {
try {
return _opt.loadClass(name);
} catch (ClassNotFoundException ex) {
ex.addSuppressed(parentException);
throw ex;
}
}

throw parentException;
}
}
16 changes: 16 additions & 0 deletions demo/cwp/packager-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ war:
source:
version: "2.263"
plugins:
- groupId: "org.jenkins-ci.plugins"
artifactId: "script-security"
source:
version: "1.73"
- groupId: "org.jenkins-ci.plugins.workflow"
artifactId: "workflow-job"
source:
Expand All @@ -39,6 +43,10 @@ plugins:
artifactId: "workflow-step-api"
source:
version: "2.22"
- groupId: "org.jenkins-ci.plugins.workflow"
artifactId: "workflow-scm-step"
source:
version: "2.11"
- groupId: "org.jenkins-ci.plugins.workflow"
artifactId: "workflow-support"
source:
Expand Down Expand Up @@ -83,6 +91,14 @@ plugins:
artifactId: "durable-task"
source:
version: "1.34"
- groupId: "io.jenkins.plugins"
artifactId: "pipeline-as-yaml"
source:
version: "0.12-rc"
- groupId: "io.jenkins.plugins"
artifactId: "snakeyaml-api"
source:
version: "1.26.4"
systemProperties: {
jenkins.model.Jenkins.slaveAgentPort: "50000",
jenkins.model.Jenkins.slaveAgentPortEnforce: "true"}
26 changes: 26 additions & 0 deletions packaging-slim-parent-pom/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
= Jenkinsfile Runner Packaging POM - Slim Edition (Experimental)
:toc:
:toc-placement: preamble
:toclevels: 3

This is an experimental parent POM for packaging custom Jenkinsfile Runner images.
It targets slim packaging when Jenkinsfile Runner loads the WAR file and the plugins from external location.

The parent POM prepares the following artifacts in `target`:

* `jenkinsfile-runner.zip` Slim Jenkinsfile Runner bundle ZIP (~1.5MB).
This archive needs to be unzipped, there is an executable in `bin/jenkinsfile-runner`
* `war/jenkins.war` - Jenkins WAR file, based on the `jenkins.version` property.
This file needs to be unzipped for consumption in the `-w` option.
* `plugins` - List of plugins based on the declared dependencies.
Parent POM automatically includes the minimum dependencies declared in plugin POM.
== Usage in Docker

See link:../packaging/docker/unix/adoptopenjdk-8-hotspot/Dockerfile-dev-slim[Dockerfile-dev-slim] for example.
This example also uses additional optimization tricks.

== Status

The packaging tool is under active development,
and it may be changed in incompatible way.
Loading

0 comments on commit 36ccf9e

Please sign in to comment.