diff --git a/act/container/docker_create_flags.go b/act/container/docker_create_flags.go new file mode 100644 index 00000000..07fcefab --- /dev/null +++ b/act/container/docker_create_flags.go @@ -0,0 +1,86 @@ +// 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" + + "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) + 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 +} + +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 +} diff --git a/act/container/docker_create_flags_test.go b/act/container/docker_create_flags_test.go new file mode 100644 index 00000000..ddbe3814 --- /dev/null +++ b/act/container/docker_create_flags_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package container + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateFlagsFromOptions(t *testing.T) { + for _, tc := range []struct { + options string + platform string + pull string + }{ + {"", "", pullPolicyMissing}, + {"-v /a:/b --platform=linux/arm64 --pull always", "linux/arm64", pullPolicyAlways}, + {"--platform linux/arm/v7 --pull never", "linux/arm/v7", pullPolicyNever}, + {`--platform "linux/amd64`, "", pullPolicyMissing}, // malformed, defaults kept + } { + t.Run(tc.options, func(t *testing.T) { + cf := createFlagsFromOptions(tc.options) + assert.Equal(t, tc.platform, cf.platform) + assert.Equal(t, tc.pull, cf.pull) + }) + } +} + +func TestCreateFlagsValidate(t *testing.T) { + for _, tc := range []struct { + options string + wantErr string + }{ + {"--quiet --disable-content-trust --name mine", ""}, + {"--pull sometimes", `invalid --pull option "sometimes"`}, + {"--use-api-socket", "--use-api-socket is not supported"}, + } { + t.Run(tc.options, func(t *testing.T) { + err := createFlagsFromOptions(tc.options).validate() + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.ErrorContains(t, err, tc.wantErr) + }) + } +} + +func TestNewContainerAppliesCreateFlags(t *testing.T) { + input := &NewContainerInput{Platform: "linux/amd64", Options: "--platform linux/arm64 --pull never"} + cr := NewContainer(input).(*containerReference) + assert.Equal(t, "linux/arm64", input.Platform) + assert.Equal(t, pullPolicyNever, cr.pullPolicy) + + kept := &NewContainerInput{Platform: "linux/amd64", Options: "--privileged"} + NewContainer(kept) + assert.Equal(t, "linux/amd64", kept.Platform) +} diff --git a/act/container/docker_run.go b/act/container/docker_run.go index 8365562b..cd10668c 100644 --- a/act/container/docker_run.go +++ b/act/container/docker_run.go @@ -35,7 +35,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/gobwas/glob" "github.com/joho/godotenv" - "github.com/kballard/go-shellquote" "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" @@ -43,7 +42,6 @@ import ( "github.com/moby/moby/api/types/system" "github.com/moby/moby/client" specs "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/spf13/pflag" ) // drainGracePeriod bounds how long we wait for an output-copy goroutine to @@ -57,6 +55,12 @@ const drainGracePeriod = 2 * time.Second func NewContainer(input *NewContainerInput) ExecutionsEnvironment { cr := new(containerReference) cr.input = input + // Resolved up front because the image pull runs before the container is created. + cf := createFlagsFromOptions(input.Options) + if cf.platform != "" { + cr.input.Platform = cf.platform + } + cr.pullPolicy = cf.pull return cr } @@ -137,6 +141,11 @@ func (cr *containerReference) Start(attach bool) common.Executor { } func (cr *containerReference) Pull(forcePull bool) common.Executor { + if cr.pullPolicy == pullPolicyNever { + return common.NewInfoExecutor("docker pull skipped image=%s, --pull=never in the options", cr.input.Image) + } + forcePull = forcePull || cr.pullPolicy == pullPolicyAlways + return common. NewInfoExecutor("docker pull image=%s platform=%s username=%s forcePull=%t", cr.input.Image, cr.input.Platform, cr.input.Username, forcePull). Then( @@ -232,11 +241,12 @@ func (cr *containerReference) ReplaceLogWriter(stdout, stderr io.Writer) (io.Wri } type containerReference struct { - cli client.APIClient - id string - input *NewContainerInput - UID int - GID int + cli client.APIClient + id string + input *NewContainerInput + pullPolicy string + UID int + GID int // attachDone is closed by the attach() streaming goroutine once it has // drained and flushed the container's output. wait() blocks on it so the // tail of the log lands before the step proceeds. @@ -411,17 +421,13 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config } // parse configuration from CLI container.options - flags := pflag.NewFlagSet("container_flags", pflag.ContinueOnError) - copts := addFlags(flags) - - optionsArgs, err := shellquote.Split(input.Options) + flags, copts, cf, err := parseContainerOptions(input.Options) if err != nil { - return nil, nil, fmt.Errorf("Cannot split container options: '%s': '%w'", input.Options, err) + return nil, nil, err } - err = flags.Parse(optionsArgs) - if err != nil { - return nil, nil, fmt.Errorf("Cannot parse container options: '%s': '%w'", input.Options, err) + if err := cf.validate(); err != nil { + return nil, nil, fmt.Errorf("Cannot process container options: '%s': '%w'", input.Options, err) } // FIXME: If everything is fine after gitea/act v0.260.0, remove the following comment. @@ -476,6 +482,9 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config } hostConfig.Binds = binds hostConfig.Mounts = mounts + if cf.name != "" { + logger.Warn("--name in the options will be ignored.") + } if len(copts.netMode.Value()) > 0 { logger.Warn("--network and --net in the options will be ignored.") }