mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-02 04:43:10 +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>
193 lines
6.3 KiB
Go
193 lines
6.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package run
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// clearProxyEnv starts a test from a runner that has no proxy, whatever the developer's own
|
|
// environment looks like.
|
|
func clearProxyEnv(t *testing.T) {
|
|
t.Helper()
|
|
for _, name := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
|
|
t.Setenv(name, "")
|
|
}
|
|
}
|
|
|
|
func TestJobProxyEnv(t *testing.T) {
|
|
const loopback = "localhost,127.0.0.1,::1"
|
|
const proxy = "http://proxy:3128"
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
runner map[string]string // the runner process environment
|
|
envs map[string]string // what runner.envs already put in the job
|
|
cacheURL string
|
|
services []string
|
|
want map[string]string
|
|
}{
|
|
{
|
|
// The guarantee that makes this safe to ship: a runner without a proxy gives its
|
|
// jobs nothing at all.
|
|
name: "runner has no proxy",
|
|
cacheURL: "http://cache.local:8088/",
|
|
},
|
|
{
|
|
name: "a lone no_proxy is not a proxy",
|
|
runner: map[string]string{"no_proxy": "example.com"},
|
|
},
|
|
{
|
|
name: "both spellings of every variable",
|
|
runner: map[string]string{"http_proxy": proxy, "https_proxy": proxy},
|
|
want: map[string]string{
|
|
"http_proxy": proxy, "HTTP_PROXY": proxy,
|
|
"https_proxy": proxy, "HTTPS_PROXY": proxy,
|
|
"no_proxy": loopback, "NO_PROXY": loopback,
|
|
},
|
|
},
|
|
{
|
|
name: "a variable the runner does not have is omitted",
|
|
runner: map[string]string{"https_proxy": proxy},
|
|
want: map[string]string{
|
|
"https_proxy": proxy, "HTTPS_PROXY": proxy,
|
|
"no_proxy": loopback, "NO_PROXY": loopback,
|
|
},
|
|
},
|
|
{
|
|
// The cache server and the service containers are on the local network; Gitea is
|
|
// not added and keeps being reached through the proxy.
|
|
name: "hosts the job must reach directly",
|
|
runner: map[string]string{"http_proxy": proxy, "no_proxy": "internal.example"},
|
|
cacheURL: "http://192.168.1.10:34567/",
|
|
services: []string{"postgres", "redis"},
|
|
want: map[string]string{
|
|
"http_proxy": proxy, "HTTP_PROXY": proxy,
|
|
"no_proxy": "internal.example," + loopback + ",postgres,redis,192.168.1.10",
|
|
"NO_PROXY": "internal.example," + loopback + ",postgres,redis,192.168.1.10",
|
|
},
|
|
},
|
|
{
|
|
name: "runner.envs wins, for both spellings",
|
|
runner: map[string]string{"http_proxy": "http://from-env:3128"},
|
|
envs: map[string]string{"http_proxy": "http://from-config:3128"},
|
|
want: map[string]string{
|
|
"http_proxy": "http://from-config:3128", "HTTP_PROXY": "http://from-config:3128",
|
|
"no_proxy": loopback, "NO_PROXY": loopback,
|
|
},
|
|
},
|
|
{
|
|
name: "runner.envs wins through the upper case spelling too",
|
|
runner: map[string]string{"http_proxy": "http://from-env:3128"},
|
|
envs: map[string]string{"HTTP_PROXY": "http://from-config:3128"},
|
|
want: map[string]string{
|
|
"http_proxy": "http://from-config:3128", "HTTP_PROXY": "http://from-config:3128",
|
|
"no_proxy": loopback, "NO_PROXY": loopback,
|
|
},
|
|
},
|
|
{
|
|
// A runner.envs no_proxy adds to the hosts that must stay direct rather than
|
|
// replacing them, which would send cache traffic through the proxy.
|
|
name: "runner.envs no_proxy is merged, not substituted",
|
|
runner: map[string]string{"http_proxy": proxy},
|
|
envs: map[string]string{"no_proxy": "operator.example"},
|
|
cacheURL: "http://192.168.1.10:34567/",
|
|
want: map[string]string{
|
|
"http_proxy": proxy, "HTTP_PROXY": proxy,
|
|
"no_proxy": "operator.example," + loopback + ",192.168.1.10",
|
|
"NO_PROXY": "operator.example," + loopback + ",192.168.1.10",
|
|
},
|
|
},
|
|
{
|
|
name: "runner.envs NO_PROXY is merged through the upper case spelling too",
|
|
runner: map[string]string{"http_proxy": proxy},
|
|
envs: map[string]string{"NO_PROXY": "operator.example"},
|
|
cacheURL: "http://192.168.1.10:34567/",
|
|
want: map[string]string{
|
|
"http_proxy": proxy, "HTTP_PROXY": proxy,
|
|
"no_proxy": "operator.example," + loopback + ",192.168.1.10",
|
|
"NO_PROXY": "operator.example," + loopback + ",192.168.1.10",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clearProxyEnv(t)
|
|
for name, value := range tc.runner {
|
|
t.Setenv(name, value)
|
|
}
|
|
assert.Equal(t, tc.want, JobProxyEnv(tc.envs, tc.cacheURL, tc.services))
|
|
})
|
|
}
|
|
}
|
|
|
|
// docker-in-docker over tcp: the docker client would otherwise send API calls to a proxy
|
|
// that cannot route to the daemon.
|
|
func TestBypassProxyForDockerHost(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
httpProxy string
|
|
dockerHost string
|
|
want string
|
|
}{
|
|
{
|
|
name: "tcp daemon is added",
|
|
httpProxy: "http://proxy:3128",
|
|
dockerHost: "tcp://docker:2375",
|
|
want: "internal.example,docker",
|
|
},
|
|
{
|
|
name: "unix socket is left alone",
|
|
httpProxy: "http://proxy:3128",
|
|
dockerHost: "unix:///var/run/docker.sock",
|
|
want: "internal.example",
|
|
},
|
|
{
|
|
name: "nothing happens without a proxy",
|
|
dockerHost: "tcp://docker:2375",
|
|
want: "internal.example",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
clearProxyEnv(t)
|
|
t.Setenv("no_proxy", "internal.example")
|
|
if tc.httpProxy != "" {
|
|
t.Setenv("http_proxy", tc.httpProxy)
|
|
}
|
|
|
|
BypassProxyForDockerHost(tc.dockerHost)
|
|
|
|
assert.Equal(t, tc.want, os.Getenv("no_proxy"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyPasswords(t *testing.T) {
|
|
clearProxyEnv(t)
|
|
t.Setenv("http_proxy", "http://user:hunter2@proxy:3128")
|
|
t.Setenv("https_proxy", "http://user:s3cret@proxy:3128")
|
|
assert.Equal(t, []string{"hunter2", "s3cret"}, proxyPasswords())
|
|
|
|
t.Setenv("https_proxy", "http://proxy:3128")
|
|
assert.Equal(t, []string{"hunter2"}, proxyPasswords())
|
|
}
|
|
|
|
func TestAppendNoProxy(t *testing.T) {
|
|
assert.Equal(t, "a.example,b.example", appendNoProxy(" a.example , b.example "))
|
|
// a host already listed is not repeated
|
|
assert.Equal(t, "cache.local", appendNoProxy("cache.local", "cache.local"))
|
|
assert.Empty(t, appendNoProxy("", "", ""))
|
|
}
|
|
|
|
func TestHostOf(t *testing.T) {
|
|
assert.Equal(t, "cache.local", hostOf("http://cache.local:8088/"))
|
|
assert.Equal(t, "192.168.1.10", hostOf("http://192.168.1.10:34567"))
|
|
// nothing to bypass for a socket path or an unparseable URL
|
|
assert.Empty(t, hostOf("unix:///var/run/docker.sock"))
|
|
assert.Empty(t, hostOf("cache.local:8088"))
|
|
assert.Empty(t, hostOf("://nope"))
|
|
}
|