diff --git a/cmd/compose/run.go b/cmd/compose/run.go index 1a4137936f6..3d0cb6decaf 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -24,6 +24,7 @@ import ( cgo "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/opts" "github.com/mattn/go-shellwords" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -48,6 +49,8 @@ type runOptions struct { workdir string entrypoint string entrypointCmd []string + capAdd opts.ListOpts + capDrop opts.ListOpts labels []string volumes []string publish []string @@ -59,20 +62,20 @@ type runOptions struct { quietPull bool } -func (opts runOptions) apply(project *types.Project) error { - target, err := project.GetService(opts.Service) +func (options runOptions) apply(project *types.Project) error { + target, err := project.GetService(options.Service) if err != nil { return err } - target.Tty = !opts.noTty - target.StdinOpen = opts.interactive - if !opts.servicePorts { + target.Tty = !options.noTty + target.StdinOpen = options.interactive + if !options.servicePorts { target.Ports = []types.ServicePortConfig{} } - if len(opts.publish) > 0 { + if len(options.publish) > 0 { target.Ports = []types.ServicePortConfig{} - for _, p := range opts.publish { + for _, p := range options.publish { config, err := types.ParsePortConfig(p) if err != nil { return err @@ -80,8 +83,8 @@ func (opts runOptions) apply(project *types.Project) error { target.Ports = append(target.Ports, config...) } } - if len(opts.volumes) > 0 { - for _, v := range opts.volumes { + if len(options.volumes) > 0 { + for _, v := range options.volumes { volume, err := loader.ParseVolume(v) if err != nil { return err @@ -90,15 +93,15 @@ func (opts runOptions) apply(project *types.Project) error { } } - if opts.noDeps { - err := project.ForServices([]string{opts.Service}, types.IgnoreDependencies) + if options.noDeps { + err := project.ForServices([]string{options.Service}, types.IgnoreDependencies) if err != nil { return err } } for i, s := range project.Services { - if s.Name == opts.Service { + if s.Name == options.Service { project.Services[i] = target break } @@ -107,10 +110,12 @@ func (opts runOptions) apply(project *types.Project) error { } func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { - opts := runOptions{ + options := runOptions{ composeOptions: &composeOptions{ ProjectOptions: p, }, + capAdd: opts.NewListOpts(nil), + capDrop: opts.NewListOpts(nil), } createOpts := createOptions{} cmd := &cobra.Command{ @@ -118,61 +123,63 @@ func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *co Short: "Run a one-off command on a service.", Args: cobra.MinimumNArgs(1), PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { - opts.Service = args[0] + options.Service = args[0] if len(args) > 1 { - opts.Command = args[1:] + options.Command = args[1:] } - if len(opts.publish) > 0 && opts.servicePorts { + if len(options.publish) > 0 && options.servicePorts { return fmt.Errorf("--service-ports and --publish are incompatible") } if cmd.Flags().Changed("entrypoint") { - command, err := shellwords.Parse(opts.entrypoint) + command, err := shellwords.Parse(options.entrypoint) if err != nil { return err } - opts.entrypointCmd = command + options.entrypointCmd = command } if cmd.Flags().Changed("tty") { if cmd.Flags().Changed("no-TTY") { return fmt.Errorf("--tty and --no-TTY can't be used together") } else { - opts.noTty = !opts.tty + options.noTty = !options.tty } } return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - project, err := p.ToProject([]string{opts.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile) + project, err := p.ToProject([]string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithDiscardEnvFile) if err != nil { return err } - opts.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans]) - return runRun(ctx, backend, project, opts, createOpts, streams) + options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans]) + return runRun(ctx, backend, project, options, createOpts, streams) }), ValidArgsFunction: completeServiceNames(p), } flags := cmd.Flags() - flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID") - flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") - flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label") - flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits") - flags.BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).") - flags.StringVar(&opts.name, "name", "", "Assign a name to the container") - flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid") - flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container") - flags.StringVar(&opts.entrypoint, "entrypoint", "", "Override the entrypoint of the image") - flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.") - flags.StringArrayVarP(&opts.volumes, "volume", "v", []string{}, "Bind mount a volume.") - flags.StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.") - flags.BoolVar(&opts.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.") - flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.") - flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.") + flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID") + flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables") + flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label") + flags.BoolVar(&options.Remove, "rm", false, "Automatically remove the container when it exits") + flags.BoolVarP(&options.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).") + flags.StringVar(&options.name, "name", "", "Assign a name to the container") + flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid") + flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container") + flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image") + flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities") + flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities") + flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.") + flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.") + flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.") + flags.BoolVar(&options.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.") + flags.BoolVar(&options.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.") + flags.BoolVar(&options.quietPull, "quiet-pull", false, "Pull without printing progress information.") flags.BoolVar(&createOpts.Build, "build", false, "Build image before starting container.") flags.BoolVar(&createOpts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") - cmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.") - cmd.Flags().BoolVarP(&opts.tty, "tty", "t", true, "Allocate a pseudo-TTY.") + cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.") + cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY.") cmd.Flags().MarkHidden("tty") //nolint:errcheck flags.SetNormalizeFunc(normalizeRunFlags) @@ -190,8 +197,8 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName { return pflag.NormalizedName(name) } -func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions, createOpts createOptions, streams api.Streams) error { - err := opts.apply(project) +func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, streams api.Streams) error { + err := options.apply(project) if err != nil { return err } @@ -202,14 +209,14 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op } err = progress.Run(ctx, func(ctx context.Context) error { - return startDependencies(ctx, backend, *project, opts.Service, opts.ignoreOrphans) + return startDependencies(ctx, backend, *project, options.Service, options.ignoreOrphans) }, streams.Err()) if err != nil { return err } labels := types.Labels{} - for _, s := range opts.labels { + for _, s := range options.labels { parts := strings.SplitN(s, "=", 2) if len(parts) != 2 { return fmt.Errorf("label must be set as KEY=VALUE") @@ -219,27 +226,29 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op // start container and attach to container streams runOpts := api.RunOptions{ - Name: opts.name, - Service: opts.Service, - Command: opts.Command, - Detach: opts.Detach, - AutoRemove: opts.Remove, - Tty: !opts.noTty, - Interactive: opts.interactive, - WorkingDir: opts.workdir, - User: opts.user, - Environment: opts.environment, - Entrypoint: opts.entrypointCmd, + Name: options.name, + Service: options.Service, + Command: options.Command, + Detach: options.Detach, + AutoRemove: options.Remove, + Tty: !options.noTty, + Interactive: options.interactive, + WorkingDir: options.workdir, + User: options.user, + CapAdd: options.capAdd.GetAll(), + CapDrop: options.capDrop.GetAll(), + Environment: options.environment, + Entrypoint: options.entrypointCmd, Labels: labels, - UseNetworkAliases: opts.useAliases, - NoDeps: opts.noDeps, + UseNetworkAliases: options.useAliases, + NoDeps: options.noDeps, Index: 0, - QuietPull: opts.quietPull, + QuietPull: options.quietPull, } for i, service := range project.Services { - if service.Name == opts.Service { - service.StdinOpen = opts.interactive + if service.Name == options.Service { + service.StdinOpen = options.interactive project.Services[i] = service } } diff --git a/docs/reference/compose_run.md b/docs/reference/compose_run.md index 0c2b2bc8121..718e043c3ef 100644 --- a/docs/reference/compose_run.md +++ b/docs/reference/compose_run.md @@ -8,6 +8,8 @@ Run a one-off command on a service. | Name | Type | Default | Description | |:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------| | `--build` | | | Build image before starting container. | +| `--cap-add` | `list` | | Add Linux capabilities | +| `--cap-drop` | `list` | | Drop Linux capabilities | | `-d`, `--detach` | | | Run container in background and print container ID | | `--dry-run` | | | Execute command in dry run mode | | `--entrypoint` | `string` | | Override the entrypoint of the image | diff --git a/docs/reference/docker_compose_run.yaml b/docs/reference/docker_compose_run.yaml index df549233198..9e6c9a07b63 100644 --- a/docs/reference/docker_compose_run.yaml +++ b/docs/reference/docker_compose_run.yaml @@ -68,6 +68,24 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: cap-add + value_type: list + description: Add Linux capabilities + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false + - option: cap-drop + value_type: list + description: Drop Linux capabilities + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false - option: detach shorthand: d value_type: bool diff --git a/pkg/api/api.go b/pkg/api/api.go index f4f730a8f45..e1954be39ad 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -305,6 +305,8 @@ type RunOptions struct { WorkingDir string User string Environment []string + CapAdd []string + CapDrop []string Labels types.Labels Privileged bool UseNetworkAliases bool diff --git a/pkg/compose/run.go b/pkg/compose/run.go index 36fedbfafc2..d8428987211 100644 --- a/pkg/compose/run.go +++ b/pkg/compose/run.go @@ -26,6 +26,7 @@ import ( "github.com/docker/cli/cli" cmd "github.com/docker/cli/cli/command/container" "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/utils" "github.com/docker/docker/pkg/stringid" ) @@ -117,6 +118,14 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts if len(opts.User) > 0 { service.User = opts.User } + if len(opts.CapAdd) > 0 { + service.CapAdd = append(service.CapAdd, opts.CapAdd...) + service.CapDrop = utils.Remove(service.CapDrop, opts.CapAdd...) + } + if len(opts.CapDrop) > 0 { + service.CapDrop = append(service.CapDrop, opts.CapDrop...) + service.CapAdd = utils.Remove(service.CapAdd, opts.CapDrop...) + } if len(opts.WorkingDir) > 0 { service.WorkingDir = opts.WorkingDir }