forked from adampointer/cali
-
Notifications
You must be signed in to change notification settings - Fork 7
/
docker.go
500 lines (413 loc) · 13.2 KB
/
docker.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
package cali
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"path"
"syscall"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/net/context"
"gopkg.in/cheggaaa/pb.v1"
)
// Event holds the json structure for Docker API events
type Event struct {
ID string `json:"id"`
Status string `json:"status"`
}
// ProgressDetail records the progress achieved downloading an image
type ProgressDetail struct {
Current int `json:"current,omitempty"`
Total int `json:"total,omitempty"`
}
// CreateResponse is the response from Docker API when pulling an image
type CreateResponse struct {
ID string `json:"id"`
Status string `json:"status"`
ProgressDetail ProgressDetail `json:"progressDetail"`
Progress string `json:"progress,omitempty"`
}
// DockerClient is a slimmed down implementation of the docker cli
type DockerClient struct {
Cli *client.Client
HostConf *container.HostConfig
NetConf *network.NetworkingConfig
Conf *container.Config
}
// InitDocker initialises the client
func (c *DockerClient) InitDocker() error {
// Do nothing if already initialised Docker
if c.Cli != nil {
return nil
}
var cli *client.Client
defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"}
cli, err := client.NewClient(dockerHost, "v1.22", nil, defaultHeaders)
if err != nil {
return fmt.Errorf("Could not connect to Docker daemon on %s: %s", dockerHost, err)
}
c.Cli = cli
return nil
}
// NewDockerClient returns a new DockerClient initialised with the API object
func NewDockerClient() *DockerClient {
c := new(DockerClient)
c.SetDefaults()
return c
}
// SetDefaults sets container, host and net configs to defaults. Called when instantiating a new client or can be called
// manually at any time to reset API configs back to empty defaults
func (c *DockerClient) SetDefaults() {
c.HostConf = &container.HostConfig{Binds: []string{}}
c.NetConf = &network.NetworkingConfig{}
c.Conf = &container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
OpenStdin: true,
Tty: true,
Env: []string{},
}
}
// SetHostConf sets the container.HostConfig struct for the new container
func (c *DockerClient) SetHostConf(h *container.HostConfig) {
c.HostConf = h
}
// SetNetConf sets the network.NetworkingConfig struct for the new container
func (c *DockerClient) SetNetConf(n *network.NetworkingConfig) {
c.NetConf = n
}
// SetConf sets the container.Config struct for the new container
func (c *DockerClient) SetConf(co *container.Config) {
c.Conf = co
}
// AddBind adds a bind mount to the HostConfig
func (c *DockerClient) AddBind(bnd string) {
c.HostConf.Binds = append(c.HostConf.Binds, bnd)
}
// AddEnv adds an environment variable to the HostConfig
func (c *DockerClient) AddEnv(key, value string) {
c.Conf.Env = append(c.Conf.Env, fmt.Sprintf("%s=%s", key, value))
}
// AddBinds adds multiple bind mounts to the HostConfig
func (c *DockerClient) AddBinds(bnds []string) {
c.HostConf.Binds = append(c.HostConf.Binds, bnds...)
}
// AddEnvs adds multiple envs to the HostConfig
func (c *DockerClient) AddEnvs(envs []string) {
c.Conf.Env = append(c.Conf.Env, envs...)
}
// SetBinds sets the bind mounts in the HostConfig
func (c *DockerClient) SetBinds(bnds []string) {
c.HostConf.Binds = bnds
}
// SetEnvs sets the environment variables in the Conf
func (c *DockerClient) SetEnvs(envs []string) {
c.Conf.Env = envs
}
// SetImage sets the image in Conf
func (c *DockerClient) SetImage(img string) {
c.Conf.Image = img
}
// Privileged sets whether the container should run as privileged
func (c *DockerClient) Privileged(p bool) {
c.HostConf.Privileged = p
}
// SetCmd sets the command to run in the container
func (c *DockerClient) SetCmd(cmd []string) {
c.Conf.Cmd = cmd
}
// SetWorkDir sets the working directory of the container
func (c *DockerClient) SetWorkDir(wd string) {
c.Conf.WorkingDir = wd
}
// BindFromGit creates a data container with a git clone inside and mounts its volumes inside your app container
// If there is no valid Git repo set in config, the noGit callback function will be executed instead
func (c *DockerClient) BindFromGit(cfg *GitCheckoutConfig, noGit func() error) error {
cli := NewDockerClient()
if err := cli.InitDocker(); err != nil {
return err
}
if cfg.Repo != "" {
// Build code from data volume
git := cli.Git()
if cfg.Image != "" {
git.Image = cfg.Image
}
id, err := git.Checkout(cfg)
if err != nil {
return err
}
c.HostConf.VolumesFrom = []string{id}
if cfg.RelPath != "" {
c.SetWorkDir(path.Join(workdir, cfg.RelPath))
}
} else {
// Execute callback
_ = noGit()
}
return nil
}
// deleteContainerOnSignal waits for a signal then deletes a container
func (c *DockerClient) deleteContainerOnSignal(id string, sigs chan os.Signal) {
sig := <-sigs
log.Debugf("Trapped signal: %s", sig)
if err := c.DeleteContainer(id); err != nil {
log.Fatalf("Failed to remove container: %s", err)
}
}
// StartContainer will create and start a container with logs and optional cleanup
func (c *DockerClient) StartContainer(rm bool, name string) (string, error) {
if err := c.InitDocker(); err != nil {
return "", err
}
log.WithFields(log.Fields{
"image": c.Conf.Image,
"envs": fmt.Sprintf("%v", c.Conf.Env),
"cmd": fmt.Sprintf("%v", c.Conf.Cmd),
}).Debug("Creating new container")
exists, err := c.ImageExists(c.Conf.Image)
if err != nil {
return "", fmt.Errorf("Failed to create container: %s", err)
}
if !exists {
if err := c.PullImage(c.Conf.Image); err != nil {
return "", fmt.Errorf("Failed to fetch image: %s", err)
}
}
// Disable TTY if we're non-interactive, or if we're not in a terminal
if nonInteractive || !terminal.IsTerminal(int(os.Stdout.Fd())) {
c.Conf.Tty = false
}
resp, err := c.Cli.ContainerCreate(context.Background(), c.Conf, c.HostConf, c.NetConf, name)
if err != nil {
return "", fmt.Errorf("Failed to create container: %s", err)
}
// Clean up on ctrl+c or kill
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
go c.deleteContainerOnSignal(resp.ID, ch)
log.WithFields(log.Fields{
"image": c.Conf.Image,
"id": resp.ID[0:12],
}).Debug("Starting new container")
// Set the TTY size to match the host terminal
fd := int(os.Stdin.Fd())
// TODO: I would argue that, at some point, we change this behaviour to be more inline with Docker:
// -i interactive - default true if in a terminal, or if we're piping stuff in with stdin
// -t tty - default true if in a terminal
// But given Cali doesn't currently support piping stuff in from stdin... not important yet
if !nonInteractive && terminal.IsTerminal(int(os.Stdout.Fd())) {
// While we have a container running, create a buffer for the pscli logs
logBuffer := bufio.NewWriter(os.Stdout)
log.SetOutput(logBuffer)
// Write buffer to stdout once detatched from container
defer logBuffer.Flush()
// Reset logs to stdout after conection is closed
defer log.SetOutput(os.Stdout)
// If we have an interactive terminal then use it!
ca := types.ContainerAttachOptions{
Stream: true,
Stdin: true,
Stdout: true,
Stderr: true,
}
hijack, err := c.Cli.ContainerAttach(context.Background(), resp.ID, ca)
if err != nil {
return resp.ID, fmt.Errorf("Failed to start container: %s", err)
}
defer hijack.Conn.Close()
oldState, err := terminal.MakeRaw(fd)
defer func() {
_ = terminal.Restore(fd, oldState)
}()
if err != nil {
panic(err)
}
if err := c.Cli.ContainerStart(context.Background(), resp.ID, types.ContainerStartOptions{}); err != nil {
return resp.ID, fmt.Errorf("Failed to start container: %s", err)
}
// Start stdin reader
go func() {
defer func() {
_ = terminal.Restore(fd, oldState)
}()
defer hijack.Conn.Close()
if _, err := io.Copy(hijack.Conn, os.Stdin); err != nil {
log.Errorf("Write error: %s", err)
}
}()
err = c.autoResizeContainer(resp.ID)
if err != nil {
return resp.ID, fmt.Errorf("Failed to start container: %s", err)
}
// Start stdout writer
if _, err := io.Copy(os.Stdout, hijack.Conn); err != nil {
log.Errorf("Read error: %s", err)
}
} else {
if err := c.startContainerNonInteractive(resp.ID); err != nil {
return resp.ID, fmt.Errorf("Failed to start container: %s", err)
}
}
// Container has finished running. Get its exit code
inspect, err := c.Cli.ContainerInspect(context.Background(), resp.ID)
if err != nil {
return resp.ID, fmt.Errorf("Failed to inspect Docker container: %s", err)
}
if rm {
if err = c.DeleteContainer(resp.ID); err != nil {
return resp.ID, fmt.Errorf("Failed to remove container: %s", err)
}
}
if inspect.State.ExitCode != 0 {
return resp.ID, fmt.Errorf("Non-zero exit status from Docker container")
}
return resp.ID, nil
}
// startContainerNonInteractive starts a container, and just copies its logs to stdout
func (c *DockerClient) startContainerNonInteractive(containerID string) error {
// No terminal, then just pump out the log output
if err := c.Cli.ContainerStart(context.Background(), containerID, types.ContainerStartOptions{}); err != nil {
return fmt.Errorf("Failed to start container: %s", err)
}
log.WithFields(log.Fields{
"image": c.Conf.Image,
"id": containerID[0:12],
}).Debug("Fetching log stream")
logOptions := types.ContainerLogsOptions{Follow: true, ShowStdout: true, ShowStderr: true}
containerLogs, err := c.Cli.ContainerLogs(context.Background(), containerID, logOptions)
if err != nil {
return fmt.Errorf("Failed to get container logs: %s", err)
}
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, containerLogs)
if err != nil {
return fmt.Errorf("Failed to get container logs: %s", err)
}
return nil
}
// ContainerExists determines if the container with this name exist
func (c *DockerClient) ContainerExists(name string) (bool, error) {
if err := c.InitDocker(); err != nil {
return false, err
}
_, err := c.Cli.ContainerInspect(context.Background(), name)
// Fairly safe assumption: no errors == container exists
if err != nil {
return false, nil
}
return true, nil
}
// DeleteContainer - Delete a container
func (c *DockerClient) DeleteContainer(id string) error {
if err := c.InitDocker(); err != nil {
return err
}
log.WithFields(log.Fields{
"id": id[0:12],
}).Debug("Removing container")
if err := c.Cli.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true}); err != nil {
return fmt.Errorf("Failed to remove container: %s", err)
}
return nil
}
// ImageExists determines if an image exist locally
func (c *DockerClient) ImageExists(image string) (bool, error) {
if err := c.InitDocker(); err != nil {
return false, err
}
log.WithFields(log.Fields{
"image": image,
}).Debug("Checking if image exists locally")
_, _, err := c.Cli.ImageInspectWithRaw(context.Background(), image)
// Safe assumption?
if err != nil {
log.WithFields(log.Fields{
"image": image,
}).Debugf("Error inspecting image: %s", err)
return false, nil
}
return true, nil
}
// PullImage - Pull an image locally
func (c *DockerClient) PullImage(image string) error {
if err := c.InitDocker(); err != nil {
return err
}
log.WithFields(log.Fields{
"image": image,
}).Info("Pulling image layers... please wait")
resp, err := c.Cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("API could not fetch \"%s\": %s", image, err)
}
scanner := bufio.NewScanner(resp)
var cr CreateResponse
bar := pb.New(1)
// Send progress bar to stderr to keep stdout clean when piping
bar.Output = os.Stderr
bar.ShowCounters = true
bar.ShowTimeLeft = false
bar.ShowSpeed = false
bar.Prefix(" ")
bar.Postfix(" ")
started := false
for scanner.Scan() {
txt := scanner.Text()
byt := []byte(txt)
if err := json.Unmarshal(byt, &cr); err != nil {
return fmt.Errorf("Error decoding json from create image API: %s", err)
}
if cr.Status == "Downloading" {
if !started {
fmt.Print("\n")
bar.Total = int64(cr.ProgressDetail.Total)
bar.Start()
started = true
}
bar.Total = int64(cr.ProgressDetail.Total)
bar.Set(cr.ProgressDetail.Current)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("Failed to get logs: %s", err)
}
bar.Finish()
fmt.Print("\n")
return nil
}
func (c *DockerClient) autoResizeContainer(id string) error {
// Initial resize
err := c.resizeContainer(id)
if err != nil {
return err
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGWINCH)
// goroutine to check for the SIGWINCH
go func() {
for err == nil {
<-ch
err = c.resizeContainer(id)
}
log.WithField("container", id).Debug("Finished auto-resize")
}()
return nil
}
func (c *DockerClient) resizeContainer(id string) error {
fd := int(os.Stdin.Fd())
tw, th, _ := terminal.GetSize(fd)
var err error
if err = c.Cli.ContainerResize(context.Background(), id, types.ResizeOptions{Height: uint(th), Width: uint(tw)}); err != nil {
return fmt.Errorf("Failed to resize container: %s", err)
}
return nil
}