feat: propagate proxy variables to jobs, services and builds (#1112)

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>
This commit is contained in:
bircni
2026-07-30 08:15:48 +00:00
committed by silverwind
parent 3f7fd16ea1
commit 41c72216bf
15 changed files with 532 additions and 4 deletions

View File

@@ -82,6 +82,7 @@ type NewDockerBuildExecutorInput struct {
BuildContext io.Reader
ImageTag string
Platform string
BuildArgs map[string]*string
}
// NewDockerNetworkCreateExecutorInput the input for the NewDockerNetworkCreateExecutor function

View File

@@ -49,6 +49,7 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
Remove: true,
AuthConfigs: LoadDockerAuthConfigs(ctx),
Dockerfile: input.Dockerfile,
BuildArgs: input.BuildArgs,
}
platform, err := parsePlatform(input.Platform)
if err != nil {

View File

@@ -372,6 +372,7 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, b
ImageTag: image,
BuildContext: buildContext,
Platform: rc.Config.ContainerArchitecture,
BuildArgs: rc.proxyBuildArgs(),
})
if buildContext == nil {
// Held across the whole build: the daemon drains contextDir lazily.

View File

@@ -435,7 +435,9 @@ func (rc *RunContext) startJobContainer() common.Executor {
continue
}
// interpolate env
interpolatedEnvs := make(map[string]string, len(spec.Env))
interpolatedEnvs := make(map[string]string, len(spec.Env)+len(rc.Config.ProxyEnv))
// a service reaches the internet the way the job does; its own env still wins
maps0.Copy(interpolatedEnvs, rc.Config.ProxyEnv)
for k, v := range spec.Env {
interpolatedEnvs[k] = rc.ExprEval.Interpolate(ctx, v)
}
@@ -969,6 +971,20 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
return true, nil
}
// proxyBuildArgs returns the job's proxy variables as docker build args. The docker CLI
// pre-populates these from its own client configuration, but act builds through the API,
// so without them a Dockerfile action's RUN steps have no network behind a proxy.
func (rc *RunContext) proxyBuildArgs() map[string]*string {
if len(rc.Config.ProxyEnv) == 0 {
return nil
}
args := make(map[string]*string, len(rc.Config.ProxyEnv))
for name, value := range rc.Config.ProxyEnv {
args[name] = &value
}
return args
}
func mergeMaps(maps ...map[string]string) map[string]string {
rtnMap := make(map[string]string)
for _, m := range maps {

View File

@@ -292,6 +292,83 @@ jobs:
require.Equal(t, [2]string{"", ""}, credentials["redis:latest"])
}
// A service container reaches the internet the same way the job does, so it inherits the
// job's proxy; a service that sets the variable itself keeps its own value.
func TestStartJobContainerGivesServicesTheJobProxy(t *testing.T) {
workflow, err := model.ReadWorkflow(strings.NewReader(`
name: test
on: push
jobs:
job:
runs-on: ubuntu-latest
container:
image: registry.example/job:latest
services:
redis:
image: redis:latest
db:
image: postgres:latest
env:
no_proxy: db-only.example
steps: []
`))
require.NoError(t, err)
var inputs []*container.NewContainerInput
origNewContainer := newContainer
newContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
inputs = append(inputs, input)
return fakeContainer{}
}
t.Cleanup(func() { newContainer = origNewContainer })
rc := &RunContext{
Name: "test",
Config: &Config{
Workdir: "/tmp",
ContainerNetworkMode: "host",
ReuseContainers: true,
Env: map[string]string{},
ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128", "no_proxy": "internal.example"},
Secrets: map[string]string{},
},
Env: map[string]string{},
Run: &model.Run{
JobID: "job",
Workflow: workflow,
},
}
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
require.NoError(t, rc.startJobContainer()(t.Context()))
env := map[string][]string{}
for _, in := range inputs {
env[in.Image] = in.Env
}
require.Contains(t, env["redis:latest"], "http_proxy=http://proxy:3128")
require.Contains(t, env["redis:latest"], "no_proxy=internal.example")
// the service's own env wins over what the runner injected, without dropping the rest
require.Contains(t, env["postgres:latest"], "no_proxy=db-only.example")
require.NotContains(t, env["postgres:latest"], "no_proxy=internal.example")
require.Contains(t, env["postgres:latest"], "http_proxy=http://proxy:3128")
}
// act builds Dockerfile actions through the API, which does not pre-populate the proxy
// build args the docker CLI would, so the RUN steps would have no network behind a proxy.
func TestProxyBuildArgs(t *testing.T) {
rc := &RunContext{Config: &Config{ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128"}}}
args := rc.proxyBuildArgs()
require.Len(t, args, 1)
require.Equal(t, "http://proxy:3128", *args["http_proxy"])
// a job without a proxy builds exactly as it does today
require.Nil(t, (&RunContext{Config: &Config{}}).proxyBuildArgs())
}
func TestRunContext_GetBindsAndMounts(t *testing.T) {
rctemplate := &RunContext{
Name: "TestRCName",

View File

@@ -73,6 +73,7 @@ type Config struct {
ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network)
ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options
ActionCache ActionCache // Use a custom ActionCache Implementation
ProxyEnv map[string]string // the proxy variables the job runs with, also given to service containers and image builds
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath