# 如何基于Jenkins共享库编写插件系统?
最近在研究 **Harbor** 服务部署方式时,不小心点看到了它的 **[jenkins-shared-library](https://github.com/goharbor/jenkins-shared-library "Harbor Jenkins Shared Library")** 项目,其中下面这张图里这两句给了我灵感

第一次发现这个 **[writeFile](https://www.jenkins.io/doc/pipeline/steps/workflow-basic-steps "Basic Steps")** **step** 可以将包文件写入工作空间,随即查看了其文档,截图如下:

今天就分享一个基于 **Jenkins** 共享库「**[Jenkins Shared Library](https://www.jenkins.io/zh/doc/book/pipeline/shared-libraries/ "Jenkins Shared Library")**」如何写一个插件系统。
## 背景介绍
几年前有幸了解到一个叫 **Wercker** 的 CI/CD 系统,通过 **Shell** 扩展其构建能力,并且在 **GitHub** 上也有很多开源 Wercker Step,比如:**[step-kubectl](https://github.com/wercker/step-kubectl "step-kubectl")**、**[step-maven](https://github.com/wercker/step-maven "step-maven")**。但是从 **Oracle** 花 $4 亿美元收购后,这项目就没咋个更新,所以一直在想能否在 **Jenkins** 中复用这些 **step** 代码呢?
现有的 **Jenkins** 虽然有大量可用的插件,但是满足自己场景的插件不多,而且在实践过程中会有很多定制场景,如果基于 **Jenkins** 现有的扩展方式开发插件,成本会很高,并且插件安装过多,容易导致内存暴涨,引起插件冲突等各种问题。
说了这么多其实就是来需求了不想写插件。
## 解决方案
在 **Jenkins** 中复用 Wercker Step 的功能,方法其实挺简单。
第一,通过 **writeFile** 将 **step** 包写入 **job** 工作空间
第二,将 **step** 配置参数,转化为环境变量,通过 **withEnv** 和 **withCredentials** 注入
第三,在 **job** 工作空间解压 step 包,并执行 **run.sh** 入口文件
## 实现细节
这里具体分析一下 **wercker-shared-library** 实现细节
第一,step 定义
定义 **wsl_step** 关键字(也可以叫 **step**),包含一个 step 名称和 step 参数,step 参数格式为 **yaml** 格式字符串。
```groovy
//file: vars/wsl_step.groovy
def call(String stepName, String stepOpts = null){
```
下面是使用样例
```groovy
wsl_step('sample', """
vars:
docker-run-image: k8ops/jenkins-build:maven-java8u201
secrets:
docker-auth: usernamePassword/acr-image-auth
""")
```
第二,下载 **wercker step** 包到 **job** 工作空间
```groovy
writeFile file: ".wercker/${stepName}.tar.gz", text: libraryResource(resource: "steps/${stepName}.tar.gz", encoding: "Base64"), encoding: "Base64"
```
第三,处理 step 参数,将其转化为环境变量,这个过程是最复杂的,为了方便使用 **jenkins credentials**,配置格式上将其扁平处理。
下面配置方式就是使用 **jenkins credentials** 方法,
```yaml
secrets:
docker-auth: usernamePassword/acr-image-auth
```
将上面的这些信息,进行文本处理转化为 **Jenkins** 能识别的格式,下面是转化代码。
```groovy
// usernamePassword/xxx-id
def values = it.value.split("/")
def type = values[0]
def credId = values[1]
if (type == "usernamePassword") {
secrets << usernamePassword(credentialsId: credId,
usernameVariable: "${secretEnvPrefix}USR",
passwordVariable: "${secretEnvPrefix}PSW")
} else if (type == "sshUserPrivateKey") {
secrets << sshUserPrivateKey(credentialsId: credId,
keyFileVariable: "${secretEnvPrefix}KEYFILE",
passphraseVariable: "${secretEnvPrefix}KEYPASSWORD",
usernameVariable: "${secretEnvPrefix}USER")
} else if (s.type == "FileBinding") {
secrets << [
$class: 'FileBinding',
credentialsId: credId,
variable: "${envPrefix}${stepKey}_${secretKey}"
]
} else {
error "${type} is not support"
}
withCredentials(secrets) {
sh "echo ${XXX_USR}"
}
```
普通参数就好处理,直接转化为环境变量即可,下面是转化代码。
```groovy
def envPrefix = "WERCKER_"
def stepKey = stepName.replace("-", "_").toUpperCase()
Map environment = [:]
if (opts.vars instanceof Map) {
opts.vars.each {
def varKey = it.key.replace("-", "_").toUpperCase()
environment["${envPrefix}${stepKey}_${varKey}"] = it.value
}
}
```
最终产生的环境变量格式如下:
```yaml
vars:
docker-run-image: k8ops/jenkins-build:maven-java8u201
---
WERCKER_SAMPLE_DOCKER_RUN_IMAGE=k8ops/jenkins-build:maven-java8u201
secrets:
docker-auth: usernamePassword/acr-image-auth
---
WERCKER_SAMPLE_DOCKER_AUTH_USR
WERCKER_SAMPLE_DOCKER_AUTH_PSW
```
`WERCKER_` 是前缀,`SAMPLE`是 step 名称。
第四,解压 step 包并执行
```groovy
List stepEnvVars = []
environment?.each { key, value ->
stepEnvVars << "$key=$value"
}
withEnv(stepEnvVars) {
withCredentials(secrets) {
sh """
rm -rf .wercker/${stepName}
mkdir -p .wercker/${stepName}
tar --strip-components=1 -xzf .wercker/${stepName}.tar.gz -C .wercker/${stepName}
chmod 755 .wercker/${stepName}/run.sh
bash .wercker/${stepName}/run.sh
"""
}
}
```
第五,下载一个 wercker step 打包到 `resources/steps` 目录
样例里面的 step 包叫 `sample`

## 如何使用
第一,环境准备
由于网络环境问题,演示代码存放在 Gitee 上,所以需要依赖 **[pipeline-gitee-lib-plugin](https://github.com/seanly/pipeline-gitee-lib-plugin "pipeline-gitee-lib-plugin")** 插件,因为这个插件是临时改的,没有产物包,需要的自己编译(其实我很奇怪为啥国内没有这种插件,只有GitHub)。


第二,新建测试任务

将下面的代码拷贝到 job 配置中

```groovy
//vim: ft=groovy
@Library ("gitee.com/opsbox-ecosystem/wercker-shared-library@master") _
node () {
catchError {
properties([
buildDiscarder(strategy: logRotator(numToKeepStr: "15"))
])
stage('Test') {
wsl_step("sample",
"""
vars:
whoami: seanly
docker-reg: registry.cn-hangzhou.aliyuncs.com
secrets:
docker-auth: usernamePassword/acr-docker-auth
""")
wsl_step("sample",
"""
vars:
whoami: jenkins
""")
}
}
}
```
第三,触发构建


## 最后
这套代码已经开源 **[wercker-shared-library](https://github.com/opsbox-ecosystem/wercker-shared-library "Wercker Shared Library")**,对此感兴趣的可以了解一下。
虽然这套插件机制方法简单,但是插件机制带来的是丰富的想象力。