RTC 和 Jenkins 在 SaaS 应用中持续集成的最佳实践

RTC 和 Jenkins 在 SaaS 应用中持续集成的最佳实践


SaaS 应用背景概述

SaaS 是 Software as a Service(软件即服务)的简称,随着互联网技术的发展和应用软件的成熟,在 21 世纪开始兴起的一种完全创新的软件应用模式。服务提供商将应用软件统一部署在自己的服务器上,客户可以根据自己的实际需求,通过互联网向提供商订购所需的应用软件服务,按订购的服务多少和时间长短向其支付费用,并通过互联网获得所订购的服务。

笔者所在的这个 SaaS 项目比较复杂。为了帮助读者更好地理解下一章描述的使用 RTC 和 Jenkins 对该项目做持续集成时遇到的问题,这里先向大家简单介绍下这个项目。

这个 SaaS 项目目前拥有超过 300 个大大小小的客户。刚开始所有客户使用的都是同一套代码,后来为了满足部分客户的业务需求,该应用针对每个有特殊需求的客户做了一定程度的定制化。目前,总共有超过 100 个客户有定制化(为了方便描述,本文用 Core 指代未定制的代码,用 Customization 指代所有定制化的代码)。

该项目总共包含了 4 个组件,即 compA、compB、compC 和 compD。有的客户对 4 个组件都有一定程度的定制,有的客户则只有其中某几个组件,另一些组件则仍然使用 Core 的。另外,Core 和 Customization 的开发方式存在一定的差异。Core 方面,一般是某几个开发人员负责一个组件,而 Customization 方面,则是一个开发人员负责至少一个客户。详见下图。

图 1. 项目模块和开发人员分布

项目模块和开发人员分布

回页首

RTC 和 Jenkins 在 SaaS 应用中遇到的问题

该 SaaS 项目原本使用 CVS 作源代码版本管理,随着 IBM Rational Team Concert(简称 RTC)的推广,以及持续集成的盛行,决定使用 RTC 协作式开发,并结合 Jenkins 进行持续集成,因此需要把整个项目代码从 CVS 迁移到 RTC。由于 SaaS 应用相比非 SaaS 应用来说,本身具有一定的复杂性,同时因为该项目的具体业务需求,笔者所在的团队在持续集成过程中遇到如下几个问题。

  • Core 和 Customization 代码访问权限分开控制。这是该项目的业务需求。要求 Customization 的开发人员没有权限读写 Core 的代码,反之亦然。同时,Core 代码的读写权限分开,即读写权限仅向 Core 的开发人员开放,读权限仅向某些指定人员开放。RTC 的常规配置很难达到这样的权限控制要求,因此需要设计一种打破常规的用法。
  • RTC 现有的某些 Work Item 类型不适用。这也是与项目的实际业务需求相关的(可能对于某些项目来说,RTC 现有的 Work Item 够用了,而对另一些项目,则会出现不适用的情况)。RTC 提供了多种 Work Item 类型,包括 Defect、Task、Track Build Item 等,但是由于该项目的实际需要,不仅需要新增一些 Work Item 类型,还需要对某些现有类型作定制化。
  • RTC 中现有 Work Item 的工作流程跟实际的业务需求不符合,需要定制。这个问题不仅限于 SaaS,非 SaaS 也可能会遇到。因为每个项目的实际业务流程不可能相同,我们或多或少需要定制现有 Work Item 的工作流程。
  • Jenkins 现有的构建参数类型不满足该项目的实际构建需求。该 SaaS 项目在构建时,需要使用带参构建,某些参数的选项之间还存在关联关系。比如选择构建 compA,某个参数的下拉选项值为[option1, option2],若构建 compB,该下拉选项变为[option3, option4]。这个问题不仅限于 SaaS 应用,可能有些非 SaaS 应用,也会需要如此复杂的构建参数。Jenkins 现有的构建参数插件虽然能满足下拉框的要求,但是不能满足参数级联。
  • Jenkins 的带参构建无法记录上次构建所填写的参数值。该 SaaS 项目构建时需要与构建者交互的参数比较多,如果不能实现参数记忆,构建者每次进入构建页面,都得重新填写参数值,用户体验差。
  • Jenkins 的带参构建无法验证所填参数的正确性。该 SaaS 项目在构建时需要构建者填写所构建组件的 version 信息,格式为 x,x,x,x,其中 x 表示 0-999 的任意值。设想构建者填完参数,并触发构建后,中途因为 version 填写错误而失败了,这时用户不得不重新填写参数,再次触发。这样的用户体验是不能接受的。如果用户提交构建后,Jenkins 能检验所填参数的正确性,只有所有参数都正确才触发构建过程,否则提示用户修改,这样将会很大程度节省平均构建时间,提升用户体验。
  • 生成详细的构建结果。详细的构建结果中包含本次构建与上次构建之间的所有变更集(change set),及其对应的 Work Item 信息。可能有的读者认为使用 RTC 自带的构建引擎就可以不用考虑构建结果的问题了。其实不然,就算暂且不考虑 RTC 构建引擎难以扩展和定制的问题,我们也没法避开如何获取变更集关联的工作项(Work Item)的问题。因为至少在 RTC4.0.3 之前(包括 4.0.3),RTC 自动生成的构建结果也不是很详细,虽然包含了变更集(change set)信息,但是没有包含其对应的工作项(Work Item)信息。

