mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-02 21:03:09 +00:00
Set `http_proxy`, `https_proxy` and `no_proxy` in the runner's environment and everything the runner controls uses them. Go already read them for the runner's own requests. This adds jobs, in lower and upper case, service containers, and Dockerfile action builds. Some hosts are added to `no_proxy` for jobs so they stay direct: the cache server, loopback, the job's service containers, and a `tcp://` Docker daemon. Without the last one the Docker client sends its API calls to the proxy and docker-in-docker breaks. Gitea is not added. Images are pulled by the Docker daemon, which has its own proxy setting. In the `dind` images it reads these same variables. The runner warns at startup if it has a proxy and the daemon does not. Fixes https://gitea.com/gitea/runner/issues/1118, originally reported as https://gitea.com/gitea/runner/issues/708. --------- Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: silverwind <me@silverwind.io> Reviewed-on: https://gitea.com/gitea/runner/pulls/1112 Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com> Co-authored-by: bircni <bircni@icloud.com>
164 lines
4.9 KiB
Go
164 lines
4.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package run
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"gitea.com/gitea/runner/act/container"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/net/http/httpproxy"
|
|
)
|
|
|
|
// proxyFromEnv returns the runner's own proxy configuration, or nil when it has none.
|
|
func proxyFromEnv() *httpproxy.Config {
|
|
cfg := httpproxy.FromEnvironment()
|
|
if cfg.HTTPProxy == "" && cfg.HTTPSProxy == "" {
|
|
return nil
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// JobProxyEnv returns the proxy variables a job runs with, given what runner.envs already
|
|
// put in jobEnvs. Gitea is deliberately not made direct, so it stays reachable however the
|
|
// runner reaches it.
|
|
func JobProxyEnv(jobEnvs map[string]string, cacheURL string, serviceNames []string) map[string]string {
|
|
cfg := proxyFromEnv()
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
// Go bypasses loopback on its own, curl and most other tools in a job do not.
|
|
direct := append([]string{"localhost", "127.0.0.1", "::1"}, serviceNames...)
|
|
direct = append(direct, hostOf(cacheURL))
|
|
|
|
proxyEnv := map[string]string{}
|
|
setPair := func(lower, upper, value string) {
|
|
if value == "" {
|
|
return
|
|
}
|
|
// Either spelling in runner.envs takes over both, so the pair cannot disagree.
|
|
if existing, ok := jobEnvs[lower]; ok {
|
|
value = existing
|
|
} else if existing, ok := jobEnvs[upper]; ok {
|
|
value = existing
|
|
}
|
|
proxyEnv[lower], proxyEnv[upper] = value, value
|
|
}
|
|
setPair("http_proxy", "HTTP_PROXY", cfg.HTTPProxy)
|
|
setPair("https_proxy", "HTTPS_PROXY", cfg.HTTPSProxy)
|
|
|
|
// no_proxy is merged rather than replaced: the hosts above are structural, and an
|
|
// operator cannot list the cache server's startup-assigned address in advance.
|
|
noProxy := appendNoProxy(cfg.NoProxy, direct...)
|
|
for _, name := range []string{"no_proxy", "NO_PROXY"} {
|
|
if existing, ok := jobEnvs[name]; ok {
|
|
noProxy = appendNoProxy(existing, strings.Split(noProxy, ",")...)
|
|
break
|
|
}
|
|
}
|
|
proxyEnv["no_proxy"], proxyEnv["NO_PROXY"] = noProxy, noProxy
|
|
return proxyEnv
|
|
}
|
|
|
|
// BypassProxyForDockerHost keeps the runner's Docker API traffic off the proxy: the docker
|
|
// client proxies every transport that is not a unix socket or a named pipe, so a tcp://
|
|
// daemon would be reached through a proxy that cannot route to it.
|
|
//
|
|
// It must run before the first Docker API call, because the standard library resolves the
|
|
// proxy environment once.
|
|
func BypassProxyForDockerHost(dockerHost string) {
|
|
cfg := proxyFromEnv()
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
host := hostOf(dockerHost)
|
|
if host == "" {
|
|
// A unix socket or named pipe is never proxied.
|
|
return
|
|
}
|
|
|
|
noProxy := appendNoProxy(cfg.NoProxy, host)
|
|
for _, name := range []string{"no_proxy", "NO_PROXY"} {
|
|
if err := os.Setenv(name, noProxy); err != nil {
|
|
log.Warnf("cannot set %s for the runner process: %v", name, err)
|
|
}
|
|
}
|
|
log.Debugf("docker host %s is reached directly, no_proxy is now %q", host, noProxy)
|
|
}
|
|
|
|
// WarnIfDaemonHasNoProxy points at the one part the runner cannot set: the docker daemon
|
|
// pulls the images, and a daemon in its own container needs its own proxy.
|
|
func WarnIfDaemonHasNoProxy(ctx context.Context) {
|
|
if proxyFromEnv() == nil {
|
|
return
|
|
}
|
|
info, err := container.GetHostInfo(ctx)
|
|
if err != nil {
|
|
log.Debugf("cannot read the docker daemon's proxy configuration: %v", err)
|
|
return
|
|
}
|
|
if info.HTTPProxy == "" && info.HTTPSProxy == "" {
|
|
log.Warn("the runner has a proxy but the docker daemon reports none, so image pulls will not use it: https://docs.docker.com/engine/daemon/proxy/")
|
|
}
|
|
}
|
|
|
|
// proxyPasswords returns the passwords embedded in the runner's proxy URLs, to mask before
|
|
// a job echoes its environment.
|
|
func proxyPasswords() []string {
|
|
cfg := proxyFromEnv()
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
|
|
var passwords []string
|
|
for _, raw := range []string{cfg.HTTPProxy, cfg.HTTPSProxy} {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || parsed.User == nil {
|
|
continue
|
|
}
|
|
if password, ok := parsed.User.Password(); ok && password != "" && !slices.Contains(passwords, password) {
|
|
passwords = append(passwords, password)
|
|
}
|
|
}
|
|
return passwords
|
|
}
|
|
|
|
// appendNoProxy adds hosts to a no_proxy list, keeping the operator's entries and adding
|
|
// none twice.
|
|
func appendNoProxy(noProxy string, hosts ...string) string {
|
|
entries := []string{}
|
|
for entry := range strings.SplitSeq(noProxy, ",") {
|
|
if entry = strings.TrimSpace(entry); entry != "" {
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
|
|
for _, host := range hosts {
|
|
if host == "" || slices.Contains(entries, host) {
|
|
continue
|
|
}
|
|
entries = append(entries, host)
|
|
}
|
|
return strings.Join(entries, ",")
|
|
}
|
|
|
|
// hostOf returns the host of a URL without its port, the form a no_proxy entry takes. It is
|
|
// empty for anything without a network host, such as a unix socket.
|
|
func hostOf(raw string) string {
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
parsed, err := url.Parse(strings.TrimSuffix(raw, "/"))
|
|
if err != nil || parsed.Hostname() == "" {
|
|
return ""
|
|
}
|
|
return parsed.Hostname()
|
|
}
|