This repository has been archived by the owner on Jun 13, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 174
/
pushpull_test.go
380 lines (336 loc) · 13.8 KB
/
pushpull_test.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
package e2e
import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/docker/app/internal"
"github.com/docker/cnab-to-oci/converter"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/assert"
"gotest.tools/assert/cmp"
"gotest.tools/fs"
"gotest.tools/icmd"
)
type dindSwarmAndRegistryInfo struct {
swarmAddress string
registryAddress string
configuredCmd icmd.Cmd
stopRegistry func()
registryLogs func() string
}
func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) {
cmd, cleanup := dockerCli.createTestCmd()
defer cleanup()
registryPort := findAvailablePort()
tmpDir := fs.NewDir(t, t.Name())
defer tmpDir.Remove()
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
// The dind doesn't have the cnab-app-base image so we save it in order to load it later
saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))}
icmd.RunCmd(saveCmd).Assert(t, icmd.Success)
// we have a difficult constraint here:
// - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry)
// - the registry must be reachable from the dind daemon on the same address/port
// Solution found is: fix the port of the registry to be the same internally and externally
// and run the dind container in the same network namespace: this way 127.0.0.1:<registry-port> both resolves to the registry from the client and from dind
swarm := NewContainer("docker:18.09-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort))
swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10),
"-p", fmt.Sprintf("%d:%d", registryPort, registryPort),
"-p", "2375")
defer swarm.Stop(t)
registry := NewContainer("registry:2", registryPort)
registry.StartWithContainerNetwork(t, swarm, "-e", "REGISTRY_VALIDATION_MANIFESTS_URLS_ALLOW=[^http]",
"-e", fmt.Sprintf("REGISTRY_HTTP_ADDR=0.0.0.0:%d", registryPort))
defer registry.StopNoFail()
// We need two contexts:
// - one for `docker` so that it connects to the dind swarm created before
// - the target context for the invocation image to install within the swarm
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// When creating a context on a Windows host we cannot use
// the unix socket but it's needed inside the invocation image.
// The workaround is to create a context with an empty host.
// This host will default to the unix socket inside the
// invocation image
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// Initialize the swarm
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
cmd.Command = dockerCli.Command("swarm", "init")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// Load the needed base cnab image into the swarm docker engine
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
info := dindSwarmAndRegistryInfo{
configuredCmd: cmd,
registryAddress: registry.GetAddress(t),
swarmAddress: swarm.GetAddress(t),
stopRegistry: registry.StopNoFail,
registryLogs: registry.Logs(t),
}
todo(info)
}
func TestPushArchs(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
testCases := []struct {
name string
args []string
expectedPlatforms []manifestlist.PlatformSpec
}{
{
name: "default",
args: []string{},
expectedPlatforms: []manifestlist.PlatformSpec{
{
OS: "linux",
Architecture: "amd64",
},
},
},
{
name: "all-platforms",
args: []string{"--all-platforms"},
expectedPlatforms: []manifestlist.PlatformSpec{
{
OS: "linux",
Architecture: "amd64",
},
{
OS: "linux",
Architecture: "386",
},
{
OS: "linux",
Architecture: "ppc64le",
},
{
OS: "linux",
Architecture: "s390x",
},
{
OS: "linux",
Architecture: "arm",
Variant: "v5",
},
{
OS: "linux",
Architecture: "arm",
Variant: "v6",
},
{
OS: "linux",
Architecture: "arm",
Variant: "v7",
},
{
OS: "linux",
Architecture: "arm64",
Variant: "v8",
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
cmd := info.configuredCmd
ref := info.registryAddress + "/test/push-pull:1"
args := []string{"app", "push", "--tag", ref}
args = append(args, testCase.args...)
args = append(args, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
cmd.Command = dockerCli.Command(args...)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
var index v1.Index
headers := map[string]string{
"Accept": "application/vnd.docker.distribution.manifest.list.v2+json",
}
err := httpGet("http://"+info.registryAddress+"/v2/test/push-pull/manifests/1", headers, &index)
assert.NilError(t, err, info.registryLogs())
digest, err := getManifestListDigest(index)
assert.NilError(t, err, info.registryLogs())
var manifestList manifestlist.ManifestList
err = httpGet("http://"+info.registryAddress+"/v2/test/push-pull/manifests/"+digest.String(), headers, &manifestList)
assert.NilError(t, err)
assert.Equal(t, len(manifestList.Manifests), len(testCase.expectedPlatforms), "Unexpected number of platforms")
for _, m := range manifestList.Manifests {
assert.Assert(t, cmp.Contains(testCase.expectedPlatforms, m.Platform), "Platform expected but not found: %s", m.Platform)
}
})
}
})
}
func TestPushInsecureRegistry(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
ref := info.registryAddress + "/test/push-insecure"
// create a command outside of the dind context so without the insecure registry configured
cmd2, cleanup2 := dockerCli.createTestCmd()
defer cleanup2()
cmd2.Command = dockerCli.Command("app", "push", "--tag", ref, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd2).Assert(t, icmd.Expected{ExitCode: 1})
// run the push with the command inside dind context configured to allow access to the insecure registry
cmd := info.configuredCmd
cmd.Command = dockerCli.Command("app", "push", "--tag", ref, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
})
}
func TestPushInstall(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
ref := info.registryAddress + "/test/push-pull"
cmd.Command = dockerCli.Command("app", "push", "--tag", ref, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "install", ref, "--name", t.Name())
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref))
})
}
func TestPushPullInstall(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
ref := info.registryAddress + "/test/push-pull"
tag := ":v.0.0.1"
cmd.Command = dockerCli.Command("app", "push", "--tag", ref+tag, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "pull", ref+tag)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// stop the registry
info.stopRegistry()
// install without --pull should succeed (rely on local store)
cmd.Command = dockerCli.Command("app", "install", ref+tag, "--name", t.Name())
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref))
// listing the installed application shows the pulled application reference
cmd.Command = dockerCli.Command("app", "ls")
checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(),
[]string{
fmt.Sprintf(`%s\s+push-pull \(1.1.0-beta1\)\s+install\s+success\s+.+second[s]?\sago\s+.+second[s]?\sago\s+%s`, t.Name(), ref+tag),
})
// install with --pull should fail (registry is stopped)
cmd.Command = dockerCli.Command("app", "install", "--pull", ref, "--name", t.Name()+"2")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1}).Combined(), "failed to resolve bundle manifest"))
})
}
func TestPushInstallBundle(t *testing.T) {
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
cmd := info.configuredCmd
ref := info.registryAddress + "/test/push-bundle"
tmpDir := fs.NewDir(t, t.Name())
defer tmpDir.Remove()
bundleFile := tmpDir.Join("bundle.json")
// render the app to a bundle, we use the app from the push pull test above.
cmd.Command = dockerCli.Command("app", "bundle", "-o", bundleFile, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd).Assert(t, icmd.Success)
// push it and install to check it is available
t.Run("push-bundle", func(t *testing.T) {
name := strings.Replace(t.Name(), "/", "_", 1)
cmd.Command = dockerCli.Command("app", "push", "--tag", ref, bundleFile)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "install", ref, "--name", name)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref))
// ensure it doesn't confuse the next test
cmd.Command = dockerCli.Command("app", "rm", name)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, !strings.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref))
})
// push it again using the first ref and install from the new ref to check it is also available
t.Run("push-ref", func(t *testing.T) {
name := strings.Replace(t.Name(), "/", "_", 1)
ref2 := info.registryAddress + "/test/push-ref"
cmd.Command = dockerCli.Command("app", "push", "--tag", ref2, ref+":latest")
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("app", "install", ref2, "--name", name)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref2))
})
// push it again using an app pre-bundled and tagged in the bundle store and install it to check it is also available
t.Run("push-bundleref", func(t *testing.T) {
name := strings.Replace(t.Name(), "/", "_", 1)
ref2 := ref + ":v0.42"
// Create a new command so the bundle store can be trashed before installing the app
cmd2, cleanup2 := dockerCli.createTestCmd()
// Enter the same context as `cmd` to run commands within the same environment
cmd2.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, info.swarmAddress))
icmd.RunCmd(cmd2).Assert(t, icmd.Success)
cmd2.Env = append(cmd2.Env, "DOCKER_CONTEXT=swarm-context")
// bundle the app again but this time with a tag to store it into the bundle store
cmd2.Command = dockerCli.Command("app", "bundle", "--tag", ref2, "-o", bundleFile, filepath.Join("testdata", "push-pull", "push-pull.dockerapp"))
icmd.RunCmd(cmd2).Assert(t, icmd.Success)
// Push the app without tagging it explicitly
cmd2.Command = dockerCli.Command("app", "push", ref2)
icmd.RunCmd(cmd2).Assert(t, icmd.Success)
// remove the bundle from the bundle store to be sure it won't be used instead of registry
cleanup2()
// install from the registry
cmd.Command = dockerCli.Command("app", "install", ref2, "--name", name)
icmd.RunCmd(cmd).Assert(t, icmd.Success)
cmd.Command = dockerCli.Command("service", "ls")
assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref))
})
})
}
func findAvailablePort() int {
rand.Seed(time.Now().UnixNano())
for {
candidate := (rand.Int() % 2000) + 5000
if isPortAvailable(candidate) {
return candidate
}
}
}
func isPortAvailable(port int) bool {
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false
}
defer l.Close()
return true
}
func httpGet(url string, headers map[string]string, obj interface{}) error {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
for k, v := range headers {
req.Header.Set(k, v)
}
r, err := client.Do(req)
if err != nil {
return err
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
return fmt.Errorf("unexpected http error code %d with message %s", r.StatusCode, string(body))
}
if err := json.NewDecoder(r.Body).Decode(obj); err != nil {
return err
}
return nil
}
func getManifestListDigest(index v1.Index) (digest.Digest, error) {
for _, m := range index.Manifests {
if m.Annotations[converter.CNABDescriptorTypeAnnotation] == "component" {
return m.Digest, nil
}
}
return "", fmt.Errorf("Service image not found")
}