本文将介绍如何利用 Gitlab API 实现一套简单灵活的数据同步机制,从而实现在多个 Gitlab 站点间同步数据。

需求描述

在继续写数学系列前,我想切回去之前的 Git 系列写点东西。我想写系列文章也可以像操作系统的进程调度一样,一个系列暂时写不动了,先 保存现场 跳去另一个 topic 写点东西,同时也给自己留点 buffer 再酝酿一下这个暂时 中断 的系列。等这个系列酝酿够了,再 恢复现场 ,继续还这个系列的技术债。

对于一个规模较大的企业,存在多个 Gitlab 站点是很常见的事情。

比如,我们团队在公司发布统一的 Gitlab 之前早已经搭了一个团队用的 Gitlab ,当公司开始推 Git 时,由于我们已经对自己团队的 Gitlab 做了大量的定制,因此并不打算迁移到公司的 Gitlab 。

自己搭建 Gitlab 的好处是可以随心所欲的进行定制,像加远程钩子之类的东西想加就加。但缺点就是平台的维护成本也落到了自己身上。相比之下,公司 Gitlab 则没有什么维护成本,服务的稳定性由更专业的运维人员保证,也不用考虑扩容的问题,但灵活定制就别想了。如果能够实现 Gitlab 间的数据自动同步,我们可以没有顾忌的使用自己的 Gitlab 平台,一旦出现问题,再无痛迁移到公司的 Gitlab 。这样一方面避免了单点问题,节省了维护成本;另一方面也能尽可能保证灵活可定制。本文想讨论的就是多个 Gitlab 站点间的数据同步问题。

要实现数据同步,Gitlab 官方提供了一套 备份恢复机制 。但这套机制并不能很好地满足我们的需求:

  1. 需要两台机器的管理员权限。进行备份和恢复的机器都需要能 SSH 进去执行操作。而我们是不可能拥有公司 Gitlab 的管理员权限的;
  2. 会覆盖目标站点的数据。在恢复数据时,目标站点原有的一切数据都会被覆盖。而公司的 Gitlab 有很多个团队的数据,我们的同步不能影响到其他团队的数据;
  3. Gitlab 版本兼容问题。Gitlab 的备份机制要求原站点和目标站点的 Gitlab 版本兼容,否则将恢复失败。而我们的 Gitlab 版本和公司的 Gitlab 版本并不相同,日后存在一方升级导致无法同步的可能。

出于以上的考虑,我们自己设计了一套同步工具。与 Gitlab 官方的备份恢复机制相比,它具有以下一些优点:

  1. 无需 ssh 账户权限。所有操作都通过 Gitlab API 和 Git 操作完成,不用 ssh 登录到机器进行操作;
  2. 同步数据类型灵活。可以选择同步组织、仓库代码、wiki、组织成员关系、权限控制信息等类型的数据;
  3. 不覆盖目标站点数据。只对目标站点相同组织内的数据进行同步,不影响其他团队的数据;
  4. 没有 Gitlab 版本兼容问题。同步过程利用了 Gitlab API ,而 Gitlab API 比 Gitlab 稳定,因此版本兼容问题比较少见。即使出现 API 接口的变更,也可以通过升级工具的接口调用来实现兼容。

下面将逐步说明整套同步的方案。为了方便描述,我把同步原 Gitlab 站点称为 A Gitlab,把同步目标站点称为 B Gitlab 。

数据的自动同步主要经历如下几步:

  1. 同步所有组织,如果建立了新组织,将自动给 B Gitlab 添加该组织;
  2. 同步所有组织的所有仓库的代码和 wiki 到 B Gitlab 。
  3. 同步所有用户的组织关系。
  4. 同步所有仓库的权限控制信息。

组织同步

利用 Gitlab API 列举出 A Gitlab 中的所有 groups,然后在 B Gitlab 中自动新建不存在的组织。

列举 Gitlab 的所有组织:

1
GET /groups

返回示例:

1
2
3
4
5
6
7
8
[
{
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
"description": "An interesting group"
}
]

根据这个可以获取组织名(name)、组织路径(path)和组织描述(description)。

同样使用类似接口获取 B Gitlab 的所有组织。如果发现 A Gitlab 的某个组织在 B Gitlab 里不存在,可以在 B Gitlab 新增一个组织:

