0%

gitlab 15.9 升级到 16.10

原因

在 15.9 版本中,gitlab ci 中的 job 无法在失败之后进行重试,表现为失败之后进入 pending 状态,一直持续。从而导致了在有时候 job 偶尔的失败需要手动去重试,非常不方便。有时候还会因为来不及手动重试会直接影响线上服务。

gitlab 15.9 使用的 docker 配置

gitlab 使用的是 docker 部署,docker-compose 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gitlab:
container_name: gitlab
image: 'gitlab/gitlab-ce:15.9.3-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8200'
pages_external_url 'http://192.168.2.168:8300'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '8200:8200'
- '8300:8300'
- '2222:22'
volumes:
- '/usr/lnmp/gitlab/config:/etc/gitlab'
- '/usr/lnmp/gitlab/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab/data:/var/opt/gitlab'
shm_size: '256m'

要解决的几个关键问题

因为是 docker 配置,所以本来打算直接用旧的文件夹来启动一个新版本的容器,不过在启动新版本的容器的时候起不来。 因为版本跨度过大,可能中间太多不兼容,最后决定起一个新的容器,然后把旧的数据手动迁移过去。

在本次迁移中,为了保证旧的用户密码在迁移后依然可用,直接使用了旧的配置文件来启动新的 gitlab 容器,也就是上面 docker-compose 配置中的 /user/lnmp/gitlab/config 这个文件夹。

gitlab 没有什么工具可以用来迁移数据的,所以比较麻烦。

在迁移过程中,有几个很关键的问题需要解决:

  1. 用户和组迁移
  2. 用户的 ssh key 迁移
  3. 项目迁移
  4. 用户权限迁移

用户权限这个没有什么好的办法,只能手动去设置。如果用户太多的话,可以看看 gitlab 有没有提供 API,使用它的 API 可能会方便一些。

其他的几个问题可以稍微简单一点,本文会详细介绍。

gitlab 获取 token 以及调用 API 的方法

我们需要使用管理员账号来创建 token,其他账号是没有权限的。

gitlab 提供了很多 REST API,可以通过这些 API 来获取到 gitlab 的数据,以及对 gitlab 进行操作。 我们的一些迁移操作就调用了它的 API 来简化操作,同时也可以避免人为操作导致的一些错误。

但是调用这些 API 之前,我们需要获取到一个 token,从 gitlab 个人设置中获取到 token,然后使用这个 token 来调用 API(Preferences -> Access Tokens,添加 token 的时候把所有权限勾上就好)。

通过 REST API 导出分组的示例:

1
curl --request POST --header "PRIVATE-TOKEN: glpat-L1efQKvKeWu" "http://192.168.2.168:8200/api/v4/groups/1/export"

说明:

  • PRIVATE-TOKEN header:就是我们在 gitlab 个人设置中获取到的 token。
  • /api/v4/groups/1/export:这个是 gitlab 的 API,可以通过这个 API 导出分组,这里的 1 是分组的 ID。

我们可以将这个 curl 命令通过使用自己熟悉的编程语言来调用,这样可以很方便地对获取到的数据进行后续操作。比如我在这个过程中就是使用 python 来调用 gitlab 的 API。

启动一个新的 gitlab 16.10 版本的容器

在开始迁移之前,需要先启动一个新的 gitlab 16.10 版本的容器,这个容器是全新的,没有任何数据。但配置文件是复制了一份旧的配置文件。新的 docker-compose.yml 配置文件如下:

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
gitlab:
container_name: gitlab
image: 'gitlab/gitlab-ce:15.9.3-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8200'
pages_external_url 'http://192.168.2.168:8300'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
ports:
- '8200:8200'
- '8300:8300'
- '2222:22'
volumes:
- '/usr/lnmp/gitlab/config:/etc/gitlab'
- '/usr/lnmp/gitlab/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab/data:/var/opt/gitlab'
shm_size: '256m'
networks:
- elasticsearch

gitlab-new:
container_name: "gitlab-new"
image: 'gitlab/gitlab-ce:16.10.0-ce.0'
restart: always
hostname: '192.168.2.168'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.2.168:8211'
pages_external_url 'http://192.168.2.168:8311'
gitlab_rails['gitlab_shell_ssh_port'] = 2233
ports:
- '8211:8211'
- '8311:8311'
- '2233:22'
volumes:
- '/usr/lnmp/gitlab-new/config:/etc/gitlab'
- '/usr/lnmp/gitlab-new/logs:/var/log/gitlab'
- '/usr/lnmp/gitlab-new/data:/var/opt/gitlab'
shm_size: '256m'
networks:
- elasticsearch

