Skip to content

Commit

Permalink
introduce run --cap-add to run maintenance commands using service image
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <[email protected]>
  • Loading branch information
ndeloof committed Jun 8, 2023
1 parent 4bf2fe9 commit aae5d87
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 59 deletions.
123 changes: 64 additions & 59 deletions cmd/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,31 +58,32 @@ type runOptions struct {
noDeps bool
ignoreOrphans bool
quietPull bool
capAdd opts.ListOpts
}

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
}
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
Expand All @@ -90,15 +92,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
}
Expand All @@ -107,72 +109,74 @@ 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),
}
createOpts := createOptions{}
cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
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.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)
Expand All @@ -190,8 +194,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
}
Expand All @@ -202,14 +206,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")
Expand All @@ -219,27 +223,28 @@ 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(),
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
}
}
Expand Down
1 change: 1 addition & 0 deletions docs/reference/compose_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Run a one-off command on a service.
| Name | Type | Default | Description |
|:----------------------|:--------------|:--------|:----------------------------------------------------------------------------------|
| `--build` | | | Build image before starting container. |
| `--cap-add` | `list` | | Add 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 |
Expand Down
9 changes: 9 additions & 0 deletions docs/reference/docker_compose_run.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ 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: detach
shorthand: d
value_type: bool
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ type RunOptions struct {
WorkingDir string
User string
Environment []string
CapAdd []string
Labels types.Labels
Privileged bool
UseNetworkAliases bool
Expand Down
3 changes: 3 additions & 0 deletions pkg/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ 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...)
}
if len(opts.WorkingDir) > 0 {
service.WorkingDir = opts.WorkingDir
}
Expand Down

0 comments on commit aae5d87

Please sign in to comment.