introduce run --cap-add to run maintenance commands using service image

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2023-06-08 11:22:44 +02:00 committed by Nicolas De loof
parent ff3984e609
commit c61b8aa5ac
5 changed files with 99 additions and 59 deletions

View File

@ -24,6 +24,7 @@ import (
cgo "github.com/compose-spec/compose-go/cli" cgo "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/loader"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/cli/opts"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -48,6 +49,8 @@ type runOptions struct {
workdir string workdir string
entrypoint string entrypoint string
entrypointCmd []string entrypointCmd []string
capAdd opts.ListOpts
capDrop opts.ListOpts
labels []string labels []string
volumes []string volumes []string
publish []string publish []string
@ -59,20 +62,20 @@ type runOptions struct {
quietPull bool quietPull bool
} }
func (opts runOptions) apply(project *types.Project) error { func (options runOptions) apply(project *types.Project) error {
target, err := project.GetService(opts.Service) target, err := project.GetService(options.Service)
if err != nil { if err != nil {
return err return err
} }
target.Tty = !opts.noTty target.Tty = !options.noTty
target.StdinOpen = opts.interactive target.StdinOpen = options.interactive
if !opts.servicePorts { if !options.servicePorts {
target.Ports = []types.ServicePortConfig{} target.Ports = []types.ServicePortConfig{}
} }
if len(opts.publish) > 0 { if len(options.publish) > 0 {
target.Ports = []types.ServicePortConfig{} target.Ports = []types.ServicePortConfig{}
for _, p := range opts.publish { for _, p := range options.publish {
config, err := types.ParsePortConfig(p) config, err := types.ParsePortConfig(p)
if err != nil { if err != nil {
return err return err
@ -80,8 +83,8 @@ func (opts runOptions) apply(project *types.Project) error {
target.Ports = append(target.Ports, config...) target.Ports = append(target.Ports, config...)
} }
} }
if len(opts.volumes) > 0 { if len(options.volumes) > 0 {
for _, v := range opts.volumes { for _, v := range options.volumes {
volume, err := loader.ParseVolume(v) volume, err := loader.ParseVolume(v)
if err != nil { if err != nil {
return err return err
@ -90,15 +93,15 @@ func (opts runOptions) apply(project *types.Project) error {
} }
} }
if opts.noDeps { if options.noDeps {
err := project.ForServices([]string{opts.Service}, types.IgnoreDependencies) err := project.ForServices([]string{options.Service}, types.IgnoreDependencies)
if err != nil { if err != nil {
return err return err
} }
} }
for i, s := range project.Services { for i, s := range project.Services {
if s.Name == opts.Service { if s.Name == options.Service {
project.Services[i] = target project.Services[i] = target
break 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 { func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
opts := runOptions{ options := runOptions{
composeOptions: &composeOptions{ composeOptions: &composeOptions{
ProjectOptions: p, ProjectOptions: p,
}, },
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
} }
createOpts := createOptions{} createOpts := createOptions{}
cmd := &cobra.Command{ 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.", Short: "Run a one-off command on a service.",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error { PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
opts.Service = args[0] options.Service = args[0]
if len(args) > 1 { 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") return fmt.Errorf("--service-ports and --publish are incompatible")
} }
if cmd.Flags().Changed("entrypoint") { if cmd.Flags().Changed("entrypoint") {
command, err := shellwords.Parse(opts.entrypoint) command, err := shellwords.Parse(options.entrypoint)
if err != nil { if err != nil {
return err return err
} }
opts.entrypointCmd = command options.entrypointCmd = command
} }
if cmd.Flags().Changed("tty") { if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") { if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together") return fmt.Errorf("--tty and --no-TTY can't be used together")
} else { } else {
opts.noTty = !opts.tty options.noTty = !options.tty
} }
} }
return nil return nil
}), }),
RunE: Adapt(func(ctx context.Context, args []string) error { 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 { if err != nil {
return err return err
} }
opts.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans]) options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, opts, createOpts, streams) return runRun(ctx, backend, project, options, createOpts, streams)
}), }),
ValidArgsFunction: completeServiceNames(p), ValidArgsFunction: completeServiceNames(p),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID") flags.BoolVarP(&options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") flags.StringArrayVarP(&options.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label") flags.StringArrayVarP(&options.labels, "label", "l", []string{}, "Add or override a label")
flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits") flags.BoolVar(&options.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.BoolVarP(&options.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.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid") flags.StringVarP(&options.user, "user", "u", "", "Run as specified username or uid")
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container") flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVar(&opts.entrypoint, "entrypoint", "", "Override the entrypoint of the image") flags.StringVar(&options.entrypoint, "entrypoint", "", "Override the entrypoint of the image")
flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.") flags.Var(&options.capAdd, "cap-add", "Add Linux capabilities")
flags.StringArrayVarP(&opts.volumes, "volume", "v", []string{}, "Bind mount a volume.") flags.Var(&options.capDrop, "cap-drop", "Drop Linux capabilities")
flags.StringArrayVarP(&opts.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.") flags.BoolVar(&options.noDeps, "no-deps", false, "Don't start linked services.")
flags.BoolVar(&opts.useAliases, "use-aliases", false, "Use the service's network useAliases in the network(s) the container connects to.") flags.StringArrayVarP(&options.volumes, "volume", "v", []string{}, "Bind mount a volume.")
flags.BoolVar(&opts.servicePorts, "service-ports", false, "Run command with the service's ports enabled and mapped to the host.") flags.StringArrayVarP(&options.publish, "publish", "p", []string{}, "Publish a container's port(s) to the host.")
flags.BoolVar(&opts.quietPull, "quiet-pull", false, "Pull without printing progress information.") 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.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.") 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(&options.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.tty, "tty", "t", true, "Allocate a pseudo-TTY.")
cmd.Flags().MarkHidden("tty") //nolint:errcheck cmd.Flags().MarkHidden("tty") //nolint:errcheck
flags.SetNormalizeFunc(normalizeRunFlags) flags.SetNormalizeFunc(normalizeRunFlags)
@ -190,8 +197,8 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(name) return pflag.NormalizedName(name)
} }
func runRun(ctx context.Context, backend api.Service, project *types.Project, opts runOptions, createOpts createOptions, streams api.Streams) error { func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, streams api.Streams) error {
err := opts.apply(project) err := options.apply(project)
if err != nil { if err != nil {
return err 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 { 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()) }, streams.Err())
if err != nil { if err != nil {
return err return err
} }
labels := types.Labels{} labels := types.Labels{}
for _, s := range opts.labels { for _, s := range options.labels {
parts := strings.SplitN(s, "=", 2) parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 { if len(parts) != 2 {
return fmt.Errorf("label must be set as KEY=VALUE") 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 // start container and attach to container streams
runOpts := api.RunOptions{ runOpts := api.RunOptions{
Name: opts.name, Name: options.name,
Service: opts.Service, Service: options.Service,
Command: opts.Command, Command: options.Command,
Detach: opts.Detach, Detach: options.Detach,
AutoRemove: opts.Remove, AutoRemove: options.Remove,
Tty: !opts.noTty, Tty: !options.noTty,
Interactive: opts.interactive, Interactive: options.interactive,
WorkingDir: opts.workdir, WorkingDir: options.workdir,
User: opts.user, User: options.user,
Environment: opts.environment, CapAdd: options.capAdd.GetAll(),
Entrypoint: opts.entrypointCmd, CapDrop: options.capDrop.GetAll(),
Environment: options.environment,
Entrypoint: options.entrypointCmd,
Labels: labels, Labels: labels,
UseNetworkAliases: opts.useAliases, UseNetworkAliases: options.useAliases,
NoDeps: opts.noDeps, NoDeps: options.noDeps,
Index: 0, Index: 0,
QuietPull: opts.quietPull, QuietPull: options.quietPull,
} }
for i, service := range project.Services { for i, service := range project.Services {
if service.Name == opts.Service { if service.Name == options.Service {
service.StdinOpen = opts.interactive service.StdinOpen = options.interactive
project.Services[i] = service project.Services[i] = service
} }
} }

View File

@ -8,6 +8,8 @@ Run a one-off command on a service.
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------| |:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------|
| `--build` | | | Build image before starting container. | | `--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 | | `-d`, `--detach` | | | Run container in background and print container ID |
| `--dry-run` | | | Execute command in dry run mode | | `--dry-run` | | | Execute command in dry run mode |
| `--entrypoint` | `string` | | Override the entrypoint of the image | | `--entrypoint` | `string` | | Override the entrypoint of the image |

View File

@ -68,6 +68,24 @@ options:
experimentalcli: false experimentalcli: false
kubernetes: false kubernetes: false
swarm: 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 - option: detach
shorthand: d shorthand: d
value_type: bool value_type: bool

View File

@ -305,6 +305,8 @@ type RunOptions struct {
WorkingDir string WorkingDir string
User string User string
Environment []string Environment []string
CapAdd []string
CapDrop []string
Labels types.Labels Labels types.Labels
Privileged bool Privileged bool
UseNetworkAliases bool UseNetworkAliases bool

View File

@ -26,6 +26,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
cmd "github.com/docker/cli/cli/command/container" cmd "github.com/docker/cli/cli/command/container"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/utils"
"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringid"
) )
@ -117,6 +118,14 @@ func applyRunOptions(project *types.Project, service *types.ServiceConfig, opts
if len(opts.User) > 0 { if len(opts.User) > 0 {
service.User = opts.User 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 { if len(opts.WorkingDir) > 0 {
service.WorkingDir = opts.WorkingDir service.WorkingDir = opts.WorkingDir
} }