在个配置文件中:

  1. 旧的配置保持不变
  2. 旧的配置目录复制了一份 /usr/lnmp/gitlab/config => /usr/lnmp/gitlab-new/config
  3. /usr/lnmp/gitlab-new 目录下只有 config 文件夹,没有 logsdata 文件夹,这两个文件夹会在容器启动过程中生成。

这样我们就可以在旧的 gitlab 运行过程中,先进行用户、组等数据的迁移。

用户迁移

Gitlab 提供了获取用户信息的 API,可以通过这个 API 获取到用户的信息(从旧的 gitlab),然后再通过 API 创建用户(在新的 gitlab)。

我们可以在 https://docs.gitlab.com/16.10/ee/api/users.html 查看 gitlab 的用户 API 详细文档。

我们会用到它的几个 API:

  1. 获取用户信息:GET /users:通过这个 API 我们可以获取到所有用户的信息(从旧的 gitlab)。
  2. 获取用户的 ssh key:GET /users/:id/keys:通过这个 API 我们可以获取到用户的 ssh key(从旧的 gitlab)。
  3. 创建用户:POST /users:通过这个 API 我们可以创建用户(到新的 gitlab)。
  4. 创建用户的 ssh key:POST /users/:id/keys:通过这个 API 我们可以创建用户的 ssh key(到新的 gitlab)。

从旧的 gitlab 获取用户信息

完整代码太长了,这里只放出关键代码:

1
2
3
# http://192.168.2.168:8200 是旧的 gitlab 地址
response = requests.get("http://192.168.2.168:8200/api/v4/users?per_page=100", headers={'Private-Token': tk})
users = response.json()

这里的 tk 是我们在 gitlab 个人设置中获取到的 token。

它返回的数据格式如下(users):

1
2
3
4
5
6
7
8
9
10
11
[
{
"id": 33,
"username": "x",
"name": "x",
"state": "deactivated",
"avatar_url": null,
"web_url": "http://192.168.2.168:8200/x"
// 其他字段....
}
]

从旧的 gitlab 获取用户的 ssh key

在上一步获取到所有的用户信息之后,我们可以通过用户的 ID 来获取用户的 ssh key。

1
2
3
4
# http://192.168.2.168:8200 是旧的 gitlab 地址
for user in users:
ssh_keys_response = requests.get(f"http://192.168.2.168:8200/api/v4/users/{user['id']}/keys", headers={'Private-Token': tk})
user['keys'] = ssh_keys_response.json()

这里的 user['keys'] 就是用户的 ssh key 信息,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"user_id": 6,
"username": "ming",
"name": "ming",
"keys": [
{
"id": 24,
"title": "win7",
"created_at": "2023-04-10T03:15:54.428Z",
"expires_at": null,
"key": "ssh-rsa AAAAB3Nza...",
"usage_type": "auth_and_signing"
}
]
}
]

这一步,我们获取了用户的 ssh key 并关联到了 user 中了。

在新的 gitlab 上创建用户

在获取到了用户信息之后,我们就可以在新的 gitlab 上创建用户了。

1
2
3
4
5
6
7
8
9
10
11
12
# http://192.168.2.168:8211 是新的 gitlab 的地址
create_user_response = requests.post("http://192.168.2.168:8211/api/v4/users", headers={'Private-Token': tk_new}, json={
'username': user['username'],
"email": user['email'],
"name": user['name'],
"password": '12345678Git*',

'note': user['note'],
'bio': user['bio'],
'commit_email': user['commit_email'],
})
user['new_user'] = create_user_response.json()

这个接口会返回新创建的用户信息,格式如下,我们可以通过这个信息来获取到新创建的用户的 ID。

因为新旧 gitlab 的用户 ID 会不一样,所以这里需要获取新的 ID 来创建 ssh key。

1
2
3
4
5
6
7
8
9
10
{
"id": 2,
"username": "xx",
"name": "xx",
"state": "active",
"locked": false,
"avatar_url": "",
"web_url": "http://192.168.2.168:8211/",
// 其他字段
}

