内网 GitLab 上拉私有 Go 包,折腾了一圈


公司内网 GitLab,没 HTTPS,Go 项目里引了几个私有包,CI 上怎么拉都拉不下来。折腾了一下午,把踩过的坑记一下,省得下次再翻一遍。

先说结论

内网无 HTTPS 的私有 GitLab,要让 go mod download 正常跑,得凑齐三样东西:

  1. 一组 GO* 环境变量,告诉 Go 哪些是私有的、允许走 HTTP
  2. 一个把 CI_JOB_TOKEN 塞进 git URL 的办法(insteadOf 或 .netrc)
  3. 对目标项目开 CI_JOB_TOKEN 的访问权限

少任何一个都会卡在某个奇怪的错误上,下面一个个说。

四个 GO 变量,各管一摊

变量作用
GOPRIVATE标记私有,不走公共 proxy、不查 sumdb
GONOPROXY直连,不走 proxy
GONOSUMDB不向 sum.golang.org 校验
GOINSECURE允许 HTTP 明文拉取

刚开 始我以为设个 GOPRIVATE 就完事了,结果 Go 还是去敲公共 proxy 的门,私有包直接 404。后来看文档才搞明白,GOPRIVATE 是个"总开关",GONOPROXYGONOSUMDB 是细分的。三个一起设最稳妥,反正内网包三个都不该走外网。

GOINSECURE 是我后来才加的。内网 GitLab 没 HTTPS,Go 默认拒绝明文 HTTP 拉 模块,报一个 TLS 相关的错。这个变量就是干这个的,告诉 Go "这个 host 我信得过,HTTP 也行"。

.gitlab-ci.yml 里这样配:

variables:
  GOPRIVATE: "gitlab.example.com"
  GONOSUMDB: "gitlab.example.com"
  GONOPROXY: "gitlab.example.com"
  GOINSECURE: "gitlab.example.com"

域名换成自己的。

有 token 还是不行,为什么

我一开始有个错觉:GitLab CI 里 submodule 是自动带 CI_JOB_TOKEN 的,那 Go 拉模块应该也自动带吧。

不是这么回事。

submodule 的自动带 token 是 Runner 的 checkout 机制干的活,只针对 runner 自己执行的 git submodule 那一步。Go 拉模块走的是自己那套 VCS 协议,会另起一个 git 进程去 clone,这个进程:

  • 不读 runner 注入给 submodule 的凭证
  • 不继承 .git/config 里 submodule 的 url

所以 CI_JOB_TOKEN 在 Go 发起的 git 调用里是裸的,私有库一看没认证,直接 401。

解决办法是把 token 手动塞进去。两种写法,效果一样,挑顺手的用。

写法一:git insteadOf

before_script:
  - git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"

${CI_SERVER_HOST} 是 GitLab 自带的变量,就是当前 GitLab 的域名,不用硬编码。

写法二:.netrc

before_script:
  - echo "machine ${CI_SERVER_HOST} login gitlab-ci-token password ${CI_JOB_TOKEN}" > ~/.netrc

我最后用的是 insteadOf,因为 git 那套我已经熟了,netrc 调试起来不直观。看个人习惯。

写法三:单独的 gitconfig 文件(builder 里的做法)

前面两种是 CI 里写 before_script 的路子,够用但有个坑。工程化的设计是放入builder以便于复用,发现 git config --global 在某些 runner 上不生效——原因是 runner 服务跑的账号和 go 实际执行 git 的账号不是同一个,--global 写进了前者 home 下的 .gitconfig,go 那个进程根本读不到。

解决办法是把配置写到一个文件里,再用 GIT_CONFIG_GLOBAL 环境变量指过去。环境变量会被 go 起的 git 子进程继承,跟哪个账号没关系。核心逻辑剥出来是这样:

class CommonBase(object):

    def __init__(self, pkg_obj):
        if isinstance(pkg_obj, str):
            self._path = pkg_obj
        else:
            self._path = pkg_obj.get_dir()
    
    @property
    def path(self):
        return self._path

    @staticmethod
    def exec_(commands, cwd=None, timeout=None):
        commands = commands or []
        environ = os.environ.copy()
        p_open = subprocess.Popen(commands, cwd=cwd, env=environ)
        try:
            p_open.wait(timeout)
        except subprocess.TimeoutExpired:
            kill(p_open.pid)
        return p_open.returncode


def _ensure_ci_git_auth(self):
    ci_job_token = os.environ.get('CI_JOB_TOKEN')
    ci_server_url = os.environ.get('CI_SERVER_URL')
    if not ci_job_token or not ci_server_url:
        return None

    host = urlparse(ci_server_url).netloc
    if not host:
        return None

    # 配置文件放 CI_PROJECT_DIR 下,跟着工作区一起被清,不污染项目根
    gitconfig_dir = os.path.join(
        os.environ.get('CI_PROJECT_DIR') or self.path, '.tmp')
    os.makedirs(gitconfig_dir, exist_ok=True)
    gitconfig_path = os.path.join(gitconfig_dir, 'ci.gitconfig')
    if os.path.exists(gitconfig_path):
        os.remove(gitconfig_path)  # 幂等,别让 --add 堆出重复条目

    authed = f"http://gitlab-ci-token:{ci_job_token}@{host}/"
    # insteadOf 是多值的,用 --add 两条都加;裸 git config 第二条会覆盖第一条
    CommonBase.exec_(['git', 'config', '--file', gitconfig_path, '--add',
                      f'url.{authed}.insteadOf', f'http://{host}/'])
    CommonBase.exec_(['git', 'config', '--file', gitconfig_path, '--add',
                      f'url.{authed}.insteadOf', f'https://{host}/'])
    return gitconfig_path