1
POST /groups

参数:

  • name(必须)- 组织名
  • path(必须)- 组织路径
  • description(可选)- 可选组织描述

仓库代码同步

同步所有组织的所有仓库的代码和 wiki 文档到 B Gitlab 。

获取一个组织的所有仓库信息接口:

1
GET /groups/:id/projects

参数:

  • archived (可选) - 只找出已被归档的项目
  • order_by (可选) - 选择基于 id, name, path, created_at, updated_atlast_activity_at 字段来排序。默认使用 created_at
  • sort (可选) - 选择升序还是降序排列。默认为降序。
  • search (可选) - 构造一个搜索条件过滤数据。
  • ci_enabled_first (可选) - 将结果根据是否带有 ci_enabled 字段来排序。

返回示例:

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
[
{
"id": 4,
"description": null,
"default_branch": "master",
"public": false,
"visibility_level": 0,
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
"tag_list": [
"example",
"disapora client"
],
"owner": {
"id": 3,
"name": "Diaspora",
"created_at": "2013-09-30T13: 46: 02Z"
},
"name": "Diaspora Client",
"name_with_namespace": "Diaspora / Diaspora Client",
"path": "diaspora-client",
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true,
"merge_requests_enabled": true,
"builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
"creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
"id": 3,
"name": "Diaspora",
"owner_id": 1,
"path": "diaspora",
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
}
]

之后利用同个接口结合 search 参数判断 B Gitlab 上的该组织是否存在同名项目。

如果不存在该项目,可以导入该项目:

1
POST /projects

参数:

  • name (必须) - 项目名
  • path (可选) - 仓库的路径。默认和项目名相同。
  • namespace_id (可选) - 新项目的所属的id。这里设为A Gitlab中拥有该项目的id。
  • description (可选) - 项目的描述。这里设为A Gitlab中该项目的描述。
  • issues_enabled (可选) - 是否开启 issue 。
  • merge_requests_enabled (可选)
  • builds_enabled (可选)
  • wiki_enabled (可选)
  • snippets_enabled (可选)
  • public (可选) - 如果为 true ,相当于设置 visibility_level 为 20
  • visibility_level (可选) - 项目可见度。这里设为A Gitlab中该项目的可见度。
  • import_url (optional) - 导入地址。这里设为A Gitlab中该项目的 http 地址。

完成后 B Gitlab 即会导入 A Gitlab 中的对应仓库。

如果该项目已存在,可以利用我开源的一个 代码同步工具 来实现两个仓库之间所有分支的同步。

用户组织关系同步

根据 A Gitlab ,将 B Gitlab 的已激活用户添加到组织中。并从 B Gitlab 删除 A Gitlab 中已 block 或者已移除的用户。

这里要注意的是两个站点间的用户的关联问题。我们的 Gitlab 在一开始就要求使用公司邮箱注册,而公司的 Gitlab 同样也是使用邮箱的 LDAP 账户体系,因此可以利用邮箱来关联两个站点间的账户。

获取 Gitlab 某个组织的所有用户:

1
GET /groups/:id/members

返回结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"id": 1,
"username": "raymond_smith",
"email": "ray@smith.org",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
},
{
"id": 2,
"username": "john_doe",
"email": "joh@doe.org",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
"access_level": 30
}
]

找出两个 Gitlab 上用户的差异,并执行如下操作:

  1. 如果 B Gitlab 比 A Gitlab 多出了一些成员,将该成员删除;
  2. 如果 A Gitlab 的某个用户 state 字段为 blocked ,则该成员可能已离职或 transfer,将该成员从 B Gitlab 中删除;
  3. 如果 A Gitlab 上某个用户在 B Gitlab 上不存在,则可能是新成员,尝试添加该成员。
  4. 如果某一用户在两个 Gitlab 上的权限等级不一样,则该用户的等级可能经过修改,需要同步该权限等级到 B Gitlab 。

添加组织成员的 API :

1
POST /groups/:id/members

参数:

  • id (必须) - 组织的 id ,也可以是路径;
  • user_id (必须) - 用户的 ID ,即 UM 账号
  • access_level (必须) - 权限等级

