master
go 101 lines 2.59 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package dockerhost
4
5 import (
6 "bytes"
7 "context"
8 "fmt"
9 "os"
10 "strings"
11 "time"
12
13 typesContainer "github.com/docker/docker/api/types/container"
14 docker "github.com/docker/docker/client"
15 "github.com/docker/docker/pkg/stdcopy"
16 )
17
18 func FromEnv() string {
19 addr := os.Getenv("DOCKER_HOST")
20 if addr == "" {
21 return ""
22 }
23 if strings.HasPrefix(addr, "tcp://") || strings.HasPrefix(addr, "unix://") {
24 return addr
25 }
26 if strings.HasPrefix(addr, "/") {
27 return fmt.Sprintf("unix://%s", addr)
28 }
29 return fmt.Sprintf("tcp://%s", addr)
30 }
31
32 func Exec(ctx context.Context, container string, cmd string, args ...string) ([]byte, error) {
33 // based on https://github.com/moby/moby/blob/8e610b2b55bfd1bfa9436ab110d311f5e8a74dcb/integration/internal/container/exec.go#L38
34 addr := docker.DefaultDockerHost
35 if v := FromEnv(); v != "" {
36 addr = v
37 }
38
39 cli, err := docker.NewClientWithOpts(docker.WithHost(addr))
40 if err != nil {
41 return nil, fmt.Errorf("failed to create docker client: %w", err)
42 }
43 defer func() { _ = cli.Close() }()
44
45 cli.NegotiateAPIVersion(ctx)
46
47 execCreateConfig := typesContainer.ExecOptions{
48 AttachStderr: true,
49 AttachStdout: true,
50 Cmd: append([]string{cmd}, args...),
51 }
52
53 createResp, err := cli.ContainerExecCreate(ctx, container, execCreateConfig)
54 if err != nil {
55 return nil, fmt.Errorf("failed to container exec create (%s): %w", container, err)
56 }
57
58 attachResp, err := cli.ContainerExecAttach(ctx, createResp.ID, typesContainer.ExecAttachOptions{})
59 if err != nil {
60 return nil, fmt.Errorf("failed to container exec attach (%s): %w", container, err)
61 }
62 defer attachResp.Close()
63
64 var outBuf, errBuf bytes.Buffer
65 done := make(chan error, 1)
66
67 go func() {
68 _, err := stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader)
69 done <- err
70 }()
71
72 select {
73 case err := <-done:
74 if err != nil {
75 return nil, fmt.Errorf("failed to read response from container (%s): %w", container, err)
76 }
77 case <-ctx.Done():
78 // Close connection to interrupt StdCopy
79 attachResp.Close()
80
81 select {
82 case <-done:
83 case <-time.After(150 * time.Millisecond):
84 // Don't wait too long, let it clean up in background
85 }
86
87 return nil, fmt.Errorf("timed out reading response: %w", ctx.Err())
88 }
89
90 inspResp, err := cli.ContainerExecInspect(ctx, createResp.ID)
91 if err != nil {
92 return nil, fmt.Errorf("failed to container exec inspect (%s): %w", container, err)
93 }
94
95 if inspResp.ExitCode != 0 {
96 msg := strings.ReplaceAll(errBuf.String(), "\n", " ")
97 return nil, fmt.Errorf("command returned non-zero exit code (%d), error: %q", inspResp.ExitCode, msg)
98 }
99
100 return outBuf.Bytes(), nil
101 }