mirror of https://github.com/docker/compose.git
Merge pull request #1341 from docker/run_opts
introduce a few more `compose run` options
This commit is contained in:
commit
ea24e499e6
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
1
go.mod
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue