From 2963716953f6d28825a9e8833d7312d4d1469b40 Mon Sep 17 00:00:00 2001 From: StarAurryon <206206+staraurryon@noreply.gitea.com> Date: Mon, 15 Jun 2026 05:05:20 +0000 Subject: [PATCH] feat: ipv6 options for network container creation (#1029) Here is a final proposal for ipv6 enablement on temporary network created by gitea runner --------- Co-authored-by: Nicolas Co-authored-by: Nicolas Schwartz <9308314+StarAurryon@users.noreply.github.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1029 Reviewed-by: Nicolas Co-authored-by: StarAurryon <206206+staraurryon@noreply.gitea.com> Co-committed-by: StarAurryon <206206+staraurryon@noreply.gitea.com> --- act/container/container_types.go | 6 ++ act/container/docker_network.go | 8 ++- act/container/docker_stub.go | 2 +- act/runner/run_context.go | 3 +- act/runner/runner.go | 84 +++++++++++++------------ internal/app/run/runner.go | 39 +++++++----- internal/pkg/config/config.example.yaml | 7 +++ internal/pkg/config/config.go | 30 +++++---- internal/pkg/config/config_test.go | 47 ++++++++++++++ 9 files changed, 151 insertions(+), 75 deletions(-) diff --git a/act/container/container_types.go b/act/container/container_types.go index 9277b6e0..363ebe85 100644 --- a/act/container/container_types.go +++ b/act/container/container_types.go @@ -84,6 +84,12 @@ type NewDockerBuildExecutorInput struct { Platform string } +// NewDockerNetworkCreateExecutorInput the input for the NewDockerNetworkCreateExecutor function +type NewDockerNetworkCreateExecutorInput struct { + EnableIPv4 *bool + EnableIPv6 *bool +} + // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function type NewDockerPullExecutorInput struct { Image string diff --git a/act/container/docker_network.go b/act/container/docker_network.go index d0fd4a5a..dd9b2960 100644 --- a/act/container/docker_network.go +++ b/act/container/docker_network.go @@ -14,7 +14,7 @@ import ( "github.com/moby/moby/client" ) -func NewDockerNetworkCreateExecutor(name string) common.Executor { +func NewDockerNetworkCreateExecutor(name string, opts NewDockerNetworkCreateExecutorInput) common.Executor { return func(ctx context.Context) error { cli, err := GetDockerClient(ctx) if err != nil { @@ -37,8 +37,10 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor { } _, err = cli.NetworkCreate(ctx, name, client.NetworkCreateOptions{ - Driver: "bridge", - Scope: "local", + Driver: "bridge", + Scope: "local", + EnableIPv4: opts.EnableIPv4, + EnableIPv6: opts.EnableIPv6, }) if err != nil { return err diff --git a/act/container/docker_stub.go b/act/container/docker_stub.go index 004ca17e..b146548c 100644 --- a/act/container/docker_stub.go +++ b/act/container/docker_stub.go @@ -61,7 +61,7 @@ func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor { } } -func NewDockerNetworkCreateExecutor(name string) common.Executor { +func NewDockerNetworkCreateExecutor(name string, opts NewDockerNetworkCreateExecutorInput) common.Executor { return func(ctx context.Context) error { return nil } diff --git a/act/runner/run_context.go b/act/runner/run_context.go index de85ebce..b5b78385 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -471,7 +471,8 @@ func (rc *RunContext) startJobContainer() common.Executor { rc.pullServicesImages(rc.Config.ForcePull), rc.JobContainer.Pull(rc.Config.ForcePull), rc.stopJobContainer(), - container.NewDockerNetworkCreateExecutor(networkName).IfBool(createAndDeleteNetwork), + container.NewDockerNetworkCreateExecutor(networkName, rc.Config.ContainerNetworkCreateOptions). + IfBool(createAndDeleteNetwork), rc.startServiceContainers(networkName), rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), rc.JobContainer.Start(false), diff --git a/act/runner/runner.go b/act/runner/runner.go index 9f14d601..4deb683e 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -15,6 +15,7 @@ import ( "time" "gitea.com/gitea/runner/act/common" + "gitea.com/gitea/runner/act/container" "gitea.com/gitea/runner/act/model" docker_container "github.com/moby/moby/api/types/container" @@ -28,47 +29,48 @@ type Runner interface { // Config contains the config for a new runner type Config struct { - Actor string // the user that triggered the event - Workdir string // path to working directory - ActionCacheDir string // path used for caching action contents - ActionOfflineMode bool // when offline, use cached action contents - BindWorkdir bool // bind the workdir to the job container - EventName string // name of event to run - EventPath string // path to JSON file to use for event.json in containers - DefaultBranch string // name of the main branch for this repository - ReuseContainers bool // reuse containers to maintain state - ForcePull bool // force pulling of the image, even if already present - ForceRebuild bool // force rebuilding local docker image action - LogOutput bool // log the output from docker run - JSONLogger bool // use json or text logger - LogPrefixJobID bool // switches from the full job name to the job id - Env map[string]string // env for containers - Inputs map[string]string // manually passed action inputs - Secrets map[string]string // list of secrets - Vars map[string]string // list of vars - Token string // GitHub token - InsecureSecrets bool // switch hiding output when printing to terminal - Platforms map[string]string // list of platforms - Privileged bool // use privileged mode - UsernsMode string // user namespace to use - ContainerArchitecture string // Desired OS/architecture platform for running containers - ContainerDaemonSocket string // Path to Docker daemon socket - ContainerOptions string // Options for the job container - UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true - GitHubInstance string // GitHub instance to use, default "github.com" - ContainerCapAdd []string // list of kernel capabilities to add to the containers - ContainerCapDrop []string // list of kernel capabilities to remove from the containers - AutoRemove bool // controls if the container is automatically removed upon workflow completion - ArtifactServerPath string // the path where the artifact server stores uploads - ArtifactServerAddr string // the address the artifact server binds to - ArtifactServerPort string // the port the artifact server binds to - NoSkipCheckout bool // do not skip actions/checkout - RemoteName string // remote name in local git repo config - ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub - ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. - Matrix map[string]map[string]bool // Matrix config to run - ContainerNetworkMode docker_container.NetworkMode // the network mode of job containers (the value of --network) - ActionCache ActionCache // Use a custom ActionCache Implementation + Actor string // the user that triggered the event + Workdir string // path to working directory + ActionCacheDir string // path used for caching action contents + ActionOfflineMode bool // when offline, use cached action contents + BindWorkdir bool // bind the workdir to the job container + EventName string // name of event to run + EventPath string // path to JSON file to use for event.json in containers + DefaultBranch string // name of the main branch for this repository + ReuseContainers bool // reuse containers to maintain state + ForcePull bool // force pulling of the image, even if already present + ForceRebuild bool // force rebuilding local docker image action + LogOutput bool // log the output from docker run + JSONLogger bool // use json or text logger + LogPrefixJobID bool // switches from the full job name to the job id + Env map[string]string // env for containers + Inputs map[string]string // manually passed action inputs + Secrets map[string]string // list of secrets + Vars map[string]string // list of vars + Token string // GitHub token + InsecureSecrets bool // switch hiding output when printing to terminal + Platforms map[string]string // list of platforms + Privileged bool // use privileged mode + UsernsMode string // user namespace to use + ContainerArchitecture string // Desired OS/architecture platform for running containers + ContainerDaemonSocket string // Path to Docker daemon socket + ContainerOptions string // Options for the job container + UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true + GitHubInstance string // GitHub instance to use, default "github.com" + ContainerCapAdd []string // list of kernel capabilities to add to the containers + ContainerCapDrop []string // list of kernel capabilities to remove from the containers + AutoRemove bool // controls if the container is automatically removed upon workflow completion + ArtifactServerPath string // the path where the artifact server stores uploads + ArtifactServerAddr string // the address the artifact server binds to + ArtifactServerPort string // the port the artifact server binds to + NoSkipCheckout bool // do not skip actions/checkout + RemoteName string // remote name in local git repo config + ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub + ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. + Matrix map[string]map[string]bool // Matrix config to run + 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 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 diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 907ebf8d..cb48cea9 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -22,6 +22,7 @@ import ( "gitea.com/gitea/runner/act/artifactcache" "gitea.com/gitea/runner/act/common" + "gitea.com/gitea/runner/act/container" "gitea.com/gitea/runner/act/model" "gitea.com/gitea/runner/act/runner" "gitea.com/gitea/runner/internal/pkg/client" @@ -33,7 +34,7 @@ import ( "connectrpc.com/connect" runnerv1 "gitea.dev/actions-proto-go/runner/v1" - "github.com/moby/moby/api/types/container" + docker_container "github.com/moby/moby/api/types/container" log "github.com/sirupsen/logrus" ) @@ -418,22 +419,26 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. AllocatePTY: r.cfg.Runner.AllocatePTY, ActionOfflineMode: r.cfg.Cache.OfflineMode, - ReuseContainers: false, - ForcePull: r.cfg.Container.ForcePull, - ForceRebuild: r.cfg.Container.ForceRebuild, - LogOutput: true, - JSONLogger: false, - Env: envs, - Secrets: task.Secrets, - GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"), - AutoRemove: true, - NoSkipCheckout: true, - PresetGitHubContext: preset, - EventJSON: string(eventJSON), - ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id), - ContainerMaxLifetime: maxLifetime, - CleanWorkdir: true, - ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network), + ReuseContainers: false, + ForcePull: r.cfg.Container.ForcePull, + ForceRebuild: r.cfg.Container.ForceRebuild, + LogOutput: true, + JSONLogger: false, + Env: envs, + Secrets: task.Secrets, + GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"), + AutoRemove: true, + NoSkipCheckout: true, + PresetGitHubContext: preset, + EventJSON: string(eventJSON), + ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id), + ContainerMaxLifetime: maxLifetime, + CleanWorkdir: true, + ContainerNetworkMode: docker_container.NetworkMode(r.cfg.Container.Network), + ContainerNetworkCreateOptions: container.NewDockerNetworkCreateExecutorInput{ + EnableIPv4: r.cfg.Container.NetworkCreateOptions.EnableIPv4, + EnableIPv6: r.cfg.Container.NetworkCreateOptions.EnableIPv6, + }, ContainerOptions: r.cfg.Container.Options, ContainerDaemonSocket: r.cfg.Container.DockerHost, Privileged: r.cfg.Container.Privileged, diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 0459dd35..9b12b913 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -116,6 +116,13 @@ container: # If it's empty, runner will create a network automatically. # Deprecated: `network_mode` is still accepted for old configs; use `network` instead. network: "" + # network_create_options only apply when `network` is left empty and the runner + # auto-creates a per-job network that does not already exist. They have no effect + # when a custom `network` name is set, because that network is used as-is and never + # created by the runner. Omit the entire block to use Docker's defaults. + network_create_options: + enable_ipv4: true # Omit to use Docker's default (IPv4 enabled). Set false to disable IPv4. + enable_ipv6: false # Omit to use Docker's default (IPv6 disabled). Enabling it requires dockerd started with --ipv6. # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). privileged: false # Any other options to be used when the container is started (e.g., --add-host=my.gitea.url:host-gateway). diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 23dd5e49..ebd2e952 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -58,18 +58,24 @@ type Cache struct { // Container represents the configuration for the container. type Container struct { - Network string `yaml:"network"` // Network specifies the network for the container. - NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20 - Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode. - Options string `yaml:"options"` // Options specifies additional options for the container. - WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory. - ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers. - DockerHost string `yaml:"docker_host"` // DockerHost specifies the Docker host. It overrides the value specified in environment variable DOCKER_HOST. - ForcePull bool `yaml:"force_pull"` // Pull docker image(s) even if already present - ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present - RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by runner - DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or runner - BindWorkdir bool `yaml:"bind_workdir"` // BindWorkdir binds the workspace to the host filesystem instead of using Docker volumes. Required for DinD when jobs use docker compose with bind mounts. + Network string `yaml:"network"` // Network specifies the network for the container. + NetworkCreateOptions ContainerNetworkCreateOptions `yaml:"network_create_options"` // Add options when the network need to be created by the runner + NetworkMode string `yaml:"network_mode"` // Deprecated: use Network instead. Could be removed after Gitea 1.20 + Privileged bool `yaml:"privileged"` // Privileged indicates whether the container runs in privileged mode. + Options string `yaml:"options"` // Options specifies additional options for the container. + WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the container's working directory. + ValidVolumes []string `yaml:"valid_volumes"` // ValidVolumes specifies the volumes (including bind mounts) can be mounted to containers. + DockerHost string `yaml:"docker_host"` // DockerHost specifies the Docker host. It overrides the value specified in environment variable DOCKER_HOST. + ForcePull bool `yaml:"force_pull"` // Pull docker image(s) even if already present + ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present + RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by runner + DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or runner + BindWorkdir bool `yaml:"bind_workdir"` // BindWorkdir binds the workspace to the host filesystem instead of using Docker volumes. Required for DinD when jobs use docker compose with bind mounts. +} + +type ContainerNetworkCreateOptions struct { + EnableIPv4 *bool `yaml:"enable_ipv4"` // Enable or disable IPv4 for the network (true for docker by default) + EnableIPv6 *bool `yaml:"enable_ipv6"` // Enable or disable IPv6 for the network (false for docker by default) } // Host represents the configuration for the host. diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index 4986ee94..1005a423 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -117,3 +117,50 @@ func TestLoadDefault_MalformedYAMLReturnsParseError(t *testing.T) { assert.Contains(t, err.Error(), "parse config file") assert.NotContains(t, err.Error(), "defaults metadata") } + +func TestContainerNetworkCreateOptions(t *testing.T) { + // Verify that the enable_ipv4/enable_ipv6 YAML keys unmarshal into the *bool fields, + // distinguishing an explicit true/false from an omitted key (nil). A nil here is + // forwarded as-is to Docker, which applies its own default. + loadOptions := func(t *testing.T, yaml string) ContainerNetworkCreateOptions { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(yaml), 0o600)) + + cfg, err := LoadDefault(path) + require.NoError(t, err) + return cfg.Container.NetworkCreateOptions + } + + t.Run("enable_ipv6 true unmarshals to non-nil true", func(t *testing.T) { + opts := loadOptions(t, "container:\n network_create_options:\n enable_ipv6: true\n") + require.NotNil(t, opts.EnableIPv6) + assert.True(t, *opts.EnableIPv6) + }) + + t.Run("enable_ipv6 false unmarshals to non-nil false", func(t *testing.T) { + opts := loadOptions(t, "container:\n network_create_options:\n enable_ipv6: false\n") + require.NotNil(t, opts.EnableIPv6) + assert.False(t, *opts.EnableIPv6) + }) + + t.Run("enable_ipv4 false unmarshals to non-nil false", func(t *testing.T) { + opts := loadOptions(t, "container:\n network_create_options:\n enable_ipv4: false\n") + require.NotNil(t, opts.EnableIPv4) + assert.False(t, *opts.EnableIPv4) + }) + + t.Run("omitted keys stay nil", func(t *testing.T) { + opts := loadOptions(t, "container:\n network_create_options:\n enable_ipv4: true\n") + require.NotNil(t, opts.EnableIPv4) + assert.True(t, *opts.EnableIPv4) + assert.Nil(t, opts.EnableIPv6, "an omitted enable_ipv6 must remain nil so Docker's default applies") + }) + + t.Run("omitted block leaves both nil", func(t *testing.T) { + opts := loadOptions(t, "container:\n network: \"\"\n") + assert.Nil(t, opts.EnableIPv4) + assert.Nil(t, opts.EnableIPv6) + }) +}