「Jenkins Pipeline」- 执行 resource 文件

  CREATED BY JENKINSBOT

问题描述

在 Jenkins Pipeline 中,使用 Groovy 语言进行共享库的开发。从理论上讲,我们可以按照需求,开发我们想要的任何功能。但是,现实中总会遇到一些棘手的问题。比如这次遇到的 Dependency hell – 我们在共享库中,通过 Grape 引入我们需要的模块,这些模块又依赖于其他模块,然而这些模块与 Jenkins 正在使用的模块冲突。

	比如,我们需要使用 [[https://github.com/ThStock/docker-java-parser|ThStock/docker-java-parser]] 模块解析 Dockerfile 文件,以检查其是否符合我们的规范。但是 [[https://github.com/ThStock/docker-java-parser|ThStock/docker-java-parser]] 模块需要 commons-lang 3.5 模块,而 Jenkins 使用 commons-lang 2.6 版本。这便是冲突。

该笔记将记录:如何解决 Jenkins Pipeline 共享库的依赖问题,以及相关问题处理。

注意事项

在多数 常规的 自动化场景中,不会遇到该问题,只是我们的需求太特殊了,需要解决这个问题。

解决方案

方案一、通过脚本实现

既然存在冲突,导致部分任务我们无法通过 Pipeline 共享库实现,那么我们可以编写自定义脚本来实现(比如通过 python 脚本实现某个极度复杂的统计功能),然后在 Pipeline 中执行该脚本。

现在,我们还需要将该脚本保存到 Pipeline 共享库中,作为共享库的组件,与其他用户分享,毕竟这个脚本也是共享库的一部分。根据目前(02/20/2021)的共享库的要求,只能保存在 resources/ 目录中。现在的问题就是:我们如何执行 resources/ 目录中的脚本

在 Jenkins Pipeline 中,可以在共享库的 resources/ 目录中保存资源文件,然后通过 def text = libraryResource ‘data.yaml’ 的方式读取资源文件。

整个过程就是这样:1)通过 libraryResource 读取脚本内容,2)然后使用 writeFile 写入外部文件,2)再执行该外部文件

程序示例:

def functionsContent = libraryResource 'com/mycorp/pipeline/somelib/functions.sh'
writeFile file: '/tmp/functions.sh', text: functionsContent
sh "sh /tmp/function.sh"

方案二、计算路径(方案一的失败改进)

# 02/20/2021 一顿忙活之后,发现该方案不可行。原因是,虽然我们可以选择 Pipeline 的执行节点,但是共享库文件只存在于主节点。不过,下面这些代码还有有些参考价值,以后可以用在别的地方。

方案一有个问题:如果我们有很多脚本,那么这些脚本只能独立执行,不能相互引用。这是因为脚本是被先读取出来,然后再执行,我们不知道正在读取的脚本中引用的文件,因此就无法同时读取相关的文件。这就导致不同脚本中,会出现大量重复代码。(也不是没有可能,我们可以自己实现解析器,以注释的形式定义脚本依赖关系,然后在加载时进行解析,以加载相关依赖)

但是,办法总是有的,我们通过获取当前源码文件的路径,然后计算出脚本文件路径。这成为解决这个问题的关键。

下面的三种方法都可以获取源码文件的路径:

// 方法一
def filePath = this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()
println filePath

// 方法二

import groovy.transform.SourceURI
import java.nio.file.Path
import java.nio.file.Paths

@SourceURI URI sourceUri
Path scriptLocation = Paths.get(sourceUri)
println scriptLocation.toString()

// 方法三

def filePath = this.class.classLoader.getResourceLoader().loadGroovySource(this.class.name).toURI()
println filePath

// 这些方法都将输出如下内容:

/var/jenkins_home/jobs/pipeline-example/builds/22/libs/TOOLBOX/vars/fileLocation.groovy

因此我们使用如下方法,适用于全局变量(vars/)与类库(src/):

import java.nio.file.Path;
import java.nio.file.Paths;

def sourceFile = "/var/jenkins_home/jobs/pipeline-example/builds/22/libs/TOOLBOX/vars/fileLocation.groovy"
Path path = Paths.get(sourceFile)
def dirname = ""
while (dirname = path.getFileName().toString()) {
	if ("vars".equals(dirname) || "src".equals(dirname))
		break
	path = path.getParent()
}
def resourcesFolder = path.getParent().resolve('resources').toString()
println resourcesFolder // /var/jenkins_home/jobs/pipeline-example/builds/22/libs/TOOLBOX/resources

但是,该方法也有局限性:当 Jenkins 存在多个共享库,那么通过该方法定位的 resources 目录可能不是我们期望的目录。

方案三、另外一种思路

自定义类加载器,来加载 Jar 包,以使用不同的类:

ClassLoader cl = new URLClassLoader(new URL[] {new File("v1.jar").toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> clazz1 = cl.loadClass("com.abc.Hello")

ClassLoader c2 = new URLClassLoader(new URL[] {new File("v2.jar").toURL()}, Thread.currentThread().getContextClassLoader());
Class<?> clazz2 = cl.loadClass("com.abc.Hello");

# 02/20/2021 目前,该方案并不适用于此问题,因为 Jar 包的管理与定位又成为另外一个问题。我们在此记录此方法,为后面提供一种思路,也许那一天就用到了。

参考文献

How to invoke bash functions defined in a resource file from a Jenkins pipeline Groovy script?
Java – how to load different versions of the same class? – Stack Overflow
android – Java: How to import older version of class – Stack Overflow
How do you get the path of the running script in groovy? – Stack Overflow
groovyshell – How to use \@SourceURI annotation to retrieve the full path of the script file in Groovy 2.3?
jenkins – How to load files from resources folder in Shared library without knowing their names (or number)?