大家好,我是煎鱼。
最近帮忙小伙伴处理了一个小问题,感觉五六年前就有人问过我,当年觉得没啥大问题记录。没想到。。。2024 年了,还是有同学表示他的姿势搜不到相关的解决办法。
今天主打一个分享和记载,看看有没有也踩过坑的朋友。(结果我发文前两天就有社区的朋友问到了我😅)
我感觉 2027 年这问题都不会解决。
功能介绍
在 GitLab 中,提供了一种叫做子组(subgroups)的功能特性。
它允许对项目仓库进行进一步的分组,而无需创建多个组织去实现不同内容的存放。
创建子组的截图和地方:
新建子组
在项目列表下子组的展示:
列表展示
注:这个功能,我翻了下 GitHub 是没有的。并且有人在 Community Discussions#4837 提出也想要,但官方没有人回应此项提议。
问题背景
虽然这个功能有一定的特色。但是子组(subgroups)和 Go 模块管理有一定的水土不服。我们直接使用 go get 命令试图拉取子组时,就会出现问题。
在最常用的 group/project 格式下,模拟 go get 命令直接拉取:
curl -X GET "https://gitlab.xxx.com/libraries/example?go-get=1"
<html><head><meta name="go-import" cnotallow="gitlab.xxx.com/libraries/example git https://gitlab.xxx.com/libraries/example.git" /></head></html>
可以看到是正常返回:gitlab.xxx.com/libraries/example.git。
那如果是子组的模式呢?
我们按照 group/subgroup/project 再获取试试:
curl -X GET "https://gitlab.xxx.com/libraries/subgroup1/example?go-get=1"
<html><head><meta name="go-import" cnotallow="gitlab.xxx.com/libraries/subgroup1 git https://gitlab.xxx.com/libraries/subgroup1.git" /></head></html>
可以看到返回的是:gitlab.xxx.com/libraries/subgroup1.git,这显然不正确。我们要获取的是 gitlab.xxx.com/libraries/subgroup1/example。获取到的层级都错了!
不支持的原因
现阶段来看,根本原因是 Go 的 go get 实现和 GitLab 的原因共同导致。互相不完全适配。Go 觉得 GitLab 这个太定制化,GitLab 觉得是 Go 实现不完整。
Go 为了使用 SSH 身份验证,go get 需要知道项目位于什么地方,然后才能去获取他。
以下是梳理的流程和步骤:
1、对于 group/subgroup/project 中的项目,go get 将发送以下请求:
- GET https://gitlab.com/group/subgroup/project?go-get=1
- GET https://gitlab.com/group/subgroup?go-get=1
- GET https://gitlab.com/group?go-get=1
2、这些请求未经授权,因此 GitLab 无法提供指向项目的正确链接 (也就是预期的结果:https://gitlab.com/group/subgroup/project.git)。
3、相反,GitLab 会返回 https://gitlab.com/group/subgroup.git 作为响应结果。(如此返回的目的是:防止未经身份验证的用户访问,避免出现暴露项目存在的安全风险)
4、然后 Go 会尝试使用 SSH 身份验证 ssh://gitlab.com/group/subgroup.git 来 git clone 仓库。想也知道,拉取的结果失败了,因为这个仓库并不存在。
因此对于 GitLab 子组中的 Go 私有项目,用户必须通过 HTTPS 认证,这样 Go 的 go get 命令才能通过 GitLab 的访问校验,再拉取到正确的响应结果。
解决方案
现阶段的解决方案大致有两种,都是一些绕过校验或逻辑的操作。
使用 .netrc 鉴权
GitLab 文档 Authenticate Go requests to private projects[1] 和 Go 文档 Passing credentials to private proxies[2],推荐的是通过定义 .netrc 文件来实现该鉴权访问。
但有前置条件的要求:
- GitLab 实例必须可通过 HTTPS 访问。
- 用到的账号必须拥有 read_api 权限的个人访问令牌。
根据你的 OS 环境不同,创建一个 .netrc 文件,并填入相关信息:
machine gitlab.example.com
login <gitlab_user_name>
password <personal_access_token>
需要注意:在 Windows 上,Go 读取 ~/_netrc 而不是 Linux 的 ~/.netrc 文件。
或者可以直接在 GOPROXY URL 鉴权凭证:
GOPROXY=https://jrgopher:hunter2@proxy.corp.example.com
采用此方法时请务必小心:环境变量可能会出现在 shell 历史记录和日志中。(不推荐使用)
但总的来说,netrc 这个方案,有部分社区反馈不是很认可,因为有安全隐患,存在一定的风险。还要配这配那的。
使用 replace .git 绕过逻辑
有一种方法更简单的方法,可以跳过 go get 请求并强制 Go 直接使用 Git 身份验证,但需要修改模块名称。
直接修改 go.mod 文件,采用 replace 的方式给实际引用的模块名的项目名增加 .git。当然,如果你不想 replace,直接引用模块后缀是 .git 也是可以的。
如下所示:
require(
gitlab.xxx.com/group/subgroup/project v1.7.0
)
replace(
gitlab.xxx.com/group/subgroup/project => gitlab.xxx.com/group/subgroup/project.git v1.7.0
)
Go 模块能用这个骚操作的原因是:如果模块路径的某个组件末尾有一个 VCS 标识符符(例如:.bzr、.fossil、.git、.hg、.svn)。go get 命令就会使用该路径限定符之前的所有内容作为仓库 URL。
例如,对于 example.com/foo.git/bar 模块,go 命令会使用 git 下载 example.com/foo.git 的版本库,并在 bar 子目录中找到该模块。
总结和吐槽
比较可惜的是,这个问题,至少 7 年前(2017 年)就有人提出了,可惜到现在 2024 年,都没有解决。
追根溯源上,Go 团队并不想完整实现一套完整的单独的程序去支持这个。Go 团队负责人 rsc(Go 模块他独立开发、应用和推广的)只想做一个符合标准的,因此 Go 最终支持了读取 $HOME/.netrc。后续至此再无更多的动作。社区反馈也不会说特别激烈。
而对于 GitLab 而已,他是乙方。客户各种吐槽。因此他们最有动力去推进解决这个事情。
近期由于 Go 团队多年对此的冷漠。GitLab 开始也有提出 Allow to set a go-modules folder for private Go projects[3]。
希望允许用户在 group 或者子组为私有 Go 模块设置 go-modules 的专属标识再通过第二点解决方案的 .git 的方式绕过逻辑:
预想的定制
不过这个还在讨论中,反对意见也不小。暂且苟住。
大家在实际使用上,可以先看看 .netrc 或 replace .git 的方式。可能前者更标准,后者更易用。这样团队成员就不用每次都配置一遍了。
参考资料
[1]Authenticate Go requests to private projects: https://docs.gitlab.com/ee/user/project/use_project_as_go_package.html#authenticate-go-requests-to-private-projects
[2]Passing credentials to private proxies: https://go.dev/ref/mod#private-module-proxy-auth
[3]Allow to set a go-modules folder for private Go projects: https://gitlab.com/gitlab-org/gitlab/-/issues/437005