-
Notifications
You must be signed in to change notification settings - Fork 767
Expand file tree
/
Copy pathrequirements.go
More file actions
474 lines (432 loc) · 16.1 KB
/
requirements.go
File metadata and controls
474 lines (432 loc) · 16.1 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
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
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdtest
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"gotest.tools/v3/assert"
"github.com/containerd/containerd/v2/defaults"
"github.com/containerd/nerdctl/mod/tigron/require"
"github.com/containerd/nerdctl/mod/tigron/test"
"github.com/containerd/nerdctl/v2/pkg/buildkitutil"
"github.com/containerd/nerdctl/v2/pkg/clientutil"
ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults"
"github.com/containerd/nerdctl/v2/pkg/infoutil"
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
"github.com/containerd/nerdctl/v2/pkg/netutil"
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
"github.com/containerd/nerdctl/v2/pkg/snapshotterutil"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform"
)
var BuildkitHost test.ConfigKey = "BuildkitHost"
// These are used for ambient requirements
var ipv6 test.ConfigKey = "IPv6Test"
var kubernetes test.ConfigKey = "KubeTest"
var flaky test.ConfigKey = "FlakyTest"
var only test.ConfigValue = "Only"
// These are used for down the road configuration and custom behavior inside command
var modePrivate test.ConfigKey = "PrivateMode"
var stargz test.ConfigKey = "Stargz"
var ipfs test.ConfigKey = "IPFS"
var enabled test.ConfigValue = "Enabled"
// OnlyIPv6 marks a test as suitable to be run exclusively inside an ipv6 environment
// This is an ambient requirement
var OnlyIPv6 = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
helpers.Write(ipv6, only)
ret = environmentHasIPv6()
if !ret {
mess = "runner skips IPv6 compatible tests in the non-IPv6 environment"
}
return ret, mess
},
}
// OnlyKubernetes marks a test as meant to be tested on Kubernetes
// This is an ambient requirement
var OnlyKubernetes = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
helpers.Write(kubernetes, only)
if _, err := exec.LookPath("kubectl"); err != nil {
return false, fmt.Sprintf("kubectl is not in the path: %+v", err)
}
ret = environmentHasKubernetes()
if ret {
helpers.Write(Namespace, "k8s.io")
} else {
mess = "runner skips Kubernetes compatible tests in the non-Kubernetes environment"
}
return ret, mess
},
}
// IsFlaky marks a test as randomly failing.
// This is an ambient requirement
var IsFlaky = func(issueLink string) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
// We do not even want to get to the setup phase here
helpers.Write(flaky, only)
ret = environmentIsForFlaky()
if !ret {
mess = "runner skips flaky compatible tests in the non-flaky environment"
}
return ret, mess
},
}
}
// Docker marks a test as suitable solely for Docker and not Nerdctl
// Generally used as require.Not(nerdtest.Docker), which of course it the opposite
var Docker = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = !isTargetNerdish()
if ret {
mess = "current target is docker"
} else {
mess = "current target is not docker"
}
return ret, mess
},
}
// NerdctlNeedsFixing marks a test as unsuitable to be run for Nerdctl, because of a specific known issue which
// url must be passed as an argument
var NerdctlNeedsFixing = func(issueLink string) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = !isTargetNerdish()
if ret {
mess = "current target is docker"
} else {
mess = "current target is nerdctl, but we will skip as nerdctl currently has issue: " + issueLink
}
return ret, mess
},
}
}
// BrokenTest marks a test as currently broken, with explanation provided in message, along with
// additional requirements / restrictions describing what it can run on.
var BrokenTest = func(message string, req *test.Requirement) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
ret, mess := req.Check(data, helpers)
return ret, message + "\n" + mess
},
Setup: req.Setup,
Cleanup: req.Cleanup,
}
}
// Rootless marks a test as suitable only for the rootless environment
var Rootless = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
// Make sure we DO not return "IsRootless true" for docker
ret = isTargetNerdish() && rootlessutil.IsRootless()
if ret {
mess = "environment is root-less"
} else {
mess = "environment is root-ful"
}
return ret, mess
},
}
// Rootful marks a test as suitable only for rootful env
var Rootful = require.Not(Rootless)
// CGroup requires that cgroup is enabled
var CGroup = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = true
mess = "cgroup is enabled"
stdout := helpers.Capture("info", "--format", "{{ json . }}")
var dinf dockercompat.Info
err := json.Unmarshal([]byte(stdout), &dinf)
assert.NilError(helpers.T(), err, "failed to parse docker info")
switch dinf.CgroupDriver {
case "none", "":
ret = false
mess = "cgroup is none"
default:
}
return ret, mess
},
}
var CgroupsAccessible = require.All(
CGroup,
&test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
isRootLess := isTargetNerdish() && rootlessutil.IsRootless()
if isRootLess {
stdout := helpers.Capture("info", "--format", "{{ json . }}")
var dinf dockercompat.Info
err := json.Unmarshal([]byte(stdout), &dinf)
assert.NilError(helpers.T(), err, "failed to parse docker info")
return dinf.CgroupVersion == "2", "we are rootless, and cgroup version is not 2"
}
return true, ""
},
},
)
// CGroupV2 requires that cgroup is enabled and cgroup version is 2
var CGroupV2 = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = true
mess = "cgroup is enabled"
stdout := helpers.Capture("info", "--format", "{{ json . }}")
var dinf dockercompat.Info
err := json.Unmarshal([]byte(stdout), &dinf)
assert.NilError(helpers.T(), err, "failed to parse docker info")
switch dinf.CgroupDriver {
case "none", "":
ret = false
mess = "cgroup is none"
default:
}
if dinf.CgroupVersion != "2" {
ret = false
mess = "cgroup version is not 2"
}
return ret, mess
},
}
// Soci requires that the soci snapshotter is enabled
var Soci = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = false
mess = "soci is not enabled"
stdout := helpers.Capture("info", "--format", "{{ json . }}")
var dinf dockercompat.Info
err := json.Unmarshal([]byte(stdout), &dinf)
assert.NilError(helpers.T(), err, "failed to parse docker info")
for _, p := range dinf.Plugins.Storage {
if p == "soci" {
ret = true
mess = "soci is enabled"
}
}
return ret, mess
},
}
var Stargz = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
ret = false
mess = "stargz is not enabled"
stdout := helpers.Capture("info", "--format", "{{ json . }}")
var dinf dockercompat.Info
err := json.Unmarshal([]byte(stdout), &dinf)
assert.NilError(helpers.T(), err, "failed to parse docker info")
for _, p := range dinf.Plugins.Storage {
if p == "stargz" {
ret = true
mess = "stargz is enabled"
}
}
// Need this to happen now for Cleanups to work
// FIXME: we should be able to access the env (at least through helpers.Command().) instead of this gym
helpers.Write(stargz, enabled)
return ret, mess
},
}
// Registry marks a test as requiring a registry to be deployed
var Registry = require.All(
// Registry requires Linux currently
require.Linux,
(func() *test.Requirement {
// Provisional: see note in cleanup
// var reg *registry.Server
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
return true, ""
},
Setup: func(data test.Data, helpers test.Helpers) {
// Ensure we have registry images now, so that we can run --pull=never
// This is useful for two reasons:
// - if ghcr.io is out, we want to fail early
// - when we start a large number of registries in subtests, no need to round-trip to ghcr everytime
// This of course assumes that the subtests are NOT going to prune / rmi images
registryImage := platform.RegistryImageStable
helpers.Ensure("pull", "--quiet", registryImage)
helpers.Ensure("pull", "--quiet", platform.DockerAuthImage)
helpers.Ensure("pull", "--quiet", platform.KuboImage)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
// FIXME: figure out what to do with reg setup/cleanup routines
// Provisionally, reg is available here in the closure
},
}
})(),
)
// Build marks a test as suitable only if buildkitd is enabled (only tested for nerdctl obviously)
var Build = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
// FIXME: shouldn't we run buildkitd in a container? At least for testing, that would be so much easier than
// against the host install
ret := true
mess := "buildkitd is enabled"
if isTargetNerdish() {
bkHostAddr, err := buildkitutil.GetBuildkitHost(defaultNamespace)
if err != nil {
ret = false
mess = fmt.Sprintf("buildkitd is not enabled: %+v", err)
return ret, mess
}
// We also require the buildctl binary in the path
_, err = exec.LookPath("buildctl")
if err != nil {
ret = false
mess = fmt.Sprintf("buildctl is not in the path: %+v", err)
return ret, mess
}
helpers.Write(BuildkitHost, test.ConfigValue(bkHostAddr))
}
return ret, mess
},
Cleanup: func(data test.Data, helpers test.Helpers) {
// Previously, every build test was sequential, and was purging the build cache.
// Running this in parallel of any other test depending on build will trash it.
// The only way to parallelize any test involving build is indeed to disable this.
// The price to pay is that we might get cache from another test.
// This can be avoided individually by passing --no-cache if and when necessary
// helpers.Anyhow("builder", "prune", "--all", "--force")
},
}
var IPFS = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
// FIXME: we should be able to access the env (at least through helpers.Command().) instead of this gym
helpers.Write(ipfs, enabled)
// FIXME: this is incomplete. We obviously need a daemon running, properly configured
return require.Binary("ipfs").Check(data, helpers)
},
}
// Private makes a test run inside a dedicated namespace, with a private config.toml, hosts directory, and DOCKER_CONFIG path
// If the target is docker, parallelism is forcefully disabled
var Private = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
// We need this to happen NOW and not in setup, as otherwise cleanup with operate on the default namespace
namespace := data.Identifier("private")
helpers.Write(Namespace, test.ConfigValue(namespace))
data.Labels().Set("_deletenamespace", namespace)
// FIXME: is this necessary? Should NoParallel be subsumed into config?
helpers.Write(modePrivate, enabled)
return true, "private mode creates a dedicated namespace for nerdctl, and disable parallelism for docker"
},
Cleanup: func(data test.Data, helpers test.Helpers) {
if isTargetNerdish() {
// FIXME: there are conditions where we still have some stuff in there and this fails...
containerList := strings.TrimSpace(helpers.Capture("ps", "-aq"))
if containerList != "" {
helpers.Ensure(append([]string{"rm", "-f"}, strings.Split(containerList, "\n")...)...)
}
helpers.Ensure("system", "prune", "-f", "--all", "--volumes")
helpers.Anyhow("namespace", "remove", data.Labels().Get("_deletenamespace"))
}
},
}
// Gomodjail returns whether the binary is packed with gomodjail.
// https://github.com/AkihiroSuda/gomodjail
var Gomodjail = &test.Requirement{
Check: func(_ test.Data, helpers test.Helpers) (ret bool, mess string) {
// FIXME: do not rely on the filename
ret = strings.HasSuffix(getTarget(), ".gomodjail")
if ret {
mess = "current target is packed with gomodjail"
} else {
mess = "current target is not packed with gomodjail"
}
return ret, mess
},
}
var AllowModifyUserns = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
if testutil.GetAllowModifyUsers() {
return true, "allow modify userns is enabled"
}
return false, "allow modify userns is disabled"
},
}
var RemapIDs = &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
// Create a cobra command for ProcessRootCmdFlags to get globalOptions
ctx := context.Background()
snapshotterName := defaults.DefaultSnapshotter
namespace := defaultNamespace
address := defaults.DefaultAddress
client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address)
if err != nil {
return false, fmt.Sprintf("failed to create client: %v", err)
}
defer cancel()
caps, err := client.GetSnapshotterCapabilities(ctx, snapshotterName)
if err != nil {
return false, fmt.Sprintf("failed to get snapshotter capabilities: %v", err)
}
for _, cap := range caps {
if cap == "remap-ids" {
return true, "snapshotter supports ID remapping"
}
}
return false, "snapshotter does not support ID remapping"
},
}
// SociVersion returns a requirement that checks if the installed SOCI version
// meets the minimum required version
func SociVersion(minVersion string) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
// Use the common CheckSociVersion function from snapshotterutil
err := snapshotterutil.CheckSociVersion(minVersion)
if err != nil {
return false, err.Error()
}
return true, fmt.Sprintf("soci version meets minimum requirement %s", minVersion)
},
}
}
func ContainerdVersion(v string) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
ctx := context.Background()
namespace := defaultNamespace
address := defaults.DefaultAddress
client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address)
if err != nil {
return false, fmt.Sprintf("failed to create client: %v", err)
}
defer cancel()
if sv, err := infoutil.ServerSemVer(ctx, client); err != nil {
return false, err.Error()
} else if sv.LessThan(semver.MustParse(v)) {
return false, fmt.Sprintf("`nerdctl commit --compression expects containerd %s or later, got containerd %v", v, sv)
}
return true, ""
},
}
}
// CNIFirewallVersion checks if the CNI firewall plugin version is greater than or equal to the specified version
func CNIFirewallVersion(requiredVersion string) *test.Requirement {
return &test.Requirement{
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
cniPath := ncdefaults.CNIPath()
firewallPath := filepath.Join(cniPath, "firewall")
ok, err := netutil.FirewallPluginGEQVersion(firewallPath, requiredVersion)
if err != nil {
return false, fmt.Sprintf("Failed to check CNI firewall version: %v", err)
}
if !ok {
return false, fmt.Sprintf("CNI firewall plugin version is less than required version %s", requiredVersion)
}
return true, fmt.Sprintf("CNI firewall plugin version is greater than or equal to required version %s", requiredVersion)
},
}
}