回页首

具体解决方案的分析与实现

本章将逐一分析上一章列举的问题,并提供笔者在项目中采用的解决方案以供读者参考。

代码访问控制

RTC 中提供了五种 Project Area(以下简称 PA)的访问权限。如下图所示:

图 2. Project Area 中的五种访问权限

Project Area 中的五种访问权限

  • Everyone:所有人都能访问该 PA
  • No one (repository administrators only):除了 RTC 管理员,其他人都没有权限访问该 PA
  • Members of the project area hierarchy: 该 PA 的所有成员都有权限访问
  • Members of the project area hierarchy and users in the access list below:该 PA 的所有成员,以及访问列表中添加的人员都有权限访问
  • Users in the access list only:只有访问列表中添加的人员才能访问

项目中要求对 Core 和 Customization 的代码分开控制,即 Core 的开发人员无权访问 Customization 的代码,反之亦然。这样的需求不难实现。我们的做法是:

  1. 在 PA 中创建两个 Team Area(以下简称 TA),分别为 Core Team 和 Customization Team。如图 3 所示。
  2. 将 Core 和 Customization 的开发人员添加到各自的 TA。
  3. 针对 PA 中的每条 Stream,修改其 Owned by 信息。比如属于 Core 的 Stream,把它的 Owned by 改成 Core Team;属于 Customization 的 Stream,则改成 Customization Team。Visibility 指哪些人能看到这个 Stream。由下图可以看到,Visibility 的选值只有两个,一个是这个 Stream 的 Owner,另一个是整个 PA。也就是说这个 Stream 要么只能被它的 Owner 看到,要么能被整个 PA 的成员看到。
图 3. PA 中包含的 TA

PA 中包含的 TA

图 4. Stream 的 Owned by 以及 Visibility 配置

Stream 的 Owned by 以及 Visibility 配置

以上配置虽然能够实现 Core 和 Customization 的开发人员只能访问各自的代码,但是不能细化对读权限的控制,这在实际项目开发中是不切实际的。因为我们往往需要仅仅把读权限开放给某些授权用户,而不是所有用户。

要满足这个需求,要么对 RTC 的权限管理进行定制,要么寻找一种替代方法。这里笔者采用了一种替代方法,能够比较快速地解决这个问题。

  1. 创建两个 PA(Core Project 和 Customization Project)分别用于管理 Core 和 Customization 的代码;
  2. 在 Core 的 PA 中建立两个 TA,比如 ReadWrite 和 OnlyRead。把 ReadWrite TA 设置为 Core 代码的 Owner,具有读写权限,见图 4;OnlyRead TA 则用来管理所有对 Core 代码有读权限的人;
  3. 在 Customization 的 PA 中为每个客户分别建立一个 TA 用于管理该客户的所有开发人员。如果需要严格限制不同客户之间的代码相互访问,则可把某个客户代码的 Owner 设为该客户的开发人员所在的 TA,如此,不同客户的开发人员则不能修改其他客户的代码。

工作项(Work Item)定制

RTC 虽然提供了不少 Work Item 类型供选择,但是由于不同项目的业务流程各有不同,RTC 很难提供一套完全通用的模板。当发现现有模板不适用时,我们通常有两种做法,一种是定制化现有 Work Item,另一种是针对项目的实际需求,重新设计一个 Work Item。

对于定制化现有 Work Item,笔者将以 Defect 为例介绍具体方法。下图是 RTC 中提供的 Defect 的属性。

图 5. Defect 现有属性

Defect 现有属性

根据上文对笔者所在项目的背景介绍中提到,该项目包含了 4 个组件。如此,很显然,对于每个 Defect,我们需要知道它是在哪个组件中发现的。此外,还需要知道是通过哪个测试用例发现这个 Defect 的。这些信息在现有的 Defect 属性中是没法记录的。这时,我们需要对 Defect 作定制,根据实际业务需求,修改 Defect 属性。