然后在跑 go 命令的上下文里把它挂上去:

@contextmanager
def cmd_ctx(self):
    extra = dict(PATH=new_path, GOENV=self.go_env_path, GOBIN=...)
    gitconfig = self._ensure_ci_git_auth()
    if gitconfig:
        extra['GIT_CONFIG_GLOBAL'] = gitconfig
    with util.Sysenv(**extra):
        yield

这套写法有几个细节值得拎出来说。

别用 --global runner 账号和 go 账号不一致时,--global 写的配置 go 读不到。写文件 + GIT_CONFIG_GLOBAL 才稳,不管谁起的进程都走同一个配置。

http 和 https 两条都要加。 insteadOf 是多值键,用 git config --add 各加一条。如果用裸 git config,第二次调用会把第一次的覆盖掉,结果只有一个 scheme 带 token,另一个 scheme 撞上去就是 401。内网没 https 看着好像只要 http 那条,但有些间接依赖的模块 url 是 https 的,漏一个就断。

幂等。 同一个工作区跑两次(CI 一般会清,但本地 builder 可能复用),--add 会堆出重复条目。跑之前先把旧文件删了。

配置文件放哪。CI_PROJECT_DIR 下的子目录(这里叫 .tmp/),跟着工作区一起被清,不污染项目根,也不用单独写清理逻辑。

跨项目要开权限。 CI_JOB_TOKEN 默认只能访问当前 job 所在的项目。要拉别的私有包(比如 wails-demo 拉 webview2boot),得去目标项目 Settings → CI/CD → Job token permissions 里把当前项目加进允许列表。代码只能把 token 塞进 git,给不给进是 GitLab 那边的事。

本地也想拉怎么办

CI 能跑了,本地开发也得拉。配 go env

go env -w GOPRIVATE="gitlab.example.com"
go env -w GOINSECURE="gitlab.example.com"
go env -w GONOPROXY="gitlab.example.com"
go env -w GONOSUMDB="gitlab.example.com"

凭证那边,本地没法用 CI_JOB_TOKEN,得自己搞个 Personal Access Token 或者 deploy key。我图省事用了 PAT,配 .netrc

machine gitlab.example.com
  login <你的用户名>
  password <PAT>
其实实测下来本地根本不受影响,因为本地自身就存在 git 配置,go get的时候会调用 git 操作,因此 builder 里面可以处理下判断逻辑(检验环境变量存在 CI_JOB_TOKENCI_PROJECT_DIR )跳过本地环境下的注入。

每个项目都改 CI 烦不烦

前面那套 before_script 每个仓库都得抄一遍,项目一多就腻歪。两个思路。

思路一:共享 CI 模板

建一个专门放 CI 片段的仓库,比如 devops/ci-templates,里面放个 go-private.yml

.go-private:
  before_script:
    - git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"

各个项目的 .gitlab-ci.yml 只写两行:

include:
  - project: 'devops/ci-templates'
    file: '/go-private.yml'

test:
  extends: .go-private
  script:
    - go test ./...

GO* 那几个变量可以放到 Group 级的 CI/CD Variables 里,子项目自动继承,连写都不用写。

思路二:自定义镜像

造一个 builder 镜像,把 go env 和 entrypoint 都预置好:

FROM golang:1.25
RUN go env -w GOPRIVATE="gitlab.example.com" \
    && go env -w GOINSECURE="gitlab.example.com" \
    && go env -w GONOPROXY="gitlab.example.com" \
    && go env -w GONOSUMDB="gitlab.example.com"
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh

#!/bin/sh
git config --global url."http://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/".insteadOf "http://${CI_SERVER_HOST}/"
exec "$@"

各项目 CI 里写一行 image: your-registry/go-builder:latest,别的都不用管。我最后走的这条,加新项目时最省心。

几个容易卡的地方

  • CI_JOB_TOKEN 默认只能访问自己所在项目。要拉别的私有包,得去目标项目 Settings → CI/CD → Token access 里,把当前项目加进允许列表
  • 域名一定要用 CI_SERVER_HOST 这种内置变量,别写死。我一开始写死了,换了个环境全报错
  • GOINSECURE 只管 Go 拉模块这层。如果代码里还直接 git clone,git 那边的 HTTP 该配 sslVerify 还得配

小结

这事不复杂,就是变量多、各管一摊,少配一个就报一个不一样的错。把四个 GO* 变量配齐、token 注入搞对、目标项目开权限,三件套齐了就通。

token 注入这块有三个写法,按场景挑。CI 脚本里手写 before_script,用 insteadOf 或 .netrc 都行,简单直接。塞进 builder 代码里,用独立 gitconfig 文件 + GIT_CONFIG_GLOBAL,能绕开 runner 账号和 go 账号不一致的坑,跨平台最稳。剩下的就是用共享 CI 模板或自定义镜像把这套配置收敛到一处,别让每个项目都抄一遍。

声明:Grows towards sunlight |版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 内网 GitLab 上拉私有 Go 包,折腾了一圈


Grows towards sunlight and Carpe Diem