在新的 gitlab 上创建用户的 ssh key

创建了用户之后,我们就可以创建用户的 ssh key 了。

1
2
3
4
5
6
7
8
# http://192.168.2.168:8211 是新的 gitlab 的地址
for ssh_key in user['keys']:
create_keys_response = requests.post(
f"http://192.168.2.168:8211/api/v4/users/{user['new_user']['id']}/keys",
headers={'Private-Token': tk_new},
json={"title": ssh_key['title'],"key": ssh_key['key']}
)
print(create_keys_response.json())

到这里,我们就把用户迁移过来了。

新旧 gitlab 用户密码不一样的问题

在现代的应用中,密码一般会使用诸如 APP_KEY 这种应用独立的 key 来做一些加密,如果是不同的 key,那么加密出来的密码就会不一样。 出于这个考虑,我们在迁移的过程中,直接保留了旧的配置,里面包含了应用的一些 key,这样我们就可以直接从旧系统的数据库中提取出加密后的密码来使用, 因为加密的时候使用的 key 是一样的,所以放到新的系统中是可以直接使用的。

从旧的 gitlab 数据库中获取用户密码

  1. 进入 gitlab 容器内部:docker exec -it gitlab bash
  2. 进入 gitlab 数据库:gitlab-rails dbconsole
  3. 获取用户密码:select username, encrypted_password from users;

这会输出所有用户的用户名和加密后的密码,我们可以把这个密码直接放到新的 gitlab 中:

1
2
3
4
  username  |                      encrypted_password
------------+--------------------------------------------------------------
a | $2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm
b | $2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC

因为 username 是唯一的,所以我们可以通过 username 来找到新的 gitlab 中对应的用户(肉眼查找法),最后更新这个用户的 encrypted_password 字段即可。

当然,也可以复制出来做一些简单的文本处理:

1
2
3
4
5
6
7
8
9
10
11
12
text = """
a | $2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm
b | $2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC
"""

lines = text.strip().split("\n")
result = []
for line in lines:
kvs = [v.strip() for v in line.strip().split('|')]
result.append({kvs[0]: kvs[1]})

print(result)

这样我们就可以得到一个字典列表:

1
2
[{'a': '$2a$10$k1/Cj2qUCyWgwalCmzTFo.iqYgnqIoFmQVuT2S6mBuQF0Nql0CRGm'},
{'b': '$2a$10$iWWXEcPJpyZVxfmIU6nv6e46JRj2dYnwGP6GXclryEAeEXJvOZ5aC'}]

在新的 gitlab 中更新用户密码

同样地,需要进入新的 gitlab 容器内部,然后进入数据库,然后更新用户密码:

  1. 进入 gitlab 容器内部:docker exec -it gitlab-new bash
  2. 进入 gitlab 数据库:gitlab-rails dbconsole
  3. 获取用户密码:select id, username from users;

拿到上一步的字典后,我们可以通过 username 找到新的 gitlab 中对应的用户 ID,然后更新密码:

  1. 通过新的 gitlab 的用户 API 获取用户列表
  2. 循环这个用户列表,如果 username 在上一步的字典中,那么就更新这个用户的密码
1
2
3
4
# http://192.168.2.168:821 是新的 gitlab 地址
# tk_new 是新的 gitlab 上的 token
response = requests.get("http://192.168.2.168:8211/api/v4/users?per_page=100", headers={'Private-Token': tk_new})
users = response.json()

因为连接到 gitlab 的数据库又比较麻烦,所以这里只是生成了的 update 语句,然后手动在新的 gitlab 数据库中执行:

1
2
3
4
5
6
7
8
9
# users 是新 gitlab 中的用户
for user in users:
if user['username'] in result:
# 通过 username 找到旧的密码
encrypted_password = result[user['username']]
# 生成 update 语句
# 这个 update 语句会将新系统中的用户密码更新为旧系统中的密码
# 因为是使用相同的 key 加密的,所以迁移后也依然可以使用旧的密码来登录
print(f"update users set encrypted_password='{encrypted_password}', reset_password_token=null, reset_password_sent_at = null,confirmation_token=null,confirmation_sent_at=null where id={user['id']};")

