前言

公司的管理后台系统的前端部分都是部署到阿里云的 OSS(对象存储服务)里。最开始,团队成员都是手动上传部署,但这样存在以下问题:

  • 权限管理不当:需要给多人分配 OSS 访问权限,增加安全风险。
  • 操作繁琐:每次发布都要手动执行上传操作,容易出错。
  • 缺乏版本管理:如果出现问题,难以快速回滚到之前的版本。

为了解决这些问题,我决定开发一个 Jenkins 插件,让前端部署更自动化、可控化,同时减少不必要的 OSS 访问权限。

插件功能概述

这个 Jenkins 插件的主要功能包括:

  • 全局配置:管理员可以在 Jenkins 后台配置多个 OSS 服务器和授权。
  • 任务配置:在具体的 Jenkins 任务中,选择 OSS 服务器和上传目录。
  • 自动备份:每次部署前,会自动对原来的版本进行备份,以便回滚。

全局配置(管理员设置)

为了确保安全性,OSS 相关配置只允许 Jenkins 管理员 进行设置。

界面如下:

  • 可以配置多个 OSS 服务器和授权。
  • AccessKeySecret 使用了 Jenkins 的密码凭证 进行存储,避免明文暴露。
  • 配置完成后,可以点击 测试连接 按钮,确保 OSS 可用。
  • 每次部署时,插件会自动备份原有版本,以便紧急情况下回滚。

界面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:c="/lib/credentials">
<f:section title="阿里云OSS配置">
<f:entry title="配置列表">
<f:repeatable var="server" name="servers" items="${descriptor.servers}">
<table width="100%">
<f:entry title="唯一名称" description="如:中台测试、订单中心测试、商品中心正式">
<f:textbox name="name" value="${server.name}" />
</f:entry>
<f:entry title="Endpoint" description="如:oss-cn-beijing">
<f:textbox name="endpoint" value="${server.endpoint}" />
</f:entry>
<f:entry title="内网" description="是否为阿里云内网">
<f:checkbox name="internal" checked="${server.internal}" />
</f:entry>
<f:entry title="BucketName">
<f:textbox name="bucketName" value="${server.bucketName}" />
</f:entry>
<f:entry title="AccessKeyId">
<f:textbox name="accessKeyId" value="${server.accessKeyId}" />
</f:entry>
<f:entry title="AccessKeySecret">
<f:password name="accessKeySecret" value="${server.accessKeySecret}"/>
</f:entry>
<f:entry title="备份存储目录" description="远程原文件备份存储目录,通常保持默认即可。">
<f:textbox name="backupFolder" value="${server.backupFolder}" default="__backup__/" />
</f:entry>
<f:entry title="上传临时目录" description="上传时的远程临时目录,通常保持默认即可。">
<f:textbox name="temporaryFolder" value="${server.temporaryFolder}" default="__temporary__/" />
</f:entry>
<f:entry title="最大备份数">
<f:textbox name="backupMaxNumber" value="${server.backupMaxNumber}" default="3"/>
</f:entry>
<f:validateButton title="测试连接" progress="连接中..." method="testConnection" with="endpoint,internal,bucketName,accessKeyId,accessKeySecret" />
<f:entry title="">
<div align="right">
<f:repeatableDeleteButton />
</div>
</f:entry>
</table>
</f:repeatable>
</f:entry>
</f:section>
</j:jelly>

任务配置

任务的配置非常简单,用户只需要:

  1. 选择已配置的 OSS 服务器
  2. 待上传的内容
  3. 指定要上传的目录

界面如下:

界面代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="OSS服务器" field="serverName">
<f:select />
</f:entry>
<f:entry title="本地目录" field="localFolder" description="待上传的目录,可使用jenkins变量。">
<f:textbox default="${descriptor.getLocalFolderDefaultValue()}" />
</f:entry>
<f:entry title="远程目录" field="remoteFolder" description="远程目录,用 / 结尾,如:user-center/ 。为空时则表示根目录。">
<f:textbox />
</f:entry>
</j:jelly>