删除组织成员的 API :

1
DELETE /groups/:id/members/:user_id

编辑组织成员的 API :

1
PUT /groups/:id/members/:user_id

另外还需要考虑 B Gitlab 不存在该用户的情况,需做容错处理。

项目权限控制信息同步

项目的权限控制信息主要包括项目成员设定及分支保护设定。

项目成员同步

项目成员的同步与组织成员的同步大同小异。

获取项目成员的 API :

1
GET /projects/:id/members

添加项目成员的 API :

1
POST /projects/:id/members

删除项目成员的 API :

1
DELETE /projects/:id/members/:user_id

编辑项目成员的 API :

1
PUT /projects/:id/members/:user_id

分支保护同步

首先获取 A Gitlab 中一个仓库的所有分支:

1
GET /projects/:id/repository/branches

返回示例:

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
[
{
"name": "async",
"commit": {
"id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
"parents": [
{
"id": "3f94fc7c85061973edc9906ae170cc269b07ca55"
}
],
"tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee",
"message": "give Caolan credit where it's due (up top)",
"author": {
"name": "Jeremy Ashkenas",
"email": "jashkenas@example.com"
},
"committer": {
"name": "Jeremy Ashkenas",
"email": "jashkenas@example.com"
},
"authored_date": "2010-12-08T21:28:50+00:00",
"committed_date": "2010-12-08T21:28:50+00:00"
},
"protected": false
},
{
"name": "gh-pages",
"commit": {
"id": "101c10a60019fe870d21868835f65c25d64968fc",
"parents": [
{
"id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
}
],
"tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a",
"message": "Underscore.js 1.5.2",
"author": {
"name": "Jeremy Ashkenas",
"email": "jashkenas@example.com"
},
"committer": {
"name": "Jeremy Ashkenas",
"email": "jashkenas@example.com"
},
"authored_date": "2013-09-07T12: 58: 21+00: 00",
"committed_date": "2013-09-07T12: 58: 21+00: 00"
},
"protected": false
}
]

其中 protected 表示该分支是否受到保护。

根据分支的保护情况修改 B Gitlab 上的分支。

保护某个分支:

1
PUT /projects/:id/repository/branches/:branch/protect

取消某个分支的保护:

1
PUT /projects/:id/repository/branches/:branch/unprotect

总结

对于同时搭建了多个 Gitlab 的团队,多个 Gitlab 间的数据同步是值得去实现的事情。它一方面能避免单点问题,降低小团队的维护成本,一方面也能尽量保证小团队的定制灵活性。因此,文本列举了组织同步、仓库代码和wiki同步、组织关系同步、权限控制信息同步等四大方面的同步的方案。我将这四个类型的同步可以写成了三个工具:

  • group_sync - 处理组织同步
  • project_sync - 处理项目代码、wiki同步
  • member_sync - 处理组织关系、权限控制信息的同步

设定每天自动按顺序执行这几个工具的同步,完成后邮件汇报同步结果。作为实例,这是我们每天都会收到的同步结果邮件(出于保护隐私的考虑,我修改了部分隐私信息):

由于项目变动、成员变动比较频繁,当希望在计划任务之前进行某方面同步,仍然可以单独手动运行以上工具完成所需方面的同步。对于一些同步及时性要求更高的仓库,则可以通过加 post-receive 钩子调用 代码同步工具 来实现 push 后即时同步。

要注意的是,这个同步方案并没有保证 A Gitlab 的所有数据都能被完整地同步。在设计同步策略的时候,我跳过了下述类型的同步:

  1. 用户私有仓库。这些仓库只是个人仓库,不会对组织财产造成影响。且如果要同步私有仓库,则要求两个 Gitlab 站点的账户都为管理员,因为只有管理员才能访问所有用户的私有仓库。
  2. SSH key。用户添加的所有 SSH key 无法同步。
  3. 头像。组织、用户、仓库的头像未做同步。
  4. issue。由于我们的 Gitlab 并不用来进行 bug 跟踪管理,所以我跳过了这方面的同步。读者也可以利用 Gitlab API 实现 issue 的同步。
  5. 附件。Wiki 中的附件是独立于仓库之外的,需要单独备份。例如使用 rsync