这会生成一些 update 语句,我们可以复制出来在新的 gitlab 中执行(也就是本小节的 gitlab-rails dbconsole)。 到这一步,用户的迁移就算完成了。

Group 的迁移

Group 的迁移和用户的迁移类似,只是 Group 的 API 不同,我们可以在 https://docs.gitlab.com/ee/api/group_import_export.html 查看 gitlab 的 Group API 文档。

注意:如果我们的 Group 中除了项目,没什么东西的话,直接自己手动在 gitlab 上创建 Group,然后把项目迁移过去就好了。 使用它的 API 是因为它可以同时迁移:milestone、label、wiki、子 Group 等信息。

  1. 我们首先需要知道在旧的 gitlab 中的 Group ID,然后通过 API 导出 Group(旧的 gitlab)。
  2. 调用了导出的 API 之后,需要等待系统导出完成,然后下载导出的文件(旧的 gitlab)。
  3. 调用下载导出文件的 API,获取到导出的分组(旧的 gitlab)。
  4. 最后,调用导入分组的 API,将分组导入到新的 gitlab 中(新的 gitlab)。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 从旧的 gitlab 导出 Group,1 是 Group ID
curl --request POST --header "PRIVATE-TOKEN: glpat-abc" "http://192.168.2.168:8200/api/v4/groups/1/export"

# 下载导出的分组
# --output 指定下载的文件名
# `api/v4/groups/1/export/download` 中的 1 是 Group ID
curl --request GET \
--header "PRIVATE-TOKEN: glpat-abc" \
--output download_group_42.tar.gz \
"http://192.168.2.168:8200/api/v4/groups/1/export/download"

# 导入分组(在新的 gitlab)
# --form 指定 form 表单参数
# `name` 是新的 Group 名称(给人看的名称)
# `path` 是新的 Group 路径(体现在 git 仓库的路径上)
# `file` 是下载的文件(上一步导出的 Group 文件)
curl --request POST --header "PRIVATE-TOKEN: glpat-def" \
--form "name=g1" --form "path=g2" \
--form "file=@/Users/ruby/Code/devops/download_group_42.tar.gz" "http://192.168.2.168:8211/api/v4/groups/import"

这样就可以把 Group 迁移过来了。

项目迁移

项目的迁移没有找到什么好的方法,只能手动迁移了。

gitlab 的项目因为有很多分支、release、issue,所以不能只是简单地把 git 仓库拷贝过去就好了,还需要把这些信息也迁移过去。 这就需要将项目导出,然后导入到新的 gitlab 中。

具体操作步骤如下:

  1. 在项目主页的 Settings -> General -> Advanced -> Export project 中导出项目
  2. 点击导出之后,需要等待导出完成,然后下载导出的文件,还是在 Settings -> General -> Advanced -> Export project 中,点击下载
  3. 在新的 gitlab 中,点击 New Project,然后选择 Import project,选择上一步下载的文件,导入项目(导入的类型选择 Gitlab export
  4. 填写项目名,命名空间选择跟旧的 gitlab 一样的 Group,选择上一步下载的文件,然后点击 Import project,等待导入完成即可

权限迁移

这个也是得手动设置,没有什么好的办法。 也许有 API,但是用户少的时候,还是手动设置更快。

gitlab runner 迁移

这个也是看着旧的 gitlab runner 配置,手动配置一下就完了,没几个。

需要注意的是,gitlab runner 配置的 docker 类型的 runner 的时候,需要加上 pull_policy = ["if-not-present"],这样会在执行 job 的时候快很多,不然每次都会去拉取镜像。

1
2
3
4
[[runners]]
name = "docker-runner"
[runners.docker]
pull_policy = ["if-not-present"]

总结

最后,再回顾一下迁移过程的一些关键操作:

  1. 用户迁移:通过 API 从旧的 gitlab 获取用户信息、ssh key,然后在新的 gitlab 中通过 API 创建用户、创建 ssh key。
  2. 用户密码可以进入容器中使用 gitlab-rails dbconsole 来获取用户密码,然后在新的 gitlab 中更新用户密码。
  3. Group 迁移:通过 API 导出 Group,然后下载导出的文件,最后导入到新的 gitlab 中。
  4. 项目迁移:通过 gitlab 的项目导出、导入功能来迁移项目。这种迁移方式会保留项目的 issues、分支 等信息。