打开 Work Item 所在的 PA,通过 Process Configuration-> Configuration Data-> Work Items 页面对其定制,丰富既有的属性。下图是定制后的 Defect。

图 6. 定制后的 Defect

定制后的 Defect

对比图 5 和图 6,不难看出,图 6 中增加了很多新的属性,同时也去掉了一些实际项目中用不到的属性。

对于创建新的 Work Item,由于具体创建方法和 Work Item 的定制大同小异,故只展示新建的某个 Work Item CR,不赘述具体步骤。

图 7. 创建新的 Work Item

创建新的 Work Item

工作流程(Workflow)定制

工作流程一般与项目的实际开发流程有关,所以经常需要定制。RTC 中,每种 Work Item 类型都有其对应的工作流程。下面还是以 Defect 为例,介绍如何实现工作流程的定制化。

图 8 是使用 Scrum 模版初始化 PA 后,得到的 Defect 的工作流程。

图 8. 定制之前,Defect 的工作流程

定制之前,Defect 的工作流程

很多项目在实际开发中需要加入代码评审(Code Review)的流程,也就是说所有需要提交到 RTC 的代码都要进行评审。评审不通过,则不能提交,以保证 RTC 中代码的质量。这个时候,上图的工作流程显然不适用,可参考如下步骤进行定制:

  1. 打开 Workflow 对应工作项(Work Item)所在的 PA,然后 Process Configuration -> Project Configuration -> Configuration Data ->Work Items -> Workflows。
  2. 添加项目实际需要的状态。这里,笔者添加两个状态:Reviewing 和 Approved,分别表示正在评审中和通过评审。
  3. 添加到达步骤 2 中所添加状态的动作。这里,假设到达 Reviewing 的动作是 Request a Review,到达 Approved 的动作是 Approve。见图 9。
  4. 修改状态转化图 Transitions。假如 In Progress 经过 Request a Review 到达 Reviewing,然后 Reviewing 通过 Approve 到达 Approved,Approved 经过 Resolve 动作到达 Resolved。图 10 表示经过定制后的状态转化图 Transitions。如果读者还需要加入其他的业务流程,可以参考以上步骤。
图 9. 添加状态以及到达该状态的动作

添加状态以及到达该状态的动作

图 10. 定制后的状态转化图

定制后的状态转化图

至此即实现了工作流程的定制化。

不论是定制工作项(Work Item),还是定制工作流程(Workflow),重点是清楚项目的实际业务流程和开发流程,然后根据流程抽象出相应的属性、状态和动作。

定制 Jenkins 构建参数

提到 Jenkins 中虽然提供了很多构建参数插件,但是却没有一个插件能够实现参数级联,而这却是很多较复杂的构建过程所需要的,比如笔者经历的这个 SaaS 项目。

需求描述

构建过程中有两个具有级联关系的下拉框参数 A 和 B。两个参数的选项都保存在数据库中,其中参数 B 的选项依赖于参数 A 的值,即 A 和 B 具有级联关系。

插件开发

要开发 Jenkins 插件,笔者大体采用的方式是从现有插件的源码入手,结合官网教程读懂代码。对现有插件了解的差不多后,再尝试动手开发。第一次开发 Jenkins 插件的读者可以参考这种方式,能加快了解插件的源码结构,缩短实际开发过程。

图 11 是该级联下拉框构建参数插件的架构图。所有下拉框中的选项都保存在 MySql 中。ParameterDefinition 是 Jenkins 中开发参数插件的一个非常重要的类,它对应于 Jenkins 配置 job 时的 Parameter 选项。所有构建参数插件都需要继承该类,才能在 job 配置页面的 Add Parameter 下拉框中看到我们开发的插件。

图 11. Cascade Drop Down Parameter 插件架构

Cascade Drop Down Parameter 插件架构

清单 1 列出了该级联插件中的关键类 CascadeParameter。该类继承了 ParameterDefinition,并且包含了一个内部类 DescriptorImpl,内部类中定义了一组 doFillXyzItems 方法,其中 xyz 是参数名称。这组方法的主要作用是从数据库中获取下拉框的选项,保存在 ListBoxModel 列表中。注意:xyz 必须和该插件的 index.jelly 文件中定义的 field 值相同,否则将会出现找不到 xyz 参数错误。

