1/* 2 Copyright 2020 Docker Compose CLI authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15*/ 16 17package compose 18 19import ( 20 "context" 21 "fmt" 22 "io" 23 24 "github.com/docker/cli/cli/streams" 25 moby "github.com/docker/docker/api/types" 26 "github.com/docker/docker/api/types/filters" 27 "github.com/docker/docker/pkg/stdcopy" 28 "github.com/moby/term" 29 30 "github.com/docker/compose/v2/pkg/api" 31) 32 33func (s *composeService) Exec(ctx context.Context, project string, opts api.RunOptions) (int, error) { 34 container, err := s.getExecTarget(ctx, project, opts) 35 if err != nil { 36 return 0, err 37 } 38 39 exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, moby.ExecConfig{ 40 Cmd: opts.Command, 41 Env: opts.Environment, 42 User: opts.User, 43 Privileged: opts.Privileged, 44 Tty: opts.Tty, 45 Detach: opts.Detach, 46 WorkingDir: opts.WorkingDir, 47 48 AttachStdin: true, 49 AttachStdout: true, 50 AttachStderr: true, 51 }) 52 if err != nil { 53 return 0, err 54 } 55 56 if opts.Detach { 57 return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{ 58 Detach: true, 59 Tty: opts.Tty, 60 }) 61 } 62 63 resp, err := s.apiClient.ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{ 64 Tty: opts.Tty, 65 }) 66 if err != nil { 67 return 0, err 68 } 69 defer resp.Close() //nolint:errcheck 70 71 if opts.Tty { 72 s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize) 73 if err != nil { 74 return 0, err 75 } 76 } 77 78 err = s.interactiveExec(ctx, opts, resp) 79 if err != nil { 80 return 0, err 81 } 82 83 return s.getExecExitStatus(ctx, exec.ID) 84} 85 86// inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116 87func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOptions, resp moby.HijackedResponse) error { 88 outputDone := make(chan error) 89 inputDone := make(chan error) 90 91 stdout := ContainerStdout{HijackedResponse: resp} 92 stdin := ContainerStdin{HijackedResponse: resp} 93 r, err := s.getEscapeKeyProxy(opts.Stdin) 94 if err != nil { 95 return err 96 } 97 98 in := streams.NewIn(opts.Stdin) 99 if in.IsTerminal() { 100 state, err := term.SetRawTerminal(in.FD()) 101 if err != nil { 102 return err 103 } 104 defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck 105 } 106 107 go func() { 108 if opts.Tty { 109 _, err := io.Copy(opts.Stdout, stdout) 110 outputDone <- err 111 } else { 112 _, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout) 113 outputDone <- err 114 } 115 stdout.Close() //nolint:errcheck 116 }() 117 118 go func() { 119 _, err := io.Copy(stdin, r) 120 inputDone <- err 121 stdin.Close() //nolint:errcheck 122 }() 123 124 for { 125 select { 126 case err := <-outputDone: 127 return err 128 case err := <-inputDone: 129 if _, ok := err.(term.EscapeError); ok { 130 return nil 131 } 132 if err != nil { 133 return err 134 } 135 // Wait for output to complete streaming 136 case <-ctx.Done(): 137 return ctx.Err() 138 } 139 } 140} 141 142func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) { 143 containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ 144 Filters: filters.NewArgs( 145 projectFilter(projectName), 146 serviceFilter(opts.Service), 147 containerNumberFilter(opts.Index), 148 ), 149 }) 150 if err != nil { 151 return moby.Container{}, err 152 } 153 if len(containers) < 1 { 154 return moby.Container{}, fmt.Errorf("service %q is not running container #%d", opts.Service, opts.Index) 155 } 156 container := containers[0] 157 return container, nil 158} 159 160func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) { 161 resp, err := s.apiClient.ContainerExecInspect(ctx, execID) 162 if err != nil { 163 return 0, err 164 } 165 return resp.ExitCode, nil 166} 167