一、背景
在devops整个流程中,gerrit把关代码的提交与评审,当某开发人员提交代码后,首先需要通过jenkins的自动化verify(对Verified+1),此时发送消息给gerrit组内的所有人员,告知他们有代码需要评审;其次,当代码评审人员在添加评审人或打分的时候,发送消息给代码提交者。
下面截图,说明下最后提到的两个地方:“添加评审人”、“打分”。
另外,我们在OA管理后台中,申请新项目的时候,当流程通过后,需要自动创建gitlab和gerrit项目。
最后,再补充一张图,说明代码评审通过后的展示:
图中指出了代码提交者,对应Owner字段,代码评审人。代码评审,分为两步,先jenkins自动化验证,后人工进行评审。
- 第一次是jenkins,可以从Reviewers列表和Verified中看到,好处是快速诊断是否符合规范、是否通过自动化测试等等。最近比较火的chatgpt,也可以做代码评审;
- 第二次是同事之间的人工评审。
二、目标
- 1、对接OA管理后台系统,自动创建gerrit项目,包括赋权限。
- 2、发送消息提醒其他同事来评审你所提交的增量代码。
- 3、把代码评审的结果及时通知提交代码者。
三、总体设计
注: webhook回调是三个流程都会共用。这里分为三类角色,申请新项目、代码提交者、代码评审者。(他们都属于gerrit组内的用户,我们为了让消息更加精准地发送到不同的人,不是盲目地发送给gerrit组内的所有人)
四、自动创建gerrit项目
分为三步:第一、创建负责人组;第二、创建开发者组;第三、创建项目;第四、给项目赋权限。
4.1、调用gerrit api创建的示例
#!/bin/bash
#
# 创建gerrit项目
#
# ./create_gerrit_project.sh user-service
#
# 参数说明:
#
# service-name: 项目名称
project_name=$1
die() {
echo $1
exit 1
}
[[ -z "$project_name" ]] && die "请输入项目名称"
group_owner_id="34dd8f718972d73c9a3474b2sfad2d52x2cf596f"
GERRIT_USER_PWD="admin:123456"
GERRIT_ADDR="http://192.168.5.60:8080/a"
# 1、创建负责人分组
owner_group_name="$project_name-owner"
rsp=$(curl -u $GERRIT_USER_PWD -s -X PUT "${GERRIT_ADDR}/groups/${owner_group_name}" -H "content-type: application/json" -d "{ \"description\": \"${owner_group_name}\", \"visible_to_all\": false, \"owner_id\": \"${group_owner_id}\"}")
owner_group_id=$(echo "${rsp:5}" | jq '.id' | sed 's#"##g')
# 2、创建开发者分组
dev_group_name="$project_name-dev"
rsp=$(curl -u $GERRIT_USER_PWD -s -X PUT "${GERRIT_ADDR}/groups/${dev_group_name}" -H "content-type: application/json" -d "{ \"description\": \"${dev_group_name}\", \"visible_to_all\": false, \"owner_id\": \"${owner_group_id}\"} ")
dev_group_id=$(echo "${rsp:5}" | jq '.id' | sed 's#"##g')
# 3、创建项目
real_project_name="root%2F${project_name}"
rsp=$(curl -u $GERRIT_USER_PWD -s -X PUT "${GERRIT_ADDR}/projects/${real_project_name}" -H "content-type: application/json" -d "{ \"description\": \"${project_name}\", \"submit_type\": \"INHERIT\", \"owners\": [ \"${owner_group_name}\" ]} ")
echo $rsp
# 4、设置权限
rsp=$(curl -u $GERRIT_USER_PWD -s -X POST "${GERRIT_ADDR}/projects/${real_project_name}/access" \
-H "content-type: application/json" \
-d "{ \"add\": { \
\"refs/*\": { \
\"permissions\": { \
\"read\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"label-Verified\": { \
\"label\": \"Verified\", \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false, \
\"min\": -1, \
\"max\": 1 \
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false, \
\"min\": -1, \
\"max\": 1 \
} \
} \
}, \
\"labelAs-Verified\": { \
\"label\": \"Verified\", \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false, \
\"min\": -1, \
\"max\": 1 \
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false, \
\"min\": -1, \
\"max\": 1 \
} \
} \
}, \
\"label-Code-Review\": { \
\"label\": \"Code-Review\",
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"min\": -2,
\"max\": 2
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"min\": -1,
\"max\": 1
} \
} \
}, \
\"forgeServerAsCommitter\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"forgeAuthor\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"removeReviewer\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"delete\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"viewPrivateChanges\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"createTag\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"createSignedTag\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"editHashtags\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"deleteOwnChanges\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"editTopicName\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"abandon\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"force\": false \
} \
} \
}, \
\"push\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"addPatchSet\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"forgeCommitter\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"pushMerge\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"rebase\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"submit\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"submitAs\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
} \
} \
}, \
\"refs/tags/*\": { \
\"permissions\": { \
\"createSignedTag\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"createTag\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
} \
} \
}, \
\"refs/heads/*\": { \
\"permissions\": { \
\"forgeCommitter\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"forgeAuthor\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"editTopicName\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"label-Code-Review\": { \
\"label\": \"Code-Review\",
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\", \
\"min\": -2,
\"max\": 2
}, \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\", \
\"min\": -1,
\"max\": 1
} \
} \
}, \
\"create\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"push\": { \
\"rules\": { \
\"${dev_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
}, \
\"submit\": { \
\"rules\": { \
\"${owner_group_id}\": { \
\"action\": \"ALLOW\" \
} \
} \
} \
} \
} \
} \
}")
echo $rsp
4.2、java语言实现
- 引入jar包
<dependency>
<groupId>com.urswolfer.gerrit.client.rest</groupId>
<artifactId>gerrit-rest-java-client</artifactId>
<version>0.9.3</version>
</dependency>
- 配置
project:
gerrit:
host: http://192.168.5.60:8080
login: admin
password: 123456
# 负责人
groupOwnerId: 34dd8f718972d73c9a3474b2sfad2d52x2cf596f
- 实现类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* sonar配置
*
* @author xxx
*/
@RefreshScope
@Data
@Configuration
@ConfigurationProperties(prefix = "project.gerrit")
public class GerritProperties {
/**
* 地址
*/
private String host;
/**
* 用户名
*/
private String login;
/**
* 密码
*/
private String password;
/**
* 所属的组ID
*/
private String groupOwnerId;
}
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.access.AccessSectionInfo;
import com.google.gerrit.extensions.api.access.PermissionInfo;
import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.urswolfer.gerrit.client.rest.GerritAuthData;
import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
import com.urswolfer.gerrit.client.rest.http.HttpStatusException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* gerrit api
* <p>
* 1.创建负责人组
* 2.创建开发人组
* 3.创建工程
* 4.设置权限
* </p>
*
* @author xxx
*/
@Slf4j
@Service
public class GerritService {
private GerritRestApiFactory gerritRestApiFactory;
private GerritAuthData.Basic authData;
private GerritApi gerritApi;
@Autowired
private GerritProperties gerritProperties;
@PostConstruct
public void init() {
gerritRestApiFactory = new GerritRestApiFactory();
authData = new GerritAuthData.Basic(gerritProperties.getHost(), gerritProperties.getLogin(), gerritProperties.getPassword());
gerritApi = gerritRestApiFactory.create(authData);
}
/**
* 新建gerrit项目及组、权限
*
* @param group 项目组
* @param projectName 项目名称,比如:root/watch-answering-server
*/
public void create(String group, String projectName) {
String ownerGroupName = projectName + "-owner";
String devGroupName = projectName + "-dev";
try {
// 创建负责人分组
GroupBaseInfo ownerGroupBaseInfo = this.createGroup(ownerGroupName, gerritProperties.getGroupOwnerId());
String ownerGroupId = ownerGroupBaseInfo.id;
log.info("负责人分组ID:{}", ownerGroupId);
// 创建开发者分组
GroupBaseInfo devGroupBaseInfo = this.createGroup(devGroupName, ownerGroupId);
String devGroupId = devGroupBaseInfo.id;
log.info("开发者分组ID:{}", devGroupId);
// 创建工程
ProjectInfo projectInfo = this.createProject(projectName, ownerGroupName);
log.info("项目的详情是:{}", JsonUtils.toJsonString(projectInfo));
// 赋权限项
if (null != projectInfo) {
this.setAccess(projectName, ownerGroupId, devGroupId);
}
} catch (Exception e) {
log.error("创建组、工程、赋权出现了异常, projectName={}", projectName, e);
}
}
/**
* 新建工程.
*
* @param projectName 工程名
* @param ownerGroupName 所属组
* @return
* @throws Exception
*/
private ProjectInfo createProject(String projectName, String ownerGroupName) throws Exception {
try {
ProjectApi projectApi = gerritApi.projects().name(projectName);
ProjectInfo projectInfo = projectApi.get();
if (null != projectInfo) {
log.warn("工程{}已存在,请勿重复创建", projectName);
return projectInfo;
}
} catch (Exception e) {
ProjectInput request = new ProjectInput();
request.name = projectName;
request.description = projectName;
request.submitType = SubmitType.INHERIT;
request.owners = Lists.newArrayList(ownerGroupName);
log.info("新建gerrit工程成功, request={}", JsonUtils.toJsonString(request));
return gerritApi.projects().create(request).get();
}
return null;
}
/**
* 创建组.
*
* @param groupName 组名
* @param ownerId 所属组
* @return
* @throws Exception
*/
private GroupBaseInfo createGroup(String groupName, String ownerId) throws Exception {
GroupBaseInfo groupBaseInfo = new GroupBaseInfo();
GroupInfo groupInfo;
try {
groupInfo = gerritApi.groups().id(groupName).get();
log.warn("组{}已存在,请勿重复创建", groupName);
} catch (HttpStatusException e) {
// 不存在,则重新创建
GroupInput groupInput = new GroupInput();
groupInput.name = groupName;
groupInput.description = groupName;
groupInput.ownerId = ownerId;
groupInput.visibleToAll = false;
groupInfo = gerritApi.groups().create(groupInput).get();
if (null == groupInfo) {
throw new IllegalArgumentException("创建组出现异常");
}
log.info("组创建成功, groupInput={}", JsonUtils.toJsonString(groupInput));
groupBaseInfo.id = groupInfo.id;
groupBaseInfo.name = groupInfo.name;
return groupBaseInfo;
}
groupBaseInfo.id = groupInfo.id;
groupBaseInfo.name = groupInfo.name;
return groupBaseInfo;
}
/**
* 赋权限.
*
* @param projectName 项目名称
* @param ownerGroupId 负责人组ID
* @param devGroupId 开发组ID
* @throws Exception
*/
private void setAccess(String projectName, String ownerGroupId, String devGroupId) throws Exception {
try {
ProjectApi projectApi = gerritApi.projects().name(projectName);
ProjectAccessInput projectAccessInput = new ProjectAccessInput();
Map<String, AccessSectionInfo> addMap = Maps.newHashMap();
addMap.put("refs/*", buildAccessSectionInfo(ownerGroupId, devGroupId));
addMap.put("refs/tags/*", buildTagsAccessSectionInfo(devGroupId));
addMap.put("refs/heads/*", buildHeadsAccessSectionInfo(ownerGroupId, devGroupId));
log.info("权限列表是:{}", JsonUtils.toJsonString(addMap));
projectAccessInput.add = addMap;
projectApi.access(projectAccessInput);
} catch (HttpStatusException e) {
log.error("赋权限出现异常, projectName={}, ownerGroupId={}, devGroupId={}", projectName, ownerGroupId, devGroupId, e);
}
}
private AccessSectionInfo buildAccessSectionInfo(String ownerGroupId, String devGroupId) {
AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
Map<String, PermissionInfo> permissions = Maps.newHashMap();
permissions.put("read", buildPermissionInfo("", devGroupId));
permissions.put("label-Code-Review", buildReviewPermissionInfo("Code-Review", ownerGroupId, devGroupId));
permissions.put("label-Verified", buildReviewPermissionInfo("Verified", ownerGroupId, devGroupId));
permissions.put("labelAs-Verified", buildReviewPermissionInfo("Verified", ownerGroupId, devGroupId));
permissions.put("forgeServerAsCommitter", buildPermissionInfo("", ownerGroupId, devGroupId));
permissions.put("forgeAuthor", buildPermissionInfo("", ownerGroupId, devGroupId));
permissions.put("removeReviewer", buildPermissionInfo("", ownerGroupId, devGroupId));
permissions.put("delete", buildPermissionInfo("", ownerGroupId));
permissions.put("viewPrivateChanges", buildPermissionInfo("", ownerGroupId));
permissions.put("createTag", buildPermissionInfo("", ownerGroupId));
permissions.put("createSignedTag", buildPermissionInfo("", ownerGroupId));
permissions.put("editHashtags", buildPermissionInfo("", ownerGroupId));
permissions.put("deleteOwnChanges", buildPermissionInfo("", devGroupId));
permissions.put("editTopicName", buildPermissionInfo("", devGroupId));
permissions.put("abandon", buildPermissionInfo("", devGroupId));
permissions.put("push", buildPermissionInfo("", devGroupId));
permissions.put("addPatchSet", buildPermissionInfo("", devGroupId));
permissions.put("forgeCommitter", buildPermissionInfo("", devGroupId));
permissions.put("pushMerge", buildPermissionInfo("", devGroupId));
permissions.put("rebase", buildPermissionInfo("", devGroupId));
permissions.put("submit", buildPermissionInfo("", ownerGroupId));
permissions.put("submitAs", buildPermissionInfo("", ownerGroupId));
accessSectionInfo.permissions = permissions;
return accessSectionInfo;
}
private AccessSectionInfo buildTagsAccessSectionInfo(String devGroupId) {
AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
Map<String, PermissionInfo> permissions = Maps.newHashMap();
permissions.put("createSignedTag", buildPermissionInfo("", devGroupId));
permissions.put("createTag", buildPermissionInfo("", devGroupId));
accessSectionInfo.permissions = permissions;
return accessSectionInfo;
}
private AccessSectionInfo buildHeadsAccessSectionInfo(String ownerGroupId, String devGroupId) {
AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
Map<String, PermissionInfo> permissions = Maps.newHashMap();
permissions.put("forgeCommitter", buildPermissionInfo("", devGroupId));
permissions.put("label-Code-Review", buildReviewPermissionInfo("Code-Review", ownerGroupId, devGroupId));
permissions.put("forgeAuthor", buildPermissionInfo("", devGroupId));
permissions.put("editTopicName", buildPermissionInfo("", devGroupId));
permissions.put("create", buildPermissionInfo("", devGroupId));
permissions.put("push", buildPermissionInfo("", devGroupId));
permissions.put("submit", buildPermissionInfo("", ownerGroupId));
accessSectionInfo.permissions = permissions;
return accessSectionInfo;
}
private PermissionInfo buildPermissionInfo(String label, String groupId) {
PermissionInfo permissionInfo = new PermissionInfo(label, false);
Map<String, PermissionRuleInfo> rules = buildRules(groupId);
permissionInfo.rules = rules;
return permissionInfo;
}
private PermissionInfo buildPermissionInfo(String label, String ownerGroupId, String devGroupId) {
PermissionInfo permissionInfo = new PermissionInfo(label, false);
Map<String, PermissionRuleInfo> rules = buildRules(ownerGroupId, devGroupId);
permissionInfo.rules = rules;
return permissionInfo;
}
private PermissionInfo buildReviewPermissionInfo(String label, String ownerGroupId, String devGroupId) {
PermissionInfo permissionInfo = new PermissionInfo(label, false);
Map<String, PermissionRuleInfo> rules = buildReviewRules(ownerGroupId, devGroupId);
permissionInfo.rules = rules;
return permissionInfo;
}
private Map<String, PermissionRuleInfo> buildRules(String groupId) {
Map<String, PermissionRuleInfo> rules = Maps.newHashMap();
PermissionRuleInfo permissionRuleInfo = buildPermissionRuleInfo();
rules.put(groupId, permissionRuleInfo);
return rules;
}
private Map<String, PermissionRuleInfo> buildRules(String ownerGroupId, String devGroupId) {
Map<String, PermissionRuleInfo> rules = Maps.newHashMap();
PermissionRuleInfo permissionRuleInfo = buildPermissionRuleInfo();
rules.put(ownerGroupId, permissionRuleInfo);
rules.put(devGroupId, permissionRuleInfo);
return rules;
}
private Map<String, PermissionRuleInfo> buildReviewRules(String ownerGroupId, String devGroupId) {
Map<String, PermissionRuleInfo> rules = Maps.newHashMap();
PermissionRuleInfo permissionRuleInfo = buildPermissionRuleInfo();
permissionRuleInfo.max = 2;
permissionRuleInfo.min = -2;
rules.put(ownerGroupId, permissionRuleInfo);
permissionRuleInfo = buildPermissionRuleInfo();
permissionRuleInfo.max = 1;
permissionRuleInfo.min = -1;
rules.put(devGroupId, permissionRuleInfo);
return rules;
}
private PermissionRuleInfo buildPermissionRuleInfo() {
PermissionRuleInfo permissionRuleInfo = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
return permissionRuleInfo;
}
}
五、测试
@Autowired
private GerritService gerritService;
@ApiOperation(value = "新建gerrit工程")
@PostMapping(value = "/api/gerrit/create/{projectName}")
public ResponseEntity<?> createProject(@NotNull @PathVariable String projectName) {
// 新建gerrit工程
gerritService.create("", "root/" + projectName);
return ResponseEntity.ok("SUCCESS");
}
写在最后的话
本文是对接gerrit的设计与实现的第一篇,由于我把详细的实现都完整贴出,导致篇幅超出预期,故将之拆分为多篇。后续将接着完成后面的设计……
另外,gerrit工程创建后,申请新项目的人,如果不在上面的组34dd8f718972d73c9a3474b2sfad2d52x2cf596f内,你会看不到已创建好的gerrti工程!!不要慌,让34dd8f718972d73c9a3474b2sfad2d52x2cf596f组里的人员给你赋权下哈。
- 所以,这里就还是有一个前提条件,你得有个管理员组34dd8f718972d73c9a3474b2sfad2d52x2cf596f,需要手动加人到组里。
- 这么说,本文只实现了自动创建gerrit项目以及权限项,创建了owner组和dev组。因为缺少信息–每个组里应该添加哪些人。
- 后期再实现给组里加人的操作,这里先列出它的api接口,以供参考。