Repository navigation
Expand file tree
/
Copy pathmain_test.go
More file actions
385 lines (332 loc) · 11.9 KB
/
Copy pathmain_test.go
File metadata and controls
385 lines (332 loc) · 11.9 KB
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
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestRunMetadataUsesInjectedVersion(t *testing.T) {
originalVersion := version
version = "v1.2.3"
t.Cleanup(func() {
version = originalVersion
})
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := run([]string{"docker-cli-plugin-metadata"}, &stdout, &stderr, func() (dockerClient, error) {
t.Fatal("docker client should not be created for metadata")
return nil, nil
})
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if stderr.Len() != 0 {
t.Fatalf("expected empty stderr, got %q", stderr.String())
}
var metadata map[string]any
if err := json.Unmarshal(stdout.Bytes(), &metadata); err != nil {
t.Fatalf("unmarshal metadata: %v", err)
}
if metadata["Version"] != "1.2.3" {
t.Fatalf("expected trimmed version, got %#v", metadata["Version"])
}
if metadata["ShortDescription"] != "Recreate containers" {
t.Fatalf("unexpected metadata: %#v", metadata)
}
}
func TestRunRequiresContainerName(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := run([]string{"recreate"}, &stdout, &stderr, func() (dockerClient, error) {
t.Fatal("docker client should not be created when args are invalid")
return nil, nil
})
if exitCode != 1 {
t.Fatalf("expected exit code 1, got %d", exitCode)
}
if stdout.Len() != 0 {
t.Fatalf("expected empty stdout, got %q", stdout.String())
}
if got := strings.TrimSpace(stderr.String()); got != "no container id or name specified" {
t.Fatalf("unexpected stderr: %q", got)
}
}
func TestRunHelpPrintsUsageAndSucceeds(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := run([]string{"--help"}, &stdout, &stderr, func() (dockerClient, error) {
t.Fatal("docker client should not be created for help")
return nil, nil
})
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if stderr.Len() != 0 {
t.Fatalf("expected empty stderr, got %q", stderr.String())
}
if !strings.Contains(stdout.String(), "Usage of recreate:") {
t.Fatalf("expected usage output, got %q", stdout.String())
}
}
func TestRunDirectInvocationRecreatesContainer(t *testing.T) {
client := &fakeDockerClient{
inspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
Image: "sha256:test",
Name: "/demo",
State: &container.State{},
HostConfig: &container.HostConfig{},
},
Config: &container.Config{Image: "alpine:latest"},
},
imageInspectResponse: image.InspectResponse{Os: "linux", Architecture: "arm64"},
createResponse: container.CreateResponse{ID: "1234567890abcdef"},
}
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := run([]string{"demo"}, &stdout, &stderr, func() (dockerClient, error) {
return client, nil
})
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d, stderr=%q", exitCode, stderr.String())
}
if stderr.Len() != 0 {
t.Fatalf("expected empty stderr, got %q", stderr.String())
}
if client.inspectedContainerID != "demo" {
t.Fatalf("expected inspect to use container arg, got %q", client.inspectedContainerID)
}
if client.removedContainerID != "demo" {
t.Fatalf("expected container removal, got %q", client.removedContainerID)
}
if client.createdContainerName != "demo" {
t.Fatalf("expected normalized name, got %q", client.createdContainerName)
}
if client.startedContainerID != "1234567890abcdef" {
t.Fatalf("expected container start, got %q", client.startedContainerID)
}
if client.createPlatform == nil || client.createPlatform.OS != "linux" || client.createPlatform.Architecture != "arm64" {
t.Fatalf("unexpected create platform: %#v", client.createPlatform)
}
if strings.Contains(stdout.String(), "tail:") {
t.Fatalf("unexpected debug output: %q", stdout.String())
}
}
func TestRecreateContainerPullsImageAndUsesDescriptorPlatform(t *testing.T) {
pullBody := &trackingReadCloser{Reader: strings.NewReader("status")}
client := &fakeDockerClient{
inspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
Image: "sha256:test",
Name: "/demo",
State: &container.State{},
HostConfig: &container.HostConfig{},
},
Config: &container.Config{Image: "repo/demo:latest"},
ImageManifestDescriptor: &v1.Descriptor{
Platform: &v1.Platform{OS: "linux", Architecture: "amd64", Variant: "v3"},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{"bridge": {}},
},
},
pullReadCloser: pullBody,
createResponse: container.CreateResponse{ID: "abcdef1234567890"},
}
var stdout bytes.Buffer
if err := recreateContainer(context.Background(), client, &stdout, "demo", true); err != nil {
t.Fatalf("recreate container: %v", err)
}
if client.imageInspectCalls != 0 {
t.Fatalf("expected image inspect to be skipped when descriptor platform exists, got %d", client.imageInspectCalls)
}
if client.pulledImageRef != "repo/demo:latest" {
t.Fatalf("unexpected pulled image ref: %q", client.pulledImageRef)
}
if client.pullPlatform != "linux/amd64/v3" {
t.Fatalf("unexpected pull platform: %q", client.pullPlatform)
}
if !pullBody.closed {
t.Fatal("expected image pull body to be closed")
}
if pullBody.bytesRead == 0 {
t.Fatal("expected image pull body to be consumed")
}
if client.createdNetworkingConfig == nil || client.createdNetworkingConfig.EndpointsConfig["bridge"] == nil {
t.Fatalf("unexpected networking config: %#v", client.createdNetworkingConfig)
}
if client.createPlatform == nil || client.createPlatform.Variant != "v3" {
t.Fatalf("unexpected create platform: %#v", client.createPlatform)
}
}
func TestRecreateContainerWaitsForAutoRemoveContainer(t *testing.T) {
waitCh := make(chan container.WaitResponse, 1)
errCh := make(chan error, 1)
waitCh <- container.WaitResponse{StatusCode: 0}
client := &fakeDockerClient{
inspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
Image: "sha256:test",
Name: "/demo",
State: &container.State{Running: true},
HostConfig: &container.HostConfig{AutoRemove: true},
},
Config: &container.Config{Image: "repo/demo:latest"},
},
imageInspectResponse: image.InspectResponse{Os: "linux", Architecture: "amd64"},
waitCh: waitCh,
errCh: errCh,
createResponse: container.CreateResponse{ID: "abcdef1234567890"},
}
if err := recreateContainer(context.Background(), client, io.Discard, "demo", false); err != nil {
t.Fatalf("recreate container: %v", err)
}
if client.stoppedContainerID != "demo" {
t.Fatalf("expected stop call, got %q", client.stoppedContainerID)
}
if client.waitedContainerID != "demo" {
t.Fatalf("expected wait for removal, got %q", client.waitedContainerID)
}
if client.removedContainerID != "" {
t.Fatalf("did not expect explicit remove, got %q", client.removedContainerID)
}
}
func TestRecreateContainerWaitErrorIncludesContext(t *testing.T) {
waitCh := make(chan container.WaitResponse, 1)
errCh := make(chan error)
waitCh <- container.WaitResponse{Error: &container.WaitExitError{Message: "remove failed"}}
client := &fakeDockerClient{
inspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
Image: "sha256:test",
Name: "/demo",
State: &container.State{Running: true},
HostConfig: &container.HostConfig{AutoRemove: true},
},
Config: &container.Config{Image: "repo/demo:latest"},
},
imageInspectResponse: image.InspectResponse{Os: "linux", Architecture: "amd64"},
waitCh: waitCh,
errCh: errCh,
}
err := recreateContainer(context.Background(), client, io.Discard, "demo", false)
if err == nil {
t.Fatal("expected wait error")
}
if got := err.Error(); got != "wait for container demo removal: remove failed" {
t.Fatalf("unexpected error: %q", got)
}
}
func TestResolvePlatformReturnsImageInspectError(t *testing.T) {
client := &fakeDockerClient{imageInspectErr: errors.New("boom")}
inspected := container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{Image: "sha256:test"},
}
_, err := resolvePlatform(context.Background(), client, inspected)
if err == nil || !strings.Contains(err.Error(), "inspect image sha256:test") {
t.Fatalf("expected wrapped inspect error, got %v", err)
}
}
func TestShortContainerIDHandlesShortValues(t *testing.T) {
if got := shortContainerID("abc123"); got != "abc123" {
t.Fatalf("expected unchanged short id, got %q", got)
}
if got := shortContainerID("1234567890abcdef"); got != "1234567890" {
t.Fatalf("expected truncated id, got %q", got)
}
}
type fakeDockerClient struct {
inspectResponse container.InspectResponse
inspectErr error
imageInspectResponse image.InspectResponse
imageInspectErr error
pullReadCloser io.ReadCloser
pullErr error
createResponse container.CreateResponse
createErr error
waitCh <-chan container.WaitResponse
errCh <-chan error
inspectedContainerID string
imageInspectCalls int
pulledImageRef string
pullPlatform string
stoppedContainerID string
removedContainerID string
waitedContainerID string
createdContainerName string
createdNetworkingConfig *network.NetworkingConfig
createPlatform *v1.Platform
startedContainerID string
}
func (f *fakeDockerClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
f.inspectedContainerID = containerID
return f.inspectResponse, f.inspectErr
}
func (f *fakeDockerClient) ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error) {
f.imageInspectCalls++
return f.imageInspectResponse, f.imageInspectErr
}
func (f *fakeDockerClient) ImagePull(_ context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) {
f.pulledImageRef = ref
f.pullPlatform = options.Platform
if f.pullReadCloser == nil {
f.pullReadCloser = io.NopCloser(strings.NewReader(""))
}
return f.pullReadCloser, f.pullErr
}
func (f *fakeDockerClient) ContainerStop(_ context.Context, containerID string, _ container.StopOptions) error {
f.stoppedContainerID = containerID
return nil
}
func (f *fakeDockerClient) ContainerRemove(_ context.Context, containerID string, _ container.RemoveOptions) error {
f.removedContainerID = containerID
return nil
}
func (f *fakeDockerClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
f.waitedContainerID = containerID
if f.waitCh == nil {
waitCh := make(chan container.WaitResponse, 1)
waitCh <- container.WaitResponse{StatusCode: 0}
f.waitCh = waitCh
}
if f.errCh == nil {
errCh := make(chan error, 1)
f.errCh = errCh
}
return f.waitCh, f.errCh
}
func (f *fakeDockerClient) ContainerCreate(_ context.Context, _ *container.Config, _ *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *v1.Platform, containerName string) (container.CreateResponse, error) {
f.createdContainerName = containerName
f.createdNetworkingConfig = networkingConfig
if platform != nil {
clone := *platform
f.createPlatform = &clone
}
return f.createResponse, f.createErr
}
func (f *fakeDockerClient) ContainerStart(_ context.Context, containerID string, _ container.StartOptions) error {
f.startedContainerID = containerID
return nil
}
type trackingReadCloser struct {
io.Reader
bytesRead int
closed bool
}
func (r *trackingReadCloser) Read(p []byte) (int, error) {
n, err := r.Reader.Read(p)
r.bytesRead += n
return n, err
}
func (r *trackingReadCloser) Close() error {
r.closed = true
return nil
}