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 (
"context"
"io"
"strings"
"time"
"github.com/compose-spec/compose-go/types"
@ -130,20 +131,38 @@ type RemoveOptions struct {
// RunOptions options to execute compose run
type RunOptions struct {
Service string
Command []string
Detach bool
AutoRemove bool
Writer io.Writer
Reader io.Reader
// used by exec
Name string
Service string
Command []string
Entrypoint []string
Detach bool
AutoRemove bool
Writer io.Writer
Reader io.Reader
Tty bool
WorkingDir string
User string
Environment []string
Labels types.Labels
Privileged bool
Index int
// used by exec
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

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 (
"context"
"fmt"
"os"
"strings"
"github.com/compose-spec/compose-go/types"
"github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client"
@ -33,9 +36,15 @@ type runOptions struct {
*composeOptions
Service string
Command []string
Environment []string
environment []string
Detach bool
Remove bool
noTty bool
user string
workdir string
entrypoint string
labels []string
name string
}
func runCommand(p *projectOptions) *cobra.Command {
@ -44,7 +53,7 @@ func runCommand(p *projectOptions) *cobra.Command {
projectOptions: p,
},
}
runCmd := &cobra.Command{
cmd := &cobra.Command{
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.",
Args: cobra.MinimumNArgs(1),
@ -56,12 +65,19 @@ func runCommand(p *projectOptions) *cobra.Command {
return runRun(cmd.Context(), opts)
},
}
runCmd.Flags().BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
runCmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits")
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, "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)
return runCmd
flags.SetInterspersed(false)
return cmd
}
func runRun(ctx context.Context, opts runOptions) error {
@ -77,14 +93,39 @@ func runRun(ctx context.Context, opts runOptions) error {
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
runOpts := compose.RunOptions{
Service: opts.Service,
Command: opts.Command,
Detach: opts.Detach,
AutoRemove: opts.Remove,
Writer: os.Stdout,
Reader: os.Stdin,
Name: opts.name,
Service: opts.Service,
Command: opts.Command,
Detach: opts.Detach,
AutoRemove: opts.Remove,
Writer: os.Stdout,
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)
if exitCode != 0 {

1
go.mod
View File

@ -39,6 +39,7 @@ require (
github.com/joho/godotenv v1.3.0
github.com/labstack/echo v3.3.10+incompatible
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/term v0.0.0-20201110203204-bea5bbe245bf
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) {
originalServices := project.Services
var requestedService types.ServiceConfig
for _, service := range originalServices {
if service.Name == opts.Service {
requestedService = service
}
service, err := project.GetService(opts.Service)
if err != nil {
return 0, err
}
project.Services = originalServices
if len(opts.Command) > 0 {
requestedService.Command = opts.Command
}
requestedService.Scale = 1
requestedService.Tty = true
requestedService.StdinOpen = true
applyRunOptions(&service, opts)
slug := moby.GenerateRandomID()
requestedService.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, requestedService.Name, moby.TruncateID(slug))
requestedService.Labels = requestedService.Labels.Add(slugLabel, slug)
requestedService.Labels = requestedService.Labels.Add(oneoffLabel, "True")
if service.ContainerName == "" {
service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, moby.TruncateID(slug))
}
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
}
if err := s.waitDependencies(ctx, project, requestedService); err != nil {
if err := s.waitDependencies(ctx, project, service); err != nil {
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
}
containerID := requestedService.ContainerName
containerID := service.ContainerName
if opts.Detach {
err := s.apiClient.ContainerStart(ctx, containerID, apitypes.ContainerStartOptions{})
@ -81,7 +76,7 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
return 0, err
}
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 {
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)
}
}