任务构建(文件上传)

上传文件的部分逻辑较为常规,主要使用 aliyun-sdk-oss 提供的 API 进行操作。

建议参考官方文档:阿里云 OSS 官方 SDK 文档

部分 Java 代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
public class AliyunOssPublisher extends Builder {

// ...

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
logger = listener.getLogger();

AliyunOssServer server = findServerByName(serverName);
if (server == null) {
logger.println("未能找到名为 " + serverName + " 的服务配置");
return false;
}

// ...
if (publish()) {
logger.println("发布成功");
logger.println("准备收尾工作");
try {
cleanBackup();
shutdown();
} catch (Exception e) {
logger.println(desensitization(e.getMessage()));
}
logger.println("收尾完成");
return true;
}
return false;
}

private boolean publish() {
try {
// ...
return true;
} catch (Exception e) {
// ...
return false;
}
}

private void cleanBackup() {
logger.println("开始检查并清理多余备份内容");
// ...
}

public void shutdown() {
if (client != null && client.sdk != null) {
client.sdk.shutdown();
}
}

@Extension
public static final class AliyunOssPublisherDescriptor extends BuildStepDescriptor<Builder> {

private List<AliyunOssServer> servers = new ArrayList<>();

public AliyunOssPublisherDescriptor() {
super(AliyunOssPublisher.class);
load();
}

public AliyunOssServer[] getServers() {
return servers.toArray(new AliyunOssServer[0]);
}

@Override
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}

@Override
public String getDisplayName() {
return "发布到阿里云OSS";
}

@Override
public boolean configure(StaplerRequest req, JSONObject formData)
throws FormException {
// ...
}

public String getLocalFolderDefaultValue() {
return "${WORKSPACE}/build";
}

public ListBoxModel doFillServerNameItems() {
ListBoxModel m = new ListBoxModel();
AliyunOssServer[] servers = getServers();
for (AliyunOssServer server : servers) {
m.add(server.getName());
}
return m;
}

public FormValidation doTestConnection(@QueryParameter("endpoint") String endpoint,
@QueryParameter("internal") Boolean internal,
@QueryParameter("bucketName") String bucketName,
@QueryParameter("accessKeyId") String accessKeyId,
@QueryParameter("accessKeySecret") String accessKeySecret) throws Exception {
AliyunOssClient client = new AliyunOssClient(endpoint, internal, bucketName, accessKeyId, SecretHelper.decrypt(accessKeySecret));
try {
client.test();
return FormValidation.ok("链接成功");
} catch (Exception e) {
return FormValidation.error("链接失败:" + e.getMessage());
}
}
}
}

入门教程

如果你是第一次开发 Jenkins 插件,推荐先阅读官方的入门教程:Jenkins Plugin 开发入门

这个教程简单直接,可以帮助你快速了解 Jenkins 插件的开发流程,避免走弯路。

如何进阶

Jenkins 官方的入门教程比较基础,要真正开发一个插件,你可能会遇到一些文档里没有覆盖的问题。我的建议是:

找类似插件的源码:比如一些已有的 Jenkins OSS 上传插件,看看它们是如何实现的。

如何调试

参考:https://blog.csdn.net/xueerfei008/article/details/81871241

文章中是以 MacOS 为例子。

如何运行

在命令行中执行:mvn hpi:run

如何打包

在命令行中执行:mvn clean install

如何发版

修改 pom.xml 中的版本号,再执行 打包命令,打包得到 hpi 文件。

结语

这次是我第一次正式用 Java 开发,过程充满挑战,但也收获颇丰。希望这篇文章能帮助到想开发 Jenkins 插件的同学。🎉

如果你有更好的建议或者遇到问题,欢迎交流!

  • 第 1 页 共 1 页
作者的图片

YaoWorld

热爱编程,热爱生活。

前端工程师

中国-成都