Add `compose exec` command

Signed-off-by: aiordache <anca.iordache@docker.com>
This commit is contained in:
aiordache 2021-02-15 09:56:34 +01:00
parent 8029db807c
commit afac025a49
10 changed files with 284 additions and 0 deletions

View File

@ -214,3 +214,7 @@ func (cs *aciComposeService) RunOneOffContainer(ctx context.Context, project *ty
func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
return errdefs.ErrNotImplemented
}

View File

@ -87,3 +87,7 @@ func (c *composeService) RunOneOffContainer(ctx context.Context, project *types.
func (c *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { func (c *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
func (c *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
return errdefs.ErrNotImplemented
}

View File

@ -55,6 +55,8 @@ type Service interface {
RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error) RunOneOffContainer(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
// Remove executes the equivalent to a `compose rm` // Remove executes the equivalent to a `compose rm`
Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error)
// Exec executes a command in a running service container
Exec(ctx context.Context, project *types.Project, opts RunOptions) error
} }
// CreateOptions group options of the Create API // CreateOptions group options of the Create API
@ -117,6 +119,14 @@ type RunOptions struct {
AutoRemove bool AutoRemove bool
Writer io.Writer Writer io.Writer
Reader io.Reader Reader io.Reader
// used by exec
Tty bool
WorkingDir string
User string
Environment []string
Privileged bool
Index int
} }
// PsOptions group options of the Ps API // PsOptions group options of the Ps API

View File

@ -117,6 +117,7 @@ func Command(contextType string) *cobra.Command {
killCommand(&opts), killCommand(&opts),
runCommand(&opts), runCommand(&opts),
removeCommand(&opts), removeCommand(&opts),
execCommand(&opts),
) )
if contextType == store.LocalContextType || contextType == store.DefaultContextType { if contextType == store.LocalContextType || contextType == store.DefaultContextType {

118
cli/cmd/compose/exec.go Normal file
View File

@ -0,0 +1,118 @@
/*
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 (
"context"
"fmt"
"os"
"github.com/containerd/console"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/compose"
)
type execOpts struct {
*composeOptions
service string
command []string
environment []string
workingDir string
tty bool
user string
detach bool
index int
privileged bool
}
func execCommand(p *projectOptions) *cobra.Command {
opts := execOpts{
composeOptions: &composeOptions{
projectOptions: p,
},
}
runCmd := &cobra.Command{
Use: "exec [options] [-e KEY=VAL...] [--] SERVICE COMMAND [ARGS...]",
Short: "Execute a command in a running container.",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
opts.command = args[1:]
}
opts.service = args[0]
return runExec(cmd.Context(), opts)
},
}
runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.")
runCmd.Flags().StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables")
runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].")
runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.")
runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.")
runCmd.Flags().BoolVarP(&opts.tty, "", "T", false, "Disable pseudo-tty allocation. By default `docker compose exec` allocates a TTY.")
runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.")
runCmd.Flags().SetInterspersed(false)
return runCmd
}
func runExec(ctx context.Context, opts execOpts) error {
c, err := client.NewWithDefaultLocalBackend(ctx)
if err != nil {
return err
}
project, err := opts.toProject(nil)
if err != nil {
return err
}
execOpts := compose.RunOptions{
Service: opts.service,
Command: opts.command,
Environment: opts.environment,
Tty: !opts.tty,
User: opts.user,
Privileged: opts.privileged,
Index: opts.index,
Detach: opts.detach,
WorkingDir: opts.workingDir,
Writer: os.Stdout,
Reader: os.Stdin,
}
if execOpts.Tty {
con := console.Current()
if err := con.SetRaw(); err != nil {
return err
}
defer func() {
if err := con.Reset(); err != nil {
fmt.Println("Unable to close the console")
}
}()
execOpts.Writer = con
execOpts.Reader = con
}
return c.ComposeService().Exec(ctx, project, execOpts)
}

View File

@ -158,6 +158,8 @@ func main() {
opts.AddConfigFlags(flags) opts.AddConfigFlags(flags)
flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
flags.SetInterspersed(false)
walk(root, func(c *cobra.Command) { walk(root, func(c *cobra.Command) {
c.Flags().BoolP("help", "h", false, "Help for "+c.Name()) c.Flags().BoolP("help", "h", false, "Help for "+c.Name())
}) })

30
ecs/exec.go Normal file
View File

@ -0,0 +1,30 @@
/*
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 ecs
import (
"context"
"github.com/compose-spec/compose-go/types"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/errdefs"
)
func (b *ecsAPIService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
return errdefs.ErrNotImplemented
}

View File

@ -179,3 +179,7 @@ func (e ecsLocalSimulation) RunOneOffContainer(ctx context.Context, project *typ
func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { func (e ecsLocalSimulation) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return e.compose.Remove(ctx, project, options) return e.compose.Remove(ctx, project, options)
} }
func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
return errdefs.ErrNotImplemented
}

View File

@ -201,3 +201,8 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) { func (s *composeService) Remove(ctx context.Context, project *types.Project, options compose.RemoveOptions) ([]string, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
// Exec executes a command in a running service container
func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
return errdefs.ErrNotImplemented
}

106
local/compose/exec.go Normal file
View File

@ -0,0 +1,106 @@
/*
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 (
"context"
"fmt"
"io"
"github.com/compose-spec/compose-go/types"
apitypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/compose-cli/api/compose"
)
func (s *composeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error {
service, err := project.GetService(opts.Service)
if err != nil {
return err
}
containers, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{
Filters: filters.NewArgs(
projectFilter(project.Name),
serviceFilter(service.Name),
filters.Arg("label", fmt.Sprintf("%s=%d", containerNumberLabel, opts.Index)),
),
})
if err != nil {
return err
}
if len(containers) < 1 {
return fmt.Errorf("container %s not running", getContainerName(project.Name, service, opts.Index))
}
container := containers[0]
exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, apitypes.ExecConfig{
Cmd: opts.Command,
Env: opts.Environment,
User: opts.User,
Privileged: opts.Privileged,
Tty: opts.Tty,
Detach: opts.Detach,
WorkingDir: opts.WorkingDir,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
})
if err != nil {
return err
}
if opts.Detach {
return s.apiClient.ContainerExecStart(ctx, exec.ID, apitypes.ExecStartCheck{
Detach: true,
Tty: opts.Tty,
})
}
resp, err := s.apiClient.ContainerExecAttach(ctx, exec.ID, apitypes.ExecStartCheck{
Detach: false,
Tty: opts.Tty,
})
if err != nil {
return err
}
defer resp.Close()
readChannel := make(chan error, 10)
writeChannel := make(chan error, 10)
go func() {
_, err := io.Copy(opts.Writer, resp.Reader)
readChannel <- err
}()
go func() {
_, err := io.Copy(resp.Conn, opts.Reader)
writeChannel <- err
}()
for {
select {
case err := <-readChannel:
return err
case err := <-writeChannel:
return err
}
}
}