清单 1. 关键代码分析
public class CascadeParameter extends ParameterDefinition {
 ...
 @Extension
 public static final class DescriptorImpl extends ParameterDescriptor {
 ...
 public ListBoxModel doFillParameterAItems() {
 // get the options of parameterA, and put them in the ListBoxModel
 ListBoxModel m = new ListBoxModel();
 }
 public ListBoxModel doFillParameterBItems(@QueryParameter String parameterA) {
 // get the options of parameterB based on the value of parameterA, 
 and put them in the ListBoxModel
 ListBoxModel m = new ListBoxModel();
 }
 }
  ...
}

根据需求,参数 B 依赖于参数 A,这时需要在参数 B 的 doFill 方法中定义一个用@QueryParameter 标记的形参,传递参数 A 的选值。如此,一旦参数 A 的选值发生变化,就会触发参数 B 的 doFill 方法,更新参数 B 下拉框中的选项。

图 12. Cascade Drop Down Parameter 插件-效果展示 1

Cascade Drop Down Parameter 插件-效果展示 1

上图展示了一个具有级联关系的 5 个参数,其中最上面的参数 FrontEnd 的选值会影响下面 4 个参数的选项。现在 FrontEnd 的值是“Choose component …”,这时下面 4 个参数都没有值。

图 13. Cascade Drop Down Parameter 插件-效果展示 2

Cascade Drop Down Parameter 插件-效果展示 2

从图 13 可以看到,当 FrontEnd 改变选值后,下面 4 个参数的选框内容也发生改变。

添加 Jenkins 参数验证功能

目前 Jenkins 的带参构建不具备参数验证功能,也就是说构建工程师只能在构建脚本中验证参数的正确性,一旦不正确,则构建失败。很明显,这样的实现方式非常浪费构建时间,影响构建效率,降低用户体验。下面将介绍如何定制 Jenkins 的构建插件,添加参数验证功能。

需求分析

参数验证功能,指的是当用户进入构建页面,填完参数,点击 Build 按钮后,先触发参数验证,如果有参数错误,则弹框提示具体错误信息,否则触发构建。

功能开发

分析 Jenkins 源码(AbstractProject.java)可知,当用户点击“Build with parameters”后,doBuild 方法被调用,该方法获取参数列表后,跳转到 ParametersDefinitionProperty 对应的 index.jelly 页面。查看该 jelly 文件,发现它会遍历每个参数,找到该参数对应的 index.jelly 文件并显示。请看清单 2.

清单 2. ParametersDefinistionProperty 对应的 index.jelly
 ......
<f:form method=”post” action=”build${empty(delay)?’’:’?delay=’+delay}”
name=”parameters” tableClass=”parameters”> ... <j:forEach var=”parameterDefinition” 
items=”${t.parameterDefinistions}”> <tbody>
<st:include it=”${parameterDefinition}”
 page=”${parameterDefinition.descriptor.valuePage}” />
</tbody>
</j:forEach>
.......

可以在这个 index.jelly 中添加 JS 实现参数验证。

清单 3. 加入 JS 代码后的 index.jelly
......
 <l:layout css=”...” title=”${it.displayName}” norefresh=”true”>
 ...
 <l:main-panel>
 ...
 <script scr=”yourPath/verification.js” />
 </l:main-panel>
 ......

图 14 是加入参数校验后的效果图,用户点击 Build 按钮后,首先触发参数校验 JS,一旦检查到参数有误,则弹框提示,这时页面被灰层遮罩。用户只有关闭弹框,改正出错参数后,才能继续构建。

图 14. 加入校验代码后的效果展示

加入校验代码后的效果展示

Jenkins 构建参数记忆和预填

构建参数记忆和预填对于参数较多的构建非常有必要,用户不用每次都填写参数,既节约构建时间,又提升用户体验感。

需求分析

每次构建时能够保存或者更新当前参数值,下次进入构建页面后,能用上次保存的值预填参数。这样,用户不用重复填写所有参数,只需要对值有改变的参数作修改。

Jenkins 虽然有个 Rebuild 插件具备类似功能,但是使用起来却有很大局限性——只能用于 input 类型的简单参数,对下拉框、单选和复选等较复杂参数不起作用。

图 15 和图 16 显示了 Rebuild 插件的测试结果。虽然 Rebuild 插件能记忆上次 build 的参数值,并预填到当前构建中,但是从图 16 明显可以看到,原本的下拉框类型和 check box 类型都变成了 input 类型,这显然是不可以的。

图 15. 测试现有的 Rebuild 插件-填写参数

测试现有的 Rebuild 插件-填写参数

图 16. 测试现有的 Rebuild 插件-参数类型被强制修改

