/
filewriter.go
142 lines (117 loc) · 3.49 KB
/
filewriter.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
package dockerutil
import (
"archive/tar"
"bytes"
"context"
"fmt"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"go.uber.org/zap"
)
// FileWriter allows retrieving a single file from a Docker volume.
// In the future it may allow retrieving an entire directory.
type FileWriter struct {
log *zap.Logger
cli *client.Client
testName string
}
// NewFileWriter returns a new FileWriter.
func NewFileWriter(log *zap.Logger, cli *client.Client, testName string) *FileWriter {
return &FileWriter{log: log, cli: cli, testName: testName}
}
// WriteFile writes the single file containing content, at relPath within the given volume.
func (w *FileWriter) WriteFile(ctx context.Context, volumeName, relPath string, content []byte) error {
const mountPath = "/mnt/dockervolume"
if err := ensureBusybox(ctx, w.cli); err != nil {
return err
}
containerName := fmt.Sprintf("interchaintest-writefile-%d-%s", time.Now().UnixNano(), RandLowerCaseLetterString(5))
cc, err := w.cli.ContainerCreate(
ctx,
&container.Config{
Image: busyboxRef,
Entrypoint: []string{"sh", "-c"},
Cmd: []string{
// Take the uid and gid of the mount path,
// and set that as the owner of the new relative path.
`chown -R "$(stat -c '%u:%g' "$1")" "$2"`,
"_", // Meaningless arg0 for sh -c with positional args.
mountPath,
mountPath,
},
// Use root user to avoid permission issues when reading files from the volume.
User: GetRootUserString(),
Labels: map[string]string{CleanupLabel: w.testName},
},
&container.HostConfig{
Binds: []string{volumeName + ":" + mountPath},
AutoRemove: true,
},
nil, // No networking necessary.
nil,
containerName,
)
if err != nil {
return fmt.Errorf("creating container: %w", err)
}
autoRemoved := false
defer func() {
if autoRemoved {
// No need to attempt removing the container if we successfully started and waited for it to complete.
return
}
if err := w.cli.ContainerRemove(ctx, cc.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
w.log.Warn("Failed to remove file content container", zap.String("container_id", cc.ID), zap.Error(err))
}
}()
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
if err := tw.WriteHeader(&tar.Header{
Name: relPath,
Size: int64(len(content)),
Mode: 0600,
// Not setting uname because the container will chown it anyway.
ModTime: time.Now(),
Format: tar.FormatPAX,
}); err != nil {
return fmt.Errorf("writing tar header: %w", err)
}
if _, err := tw.Write(content); err != nil {
return fmt.Errorf("writing content to tar: %w", err)
}
if err := tw.Close(); err != nil {
return fmt.Errorf("closing tar writer: %w", err)
}
if err := w.cli.CopyToContainer(
ctx,
cc.ID,
mountPath,
&buf,
types.CopyToContainerOptions{},
); err != nil {
return fmt.Errorf("copying tar to container: %w", err)
}
if err := w.cli.ContainerStart(ctx, cc.ID, types.ContainerStartOptions{}); err != nil {
return fmt.Errorf("starting write-file container: %w", err)
}
waitCh, errCh := w.cli.ContainerWait(ctx, cc.ID, container.WaitConditionNotRunning)
select {
case <-ctx.Done():
return ctx.Err()
case err := <-errCh:
return err
case res := <-waitCh:
autoRemoved = true
if res.Error != nil {
return fmt.Errorf("waiting for write-file container: %s", res.Error.Message)
}
if res.StatusCode != 0 {
return fmt.Errorf("chown on new file exited %d", res.StatusCode)
}
}
return nil
}