mirror of
https://gitea.com/gitea/runner.git
synced 2026-08-25 21:37:45 +00:00
745a1e70e6
The runner's own `container.options` and the workflow's were joined into one string before parsing, so the host-escape filter added in https://gitea.com/gitea/runner/pulls/1058 dropped the administrator's options along with the workflow's. Setups that need `--device` or `--security-opt` from the config file had no way left to get them short of enabling privileged mode. `NewContainerInput` now carries the two sources apart, as `RunnerOptions` and `WorkflowOptions`, down to the point where the filter runs. With privileged mode off, the host-escape fields are reset to what the runner's own options parse to on their own, so only the workflow's contribution is dropped. Three further ways a workflow's options reached past its container, all resolved on the runner before anything reaches the daemon: 1. `--env-file` and `--label-file` name files that are read on the runner, so any file it could read became container environment or labels. Both are refused from a workflow now, and still serve the runner's own options. 2. A bare `--env NAME` was resolved from the runner's own environment by docker's validator. That lookup is gone, for every source. Use `runner.envs` or `runner.env_file` to pass a variable on. 3. A volume driver decides for itself what it mounts, and the local driver's `device=` option turns a name `valid_volumes` allows into a bind of any host path. A workflow's mounts may no longer carry one. `--isolation`, `--volume-driver` and the two paths `--security-opt systempaths=unconfined` lands in were also missing from the fields a workflow may not set. Last, the `--network and --net in the options will be ignored.` warning fired for every container, because the runner's own network mode is fed into the parsed options before the check runs. Fixes https://gitea.com/gitea/runner/issues/1142 Reviewed-on: https://gitea.com/gitea/runner/pulls/1151 Reviewed-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
|
|
|
package container
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/opts"
|
|
"github.com/kballard/go-shellquote"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
const (
|
|
pullPolicyAlways = "always"
|
|
pullPolicyMissing = "missing"
|
|
pullPolicyNever = "never"
|
|
)
|
|
|
|
var pullPolicies = []string{pullPolicyAlways, pullPolicyMissing, pullPolicyNever}
|
|
|
|
// createFlags are the flags docker/cli registers on the `create` and `run` commands
|
|
// instead of in addFlags, so they are not part of containerOptions.
|
|
type createFlags struct {
|
|
platform string
|
|
pull string
|
|
name string
|
|
useAPISocket bool
|
|
}
|
|
|
|
func registerCreateFlags(flags *pflag.FlagSet) *createFlags {
|
|
cf := new(createFlags)
|
|
flags.StringVar(&cf.platform, "platform", "", "Set platform if server is multi-platform capable")
|
|
flags.StringVar(&cf.pull, "pull", pullPolicyMissing, `Pull image before creating ("always", "missing", "never")`)
|
|
flags.StringVar(&cf.name, "name", "", "Assign a name to the container")
|
|
flags.BoolVar(&cf.useAPISocket, "use-api-socket", false, "Bind mount Docker API socket and required auth")
|
|
// Accepted without effect: pull progress is only logged at debug level, and docker
|
|
// no longer implements content trust.
|
|
flags.BoolP("quiet", "q", false, "Suppress the pull output")
|
|
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
|
|
return cf
|
|
}
|
|
|
|
// parseContainerOptions parses a container options string. The flags are returned even
|
|
// on error, holding whatever was read before the failure.
|
|
func parseContainerOptions(options string) (*pflag.FlagSet, *containerOptions, *createFlags, error) {
|
|
flags := pflag.NewFlagSet("container_flags", pflag.ContinueOnError)
|
|
flags.SetOutput(io.Discard)
|
|
copts := addFlags(flags)
|
|
copts.env = opts.NewListOpts(validateEnv) // addFlags registered this field's address, so the swap takes effect
|
|
cf := registerCreateFlags(flags)
|
|
|
|
args, err := shellquote.Split(options)
|
|
if err != nil {
|
|
return flags, copts, cf, fmt.Errorf("cannot split container options: '%s': '%w'", options, err)
|
|
}
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
return flags, copts, cf, fmt.Errorf("cannot parse container options: '%s': '%w'", options, err)
|
|
}
|
|
|
|
return flags, copts, cf, nil
|
|
}
|
|
|
|
// createFlagsFromOptions reads the create-level flags that have to be known before the
|
|
// container is created. Malformed options keep the defaults here and are reported by
|
|
// mergeContainerConfigs at create time.
|
|
func createFlagsFromOptions(options string) *createFlags {
|
|
_, _, cf, _ := parseContainerOptions(options)
|
|
return cf
|
|
}
|
|
|
|
// validateEnv is opts.ValidateEnv without its lookup of a bare name in the runner's environment.
|
|
func validateEnv(val string) (string, error) {
|
|
if name, _, _ := strings.Cut(val, "="); name == "" {
|
|
return "", errors.New("invalid environment variable: " + val)
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// rejectHostReadingOptions refuses the flags naming files that are read here, on the
|
|
// runner, rather than in the container.
|
|
func rejectHostReadingOptions(options string) error {
|
|
flags, _, _, err := parseContainerOptions(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, name := range []string{"env-file", "label-file"} {
|
|
if flags.Changed(name) {
|
|
return fmt.Errorf("container option --%s reads files from the runner and is not allowed in a workflow", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cf *createFlags) validate() error {
|
|
if !slices.Contains(pullPolicies, cf.pull) {
|
|
return fmt.Errorf("invalid --pull option %q: must be one of %q", cf.pull, pullPolicies)
|
|
}
|
|
|
|
if cf.useAPISocket {
|
|
return errors.New("--use-api-socket is not supported, use the runner's container.docker_host setting to expose a docker socket")
|
|
}
|
|
|
|
return nil
|
|
}
|