测试现有的 Rebuild 插件-参数类型被强制修改

功能实现

现有的 Rebuild 插件不适用,那就只能对 Jenkins 进行定制。要实现该功能,主要解决两个问题,如何记忆和如何预填。

参数记忆的问题容易解决,笔者采用的方法是每次构建触发后,构建脚本会把用户填写的参数更新到数据库中。

至于参数预填,参考上一小节的参数验证,也不难解决。我们同样可以在 ParametersDefinitionProperty 对应的 index.jelly 中添加 JS 实现:每次构建参数页面出来前,通过 Ajax 方法到数据库中获取上一次的参数值,填充到对应参数中。填充过程中页面被灰层遮罩,无法操作;预填结束后,灰层消失,用户可根据需要更新参数。由于具体实现方法类似,故不再赘述。

生成构建报告

上文中提到,不管是使用 RTC 自带的构建引擎,还是使用其他构建系统,比如 Jenkins,我们都需要考虑如何生成详细的构建结果。下面将介绍如何使用 RTC Plain Java API 生成构建报告。

下图是 RTC 提供的 Plain Java API(感兴趣的读者可以在 jazz 平台下载),其中 Source Control API 包含了三个用于获取工作项(Work Item)信息的接口:

  • IWorkspaceConnection:用来与 Workspace 建立连接。只有建立连接后,才能操作这个 Workspace 中的内容;
  • IChangeHistorySyncReport:用来获取所有变更集信息,包括 Outgoing 变更和 Incoming 变更;
  • IWorkspaceManager:用来获取变更集的 link 信息,由 link 信息再获取变更集关联的工作项(Work Item)信息。
图 17. Plain Java API

Plain Java API

要获取当前构建包含的所有代码修改,以及关联的工作项(Work Item),大致步骤如下:

  1. 获取从上次构建到当前构建期间的所有变更历史信息。根据清单 4 的代码,可以获取从上次构建到当前构建期间的所有变更集(Change Sets);
  2. 获取变更集关联的工作项(Work Item),详见清单 5。
清单 4. 获取变更历史
......
List<IWorkspaceHandle> workspaceHandleList = 
 workspaceManager.findWorkspacesContainingComponent(......);
IWorkspaceConnection workspaceConnection = 
 workspaceManager.getWorkspaceConnection(workspaceHandleList.get(0), null);
IChangeHistorySyncReport rep = 
 workspaceConnection.compareToBaseline(......);
List<IChangeSetHandle> outgoting = rep.outgoingChangeSets(IComponentHandle)
......
清单 5. 获取工作项信息
......
IWorkspaceManager wm = SCMPlatform.getWorkspaceManager(ITeamRepository repo);
List<IChangeSetLinkSummany> links = wm.getChangeSetLinkSummary(......);
for(IChangeSetLinkSummany summary : links) {
 List<ILinkHandle> linkHandles = summary.getLinks();
 for(ILinkHandle linkHandle : linkHandles) {
 ILink link = (ILink) repo.itemManager().fetchCompleteItem(......);
 IReference srcRef = link.getSourceRef();
 Object csObj = srcRef.resolve();
 IReference tarRef = link.getTargetRef();
 Object wiObj = tarRef.resolve();
 
 if((wiObj instanceof IWorkItemHandle) && (csObj instanceof IChangeSetHandle)) {
 IWorkItemHandle itemHandle = (IWorkItemHandle) wiObj;
 IChangeSetHandle changesetHandle = (IChangeSetHandle) csObj;
 IWorkItem workItem = (IWorkItem) repo.itemManager().getchCompleteItem(......);
 IChangeSet set = (IChangeSet) repo.itemManager().fetchCompleteItem(......);
 
 WorkItemInfo info = null;
 if(infosMap.containsKey(workItem)){
 info = infosMap.get(workItem);
 info.getHands().add(set);
 } else {
 IContributor owner = (IContributor)repo.itemManager().fetchCompleteItem(......);
 info = new WorkItemInfo();
 info.setItem(workItem);
 info.setOwner(owner.getName());
 info.getHands().add(set);
 }
 infosMap.put(workItem, info);
 }
 }
}
......

回页首

总结

本文从 SaaS 应用的基本特征入手,介绍了笔者在工作中遇到的一个比较复杂的 SaaS 应用。随后列举了在使用 RTC 和 Jenkins 对这个 SaaS 项目作持续集成过程中遇到的几个比较典型的问题。最后针对这些问题,逐一进行分析,并给出了笔者在实际工作中采用的解决方案,以供读者参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值