内网 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 是个"总开关",GONOPROXY 和 GONOSUMDB 是细分的。三个一起设最稳妥,反正内网包三个都不该走外网。

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_TOKEN 和 CI_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