-
Notifications
You must be signed in to change notification settings - Fork 8
/
exec.go
132 lines (111 loc) · 3.1 KB
/
exec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package dockerutil
import (
"bytes"
"context"
"io"
"time"
dockertypes "github.com/docker/docker/api/types"
"golang.org/x/xerrors"
"github.com/coder/envbox/xio"
"github.com/coder/retry"
)
type ExecConfig struct {
ContainerID string
User string
Cmd string
Args []string
Stdin io.Reader
StdOutErr io.Writer
Env []string
Detach bool
}
// ExecContainer runs a command in a container. It returns the output and any error.
// If an error occurs during the execution of the command, the output is appended to the error.
func ExecContainer(ctx context.Context, client DockerClient, config ExecConfig) (io.Reader, error) {
exec, err := client.ContainerExecCreate(ctx, config.ContainerID, dockertypes.ExecConfig{
Detach: config.Detach,
Cmd: append([]string{config.Cmd}, config.Args...),
User: config.User,
AttachStderr: true,
AttachStdout: true,
AttachStdin: config.Stdin != nil,
Env: config.Env,
})
if err != nil {
return nil, xerrors.Errorf("exec create: %w", err)
}
resp, err := client.ContainerExecAttach(ctx, exec.ID, dockertypes.ExecStartCheck{})
if err != nil {
return nil, xerrors.Errorf("attach to exec: %w", err)
}
defer resp.Close()
if config.Stdin != nil {
_, err = io.Copy(resp.Conn, config.Stdin)
if err != nil {
return nil, xerrors.Errorf("copy stdin: %w", err)
}
err = resp.CloseWrite()
if err != nil {
return nil, xerrors.Errorf("close write: %w", err)
}
}
var (
buf bytes.Buffer
// Avoid capturing too much output. We want to prevent
// a memory leak. This is especially important when
// we run the bootstrap script since we do not return.
psw = &xio.PrefixSuffixWriter{
W: &buf,
N: 1 << 10,
}
wr io.Writer = psw
)
if config.StdOutErr != nil {
wr = io.MultiWriter(psw, config.StdOutErr)
}
_, err = io.Copy(wr, resp.Reader)
if err != nil {
return nil, xerrors.Errorf("copy cmd output: %w", err)
}
resp.Close()
inspect, err := client.ContainerExecInspect(ctx, exec.ID)
if err != nil {
return nil, xerrors.Errorf("exec inspect: %w", err)
}
if inspect.Running {
return nil, xerrors.Errorf("unexpectedly still running")
}
if inspect.ExitCode > 0 {
return nil, xerrors.Errorf("%s: exit code %d", buf.Bytes(), inspect.ExitCode)
}
return &buf, nil
}
func GetExecPID(ctx context.Context, client DockerClient, execID string) (int, error) {
for r := retry.New(time.Second, time.Second); r.Wait(ctx); {
inspect, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
return 0, xerrors.Errorf("exec inspect: %w", err)
}
if inspect.Pid == 0 {
continue
}
return inspect.Pid, nil
}
return 0, ctx.Err()
}
func WaitForExit(ctx context.Context, client DockerClient, execID string) error {
for r := retry.New(time.Second, time.Second); r.Wait(ctx); {
inspect, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
return xerrors.Errorf("exec inspect: %w", err)
}
if inspect.Running {
continue
}
if inspect.ExitCode > 0 {
return xerrors.Errorf("exit code %d", inspect.ExitCode)
}
return nil
}
return ctx.Err()
}