Merge pull request #1341 from docker/run_opts

introduce a few more `compose run` options
This commit is contained in:
Guillaume Tardif 2021-02-23 13:59:30 +01:00 committed by GitHub
commit ea24e499e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 160 additions and 43 deletions

View File

@ -19,6 +19,7 @@ package compose
import ( import (
"context" "context"
"io" "io"
"strings"
"time" "time"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
@ -130,22 +131,40 @@ type RemoveOptions struct {
// RunOptions options to execute compose run // RunOptions options to execute compose run
type RunOptions struct { type RunOptions struct {
Name string
Service string Service string
Command []string Command []string
Entrypoint []string
Detach bool Detach bool
AutoRemove bool AutoRemove bool
Writer io.Writer Writer io.Writer
Reader io.Reader Reader io.Reader
// used by exec
Tty bool Tty bool
WorkingDir string WorkingDir string
User string User string
Environment []string Environment []string
Labels types.Labels
Privileged bool Privileged bool
// used by exec
Index int Index int
} }
// EnvironmentMap return RunOptions.Environment as a MappingWithEquals
func (opts *RunOptions) EnvironmentMap() types.MappingWithEquals {
environment := types.MappingWithEquals{}
for _, s := range opts.Environment {
parts := strings.SplitN(s, "=", 2)
key := parts[0]
switch {
case len(parts) == 1:
environment[key] = nil
default:
environment[key] = &parts[1]
}
}
return environment
}
// PsOptions group options of the Ps API // PsOptions group options of the Ps API
type PsOptions struct { type PsOptions struct {
All bool All bool

37
api/compose/api_test.go Normal file
View File

@ -0,0 +1,37 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"testing"
"gotest.tools/v3/assert"
)
func TestRunOptionsEnvironmentMap(t *testing.T) {
opts := RunOptions{
Environment: []string{
"FOO=BAR",
"ZOT=",
"QIX",
},
}
env := opts.EnvironmentMap()
assert.Equal(t, *env["FOO"], "BAR")
assert.Equal(t, *env["ZOT"], "")
assert.Check(t, env["QIX"] == nil)
}

View File

@ -18,9 +18,12 @@ package compose
import ( import (
"context" "context"
"fmt"
"os" "os"
"strings"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
@ -33,9 +36,15 @@ type runOptions struct {
*composeOptions *composeOptions
Service string Service string
Command []string Command []string
Environment []string environment []string
Detach bool Detach bool
Remove bool Remove bool
noTty bool
user string
workdir string
entrypoint string
labels []string
name string
} }
func runCommand(p *projectOptions) *cobra.Command { func runCommand(p *projectOptions) *cobra.Command {
@ -44,7 +53,7 @@ func runCommand(p *projectOptions) *cobra.Command {
projectOptions: p, projectOptions: p,
}, },
} }
runCmd := &cobra.Command{ cmd := &cobra.Command{
Use: "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]", Use: "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]",
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),
@ -56,12 +65,19 @@ func runCommand(p *projectOptions) *cobra.Command {
return runRun(cmd.Context(), opts) return runRun(cmd.Context(), opts)
}, },
} }
runCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID") flags := cmd.Flags()
runCmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables") flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
runCmd.Flags().BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits") flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
flags.StringArrayVarP(&opts.labels, "labels", "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", false, "Disable pseudo-tty allocation. By default docker compose run allocates a TTY")
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")
runCmd.Flags().SetInterspersed(false) flags.SetInterspersed(false)
return runCmd return cmd
} }
func runRun(ctx context.Context, opts runOptions) error { func runRun(ctx context.Context, opts runOptions) error {
@ -77,14 +93,39 @@ func runRun(ctx context.Context, opts runOptions) error {
return err return err
} }
var entrypoint []string
if opts.entrypoint != "" {
entrypoint, err = shellwords.Parse(opts.entrypoint)
if err != nil {
return err
}
}
labels := types.Labels{}
for _, s := range opts.labels {
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("label must be set as KEY=VALUE")
}
labels[parts[0]] = parts[1]
}
// start container and attach to container streams // start container and attach to container streams
runOpts := compose.RunOptions{ runOpts := compose.RunOptions{
Name: opts.name,
Service: opts.Service, Service: opts.Service,
Command: opts.Command, Command: opts.Command,
Detach: opts.Detach, Detach: opts.Detach,
AutoRemove: opts.Remove, AutoRemove: opts.Remove,
Writer: os.Stdout, Writer: os.Stdout,
Reader: os.Stdin, Reader: os.Stdin,
Tty: !opts.noTty,
WorkingDir: opts.workdir,
User: opts.user,
Environment: opts.environment,
Entrypoint: entrypoint,
Labels: labels,
Index: 0,
} }
exitCode, err := c.ComposeService().RunOneOffContainer(ctx, project, runOpts) exitCode, err := c.ComposeService().RunOneOffContainer(ctx, project, runOpts)
if exitCode != 0 { if exitCode != 0 {

1
go.mod
View File

@ -39,6 +39,7 @@ require (
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/labstack/echo v3.3.10+incompatible github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect github.com/labstack/gommon v0.3.0 // indirect
github.com/mattn/go-shellwords v1.0.11
github.com/moby/buildkit v0.8.1-0.20201205083753-0af7b1b9c693 github.com/moby/buildkit v0.8.1-0.20201205083753-0af7b1b9c693
github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf github.com/moby/term v0.0.0-20201110203204-bea5bbe245bf
github.com/morikuni/aec v1.0.0 github.com/morikuni/aec v1.0.0

View File

@ -30,37 +30,32 @@ import (
) )
func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) { func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
originalServices := project.Services service, err := project.GetService(opts.Service)
var requestedService types.ServiceConfig if err != nil {
for _, service := range originalServices { return 0, err
if service.Name == opts.Service {
requestedService = service
}
} }
project.Services = originalServices applyRunOptions(&service, opts)
if len(opts.Command) > 0 {
requestedService.Command = opts.Command
}
requestedService.Scale = 1
requestedService.Tty = true
requestedService.StdinOpen = true
slug := moby.GenerateRandomID() slug := moby.GenerateRandomID()
requestedService.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, requestedService.Name, moby.TruncateID(slug)) if service.ContainerName == "" {
requestedService.Labels = requestedService.Labels.Add(slugLabel, slug) service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, moby.TruncateID(slug))
requestedService.Labels = requestedService.Labels.Add(oneoffLabel, "True") }
service.Scale = 1
service.StdinOpen = true
service.Labels = service.Labels.Add(slugLabel, slug)
service.Labels = service.Labels.Add(oneoffLabel, "True")
if err := s.ensureImagesExists(ctx, project); err != nil { // all dependencies already checked, but might miss requestedService img if err := s.ensureImagesExists(ctx, project); err != nil { // all dependencies already checked, but might miss service img
return 0, err return 0, err
} }
if err := s.waitDependencies(ctx, project, requestedService); err != nil { if err := s.waitDependencies(ctx, project, service); err != nil {
return 0, err return 0, err
} }
if err := s.createContainer(ctx, project, requestedService, requestedService.ContainerName, 1, opts.AutoRemove); err != nil { if err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.AutoRemove); err != nil {
return 0, err return 0, err
} }
containerID := requestedService.ContainerName containerID := service.ContainerName
if opts.Detach { if opts.Detach {
err := s.apiClient.ContainerStart(ctx, containerID, apitypes.ContainerStartOptions{}) err := s.apiClient.ContainerStart(ctx, containerID, apitypes.ContainerStartOptions{})
@ -81,7 +76,7 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
return 0, err return 0, err
} }
oneoffContainer := containers[0] oneoffContainer := containers[0]
err = s.attachContainerStreams(ctx, oneoffContainer, true, opts.Reader, opts.Writer) err = s.attachContainerStreams(ctx, oneoffContainer, service.Tty, opts.Reader, opts.Writer)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -100,3 +95,27 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
} }
} }
func applyRunOptions(service *types.ServiceConfig, opts compose.RunOptions) {
service.Tty = opts.Tty
service.ContainerName = opts.Name
if len(opts.Command) > 0 {
service.Command = opts.Command
}
if len(opts.User) > 0 {
service.User = opts.User
}
if len(opts.WorkingDir) > 0 {
service.WorkingDir = opts.WorkingDir
}
if len(opts.Entrypoint) > 0 {
service.Entrypoint = opts.Entrypoint
}
if len(opts.Environment) > 0 {
service.Environment.OverrideBy(opts.EnvironmentMap())
}
for k, v := range opts.Labels {
service.Labels.Add(k, v)
}
}