From b2eada4135d5f843a665b0d863379e93a82a61d3 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 2 Oct 2024 10:53:47 -0700 Subject: [PATCH] Test rework, part 4 Signed-off-by: apostasie --- .../builder/builder_build_linux_test.go | 88 +-- cmd/nerdctl/builder/builder_build_test.go | 213 +++++-- cmd/nerdctl/builder/builder_linux_test.go | 221 +++---- .../completion/completion_linux_test.go | 16 +- .../container/container_attach_linux_test.go | 157 ++--- .../container/container_commit_test.go | 79 ++- cmd/nerdctl/container/container_logs_test.go | 2 + .../container/container_run_linux_test.go | 135 ++-- .../container/container_start_linux_test.go | 71 ++- cmd/nerdctl/helpers/testing.go | 105 ++++ cmd/nerdctl/helpers/testing_linux.go | 101 --- cmd/nerdctl/image/image_convert_linux_test.go | 152 +++-- cmd/nerdctl/image/image_convert_test.go | 70 --- cmd/nerdctl/image/image_encrypt_linux_test.go | 83 ++- cmd/nerdctl/image/image_history_test.go | 189 +++--- cmd/nerdctl/image/image_list_test.go | 397 ++++++++---- cmd/nerdctl/image/image_load_linux_test.go | 70 --- cmd/nerdctl/image/image_load_test.go | 81 +++ cmd/nerdctl/image/image_prune_test.go | 295 ++++++--- cmd/nerdctl/image/image_pull_linux_test.go | 315 ++++++---- cmd/nerdctl/image/image_push_linux_test.go | 384 +++++++----- cmd/nerdctl/image/image_remove_linux_test.go | 107 ---- cmd/nerdctl/image/image_remove_test.go | 304 +++++++++ cmd/nerdctl/image/image_save_linux_test.go | 50 -- cmd/nerdctl/image/image_save_test.go | 141 +++-- cmd/nerdctl/ipfs/ipfs_build_linux_test.go | 67 -- cmd/nerdctl/ipfs/ipfs_compose_linux_test.go | 293 ++++++--- cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go | 107 ++++ cmd/nerdctl/ipfs/ipfs_linux_test.go | 148 ----- cmd/nerdctl/ipfs/ipfs_registry_linux_test.go | 153 ++++- cmd/nerdctl/ipfs/ipfs_simple_linux_test.go | 240 +++++++ cmd/nerdctl/{ => issues}/main_linux_test.go | 20 +- cmd/nerdctl/login/login_linux_test.go | 437 ++++++++++--- cmd/nerdctl/main_test.go | 86 ++- cmd/nerdctl/main_test_test.go | 3 +- .../network/network_create_linux_test.go | 6 +- cmd/nerdctl/network/network_inspect_test.go | 22 +- .../network/network_list_linux_test.go | 109 ++-- .../network/network_prune_linux_test.go | 12 +- .../network/network_remove_linux_test.go | 191 +++--- cmd/nerdctl/system/system_info_test.go | 4 +- cmd/nerdctl/system/system_prune_linux_test.go | 24 +- cmd/nerdctl/volume/volume_create_test.go | 13 +- cmd/nerdctl/volume/volume_inspect_test.go | 292 ++++----- cmd/nerdctl/volume/volume_list_test.go | 585 +++++++++--------- cmd/nerdctl/volume/volume_namespace_test.go | 126 ++-- cmd/nerdctl/volume/volume_prune_linux_test.go | 35 +- .../volume/volume_remove_linux_test.go | 80 +-- docs/testing/tools.md | 96 ++- go.mod | 1 + go.sum | 2 + pkg/testutil/nerdtest/ca/ca.go | 162 +++++ pkg/testutil/nerdtest/command.go | 118 ++++ pkg/testutil/nerdtest/helpers.go | 125 ++++ pkg/testutil/nerdtest/hoststoml/hoststoml.go | 67 ++ pkg/testutil/nerdtest/registry/cesanta.go | 246 ++++++++ pkg/testutil/nerdtest/registry/common.go | 108 ++++ pkg/testutil/nerdtest/registry/docker.go | 162 +++++ pkg/testutil/nerdtest/registry/kubo.go | 96 +++ pkg/testutil/nerdtest/requirements.go | 286 +++++++++ pkg/testutil/nerdtest/test.go | 289 +++------ pkg/testutil/nerdtest/third-party.go | 74 +++ pkg/testutil/test/case.go | 177 +++--- pkg/testutil/test/command.go | 135 ++-- pkg/testutil/test/data.go | 57 +- pkg/testutil/test/expected.go | 39 +- pkg/testutil/test/helpers.go | 35 +- pkg/testutil/test/requirement.go | 145 +++-- pkg/testutil/test/test.go | 50 +- pkg/testutil/test/utilities.go | 42 ++ pkg/testutil/testregistry/certsd_linux.go | 28 +- .../testregistry/testregistry_linux.go | 6 +- pkg/testutil/testutil.go | 6 + pkg/testutil/testutil_linux.go | 2 +- 74 files changed, 6210 insertions(+), 3223 deletions(-) delete mode 100644 cmd/nerdctl/image/image_convert_test.go delete mode 100644 cmd/nerdctl/image/image_load_linux_test.go create mode 100644 cmd/nerdctl/image/image_load_test.go delete mode 100644 cmd/nerdctl/image/image_remove_linux_test.go create mode 100644 cmd/nerdctl/image/image_remove_test.go delete mode 100644 cmd/nerdctl/image/image_save_linux_test.go delete mode 100644 cmd/nerdctl/ipfs/ipfs_build_linux_test.go create mode 100644 cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go delete mode 100644 cmd/nerdctl/ipfs/ipfs_linux_test.go create mode 100644 cmd/nerdctl/ipfs/ipfs_simple_linux_test.go rename cmd/nerdctl/{ => issues}/main_linux_test.go (85%) create mode 100644 pkg/testutil/nerdtest/ca/ca.go create mode 100644 pkg/testutil/nerdtest/command.go create mode 100644 pkg/testutil/nerdtest/helpers.go create mode 100644 pkg/testutil/nerdtest/hoststoml/hoststoml.go create mode 100644 pkg/testutil/nerdtest/registry/cesanta.go create mode 100644 pkg/testutil/nerdtest/registry/common.go create mode 100644 pkg/testutil/nerdtest/registry/docker.go create mode 100644 pkg/testutil/nerdtest/registry/kubo.go create mode 100644 pkg/testutil/nerdtest/requirements.go create mode 100644 pkg/testutil/nerdtest/third-party.go create mode 100644 pkg/testutil/test/utilities.go diff --git a/cmd/nerdctl/builder/builder_build_linux_test.go b/cmd/nerdctl/builder/builder_build_linux_test.go index 0f80066b0a2..eaded7ec03f 100644 --- a/cmd/nerdctl/builder/builder_build_linux_test.go +++ b/cmd/nerdctl/builder/builder_build_linux_test.go @@ -18,18 +18,22 @@ package builder import ( "fmt" + "strings" "testing" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestBuildContextWithOCILayout(t *testing.T) { + nerdtest.Setup() + testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) - var dockerBuilderArgs []string if testutil.IsDocker() { // Default docker driver does not support OCI exporter. @@ -38,48 +42,50 @@ func TestBuildContextWithOCILayout(t *testing.T) { dockerBuilderArgs = []string{"buildx", "--builder", builderName} } - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - ociLayout := "parent" - parentImageName := fmt.Sprintf("%s-%s", imageName, ociLayout) - - teardown := func() { - base.Cmd("rmi", parentImageName, imageName).Run() - } - t.Cleanup(teardown) - teardown() - - dockerfile := fmt.Sprintf(`FROM %s + testCase := &test.Case{ + Description: "Build context OCI layout", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", fmt.Sprintf("%s-parent", data.Identifier())) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s LABEL layer=oci-layout-parent CMD ["echo", "test-nerdctl-build-context-oci-layout-parent"]`, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, ociLayout) - - // Create OCI archive from parent image. - base.Cmd("build", buildCtx, "--tag", parentImageName).AssertOK() - base.Cmd("image", "save", "--output", tarPath, parentImageName).AssertOK() - // Unpack OCI archive into OCI layout directory. - ociLayoutDir := t.TempDir() - err := helpers.ExtractTarFile(ociLayoutDir, tarPath) - assert.NilError(t, err) - - dockerfile = fmt.Sprintf(`FROM %s -CMD ["echo", "test-nerdctl-build-context-oci-layout"]`, ociLayout) - buildCtx = helpers.CreateBuildContext(t, dockerfile) - - var buildArgs = []string{} - if testutil.IsDocker() { - buildArgs = dockerBuilderArgs - } - - buildArgs = append(buildArgs, "build", buildCtx, fmt.Sprintf("--build-context=%s=oci-layout://%s", ociLayout, ociLayoutDir), "--tag", imageName) - if testutil.IsDocker() { - // Need to load the container image from the builder to be able to run it. - buildArgs = append(buildArgs, "--load") + // FIXME: replace with a generic file creation helper - search for all occurrences of temp file creation + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + tarPath := fmt.Sprintf("%s/parent.tar", buildCtx) + + helpers.Ensure("build", buildCtx, "--tag", fmt.Sprintf("%s-parent", data.Identifier())) + helpers.Ensure("image", "save", "--output", tarPath, fmt.Sprintf("%s-parent", data.Identifier())) + helpers.CustomCommand("tar", "Cxf", data.TempDir(), tarPath).Run(&test.Expected{}) + }, + + Command: func(data test.Data, helpers test.Helpers) test.Command { + dockerfile := `FROM parent +CMD ["echo", "test-nerdctl-build-context-oci-layout"]` + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + var cmd test.Command + if testutil.IsDocker() { + cmd = helpers.Command(dockerBuilderArgs...) + } else { + cmd = helpers.Command() + } + cmd.WithArgs("build", buildCtx, fmt.Sprintf("--build-context=parent=oci-layout://%s", data.TempDir()), "--tag", data.Identifier()) + if testutil.IsDocker() { + // Need to load the container image from the builder to be able to run it. + cmd.WithArgs("--load") + } + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("run", "--rm", data.Identifier()), "test-nerdctl-build-context-oci-layout"), info) + }, + } + }, } - base.Cmd(buildArgs...).AssertOK() - base.Cmd("run", "--rm", imageName).AssertOutContains("test-nerdctl-build-context-oci-layout") + testCase.Run(t) } diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index 92a0928ecae..ccf0e787d32 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -17,9 +17,11 @@ package builder import ( + "encoding/json" "fmt" "os" "path/filepath" + "runtime" "strings" "testing" @@ -27,61 +29,148 @@ import ( "github.com/containerd/platforms" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestBuild(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName).AssertOK() - base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") - - ignoredImageNamed := imageName + "-" + "ignored" - outputOpt := fmt.Sprintf("--output=type=docker,name=%s", ignoredImageNamed) - base.Cmd("build", buildCtx, "-t", imageName, outputOpt).AssertOK() +func TestBuildBAB(t *testing.T) { + nerdtest.Setup() + + testGroup := &test.Group{ + { + Description: "TestBuild", + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + data.Set("buildCtx", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + helpers.Anyhow("builder", "prune", "--all", "--force") + }, + Expected: test.Expects(0, nil, nil), + SubTests: []*test.Case{ + { + Description: "Successfully build with tag first buildctx second", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("build", "-t", data.Identifier(), data.Get("buildCtx")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + { + Description: "Successfully build with buildctx first tag second", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + { + Description: "Successfully build with output docker, main tag still works", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + { + Description: "Successfully build with output docker, name cannot be used", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Identifier("ignored")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(1, nil, nil), + }, + }, + }, + } - base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") - base.Cmd("run", "--rm", ignoredImageNamed).AssertFail() + testGroup.Run(t) } -func TestBuildIsShareableForCompatiblePlatform(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", buildCtx, "-t", imageName).AssertErrNotContains("tarball") - - d := platforms.DefaultSpec() - platformConfig := fmt.Sprintf("%s/%s", d.OS, d.Architecture) - base.Cmd("build", buildCtx, "-t", imageName, "--platform", platformConfig).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName, "--platform", platformConfig, "--progress", "plain").AssertErrNotContains("tarball") +func TestCanBuildOnOtherPlatform(t *testing.T) { + nerdtest.Setup() + + requireEmulation := test.MakeRequirement(func(data test.Data, helpers test.Helpers, t *testing.T) (bool, string) { + host, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(t, err) + var plt []struct { + Platforms []platforms.Platform + } + cmd := &test.GenericCommand{} + cmd.WithT(t) + cmd.WithBinary("buildctl") + cmd.WithArgs("--addr", host, "debug", "workers", "--format", "json") + cmd.Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + err = json.Unmarshal([]byte(stdout), &plt) + assert.NilError(t, err, info) + }, + }) - n := platforms.Platform{OS: "linux", Architecture: "arm", Variant: ""} - if n.OS != d.OS && n.Architecture != d.Architecture { - notCompatiblePlatformConfig := fmt.Sprintf("%s/%s", n.OS, n.Architecture) - base.Cmd("build", buildCtx, "-t", imageName, "--platform", notCompatiblePlatformConfig).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName, "--platform", notCompatiblePlatformConfig, "--progress", "plain").AssertErrContains("tarball") + assert.Assert(t, len(plt) > 0) + var found *platforms.Platform + for _, plat := range plt[0].Platforms { + if plat.Architecture != runtime.GOARCH && plat.OS != runtime.GOOS { + found = &plat + break + } + } + + mess := "buildkit worker does not support emulation" + ret := found != nil + if ret { + mess = "buildkit worker does support emulation" + data.Set("OS", found.OS) + data.Set("Arch", found.Architecture) + } + + return ret, mess + }) + + testCase := &test.Case{ + Description: "Successfully build on emulated platforms", + Require: test.Require( + nerdtest.Build, + requireEmulation, + ), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx"), "--platform", fmt.Sprintf("%s/%s", data.Get("OS"), data.Get("Architecture")), "-t", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), } + + testCase.Run(t) } // TestBuildBaseImage tests if an image can be built on the previously built image. @@ -100,7 +189,7 @@ RUN echo hello > /hello CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() base.Cmd("build", buildCtx, "-t", imageName).AssertOK() @@ -110,7 +199,7 @@ RUN echo hello2 > /hello2 CMD ["cat", "/hello2"] `, imageName) - buildCtx2 := helpers.CreateBuildContext(t, dockerfile2) + buildCtx2 := testhelpers.CreateBuildContext(t, dockerfile2) base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK() base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK() @@ -143,7 +232,7 @@ RUN echo hello2 > /hello2 CMD ["cat", "/hello2"] `, imageName) - buildCtx2 := helpers.CreateBuildContext(t, dockerfile2) + buildCtx2 := testhelpers.CreateBuildContext(t, dockerfile2) base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK() base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK() @@ -209,7 +298,7 @@ func TestBuildLocal(t *testing.T) { COPY %s /`, testFileName) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) if err := os.WriteFile(filepath.Join(buildCtx, testFileName), []byte(testContent), 0644); err != nil { t.Fatal(err) @@ -248,7 +337,7 @@ ENV TEST_STRING=$TEST_STRING CMD echo $TEST_STRING `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", buildCtx, "-t", imageName).AssertOK() base.Cmd("run", "--rm", imageName).AssertOutExactly("1\n") @@ -291,7 +380,7 @@ func TestBuildWithIIDFile(t *testing.T) { CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) fileName := filepath.Join(t.TempDir(), "id.txt") base.Cmd("build", "-t", imageName, buildCtx, "--iidfile", fileName).AssertOK() @@ -314,7 +403,7 @@ func TestBuildWithLabels(t *testing.T) { LABEL name=nerdctl-build-test-label `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx, "--label", "label=test").AssertOK() defer base.Cmd("rmi", imageName).Run() @@ -337,7 +426,7 @@ func TestBuildMultipleTags(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "%s"] `, testutil.CommonImage, output) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", img, buildCtx).AssertOK() base.Cmd("build", buildCtx, "-t", img, "-t", imgWithNoTag, "-t", imgWithCustomTag).AssertOK() @@ -390,7 +479,7 @@ CMD ["echo", "dockerfile"] err = os.WriteFile(filepath.Join(tmpDir, "Containerfile"), []byte(containerfile), 0644) assert.NilError(t, err) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", "-t", imageName, buildCtx).AssertOK() base.Cmd("run", "--rm", imageName).AssertOutExactly("dockerfile\n") @@ -405,7 +494,7 @@ func TestBuildNoTag(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", buildCtx).AssertOK() base.Cmd("images").AssertOutContains("") @@ -420,7 +509,7 @@ func TestBuildContextDockerImageAlias(t *testing.T) { dockerfile := `FROM myorg/myapp CMD ["echo", "nerdctl-build-myorg/myapp"]` - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", buildCtx, fmt.Sprintf("--build-context=myorg/myapp=docker-image://%s", testutil.CommonImage)).AssertOK() base.Cmd("images").AssertOutContains("") @@ -445,7 +534,7 @@ func TestBuildContextWithCopyFromDir(t *testing.T) { COPY --from=dir2 /%s /hello_from_dir2.txt RUN ["cat", "/hello_from_dir2.txt"]`, testutil.CommonImage, filename) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) base.Cmd("build", buildCtx, fmt.Sprintf("--build-context=dir2=%s", dir2)).AssertOK() base.Cmd("images").AssertOutContains("") @@ -466,7 +555,7 @@ RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch CMD ["cat", "/source-date-epoch"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) const sourceDateEpochEnvStr = "1111111111" base.Env = append(base.Env, "SOURCE_DATE_EPOCH="+sourceDateEpochEnvStr) @@ -487,7 +576,7 @@ func TestBuildNetwork(t *testing.T) { RUN apk add --no-cache curl RUN curl -I http://google.com `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) validCases := []struct { name string @@ -543,7 +632,7 @@ func TestBuildAttestation(t *testing.T) { } dockerfile := "FROM " + testutil.NginxAlpineImage - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) // Test sbom outputSBOMDir := t.TempDir() diff --git a/cmd/nerdctl/builder/builder_linux_test.go b/cmd/nerdctl/builder/builder_linux_test.go index 862320142f9..2e0b47ef774 100644 --- a/cmd/nerdctl/builder/builder_linux_test.go +++ b/cmd/nerdctl/builder/builder_linux_test.go @@ -18,136 +18,141 @@ package builder import ( "bytes" + "errors" "fmt" "os" "os/exec" - "path/filepath" "testing" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestBuilderPrune(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) +func TestBuilder(t *testing.T) { + nerdtest.Setup() - base := testutil.NewBase(t) + // FIXME: this is a dirty hack to pass a function from Setup to Cleanup, which is not currently possible + var bkGC func() - dockerfile := fmt.Sprintf(`FROM %s + testCase := &test.Case{ + Require: nerdtest.Build, + SubTests: []*test.Case{ + { + Description: "PruneForce", + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - testCases := []struct { - name string - commandArgs []string - }{ - { - name: "TestBuilderPruneForce", - commandArgs: []string{"builder", "prune", "--force"}, - }, - { - name: "TestBuilderPruneForceAll", - commandArgs: []string{"builder", "prune", "--force", "--all"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - base.Cmd("build", buildCtx).AssertOK() - base.Cmd(tc.commandArgs...).AssertOK() - }) - } -} - -func TestBuilderDebug(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-builder-debug-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("builder", "debug", buildCtx).CmdOption(testutil.WithStdin(bytes.NewReader([]byte("c\n")))).AssertOK() -} - -func TestBuildWithPull(t *testing.T) { - testutil.DockerIncompatible(t) - if rootlessutil.IsRootless() { - t.Skipf("skipped because the test needs a custom buildkitd config") - } - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - oldImage := testutil.BusyboxImage - oldImageSha := "141c253bc4c3fd0a201d32dc1f493bcf3fff003b6df416dea4f41046e0f37d47" - newImage := testutil.AlpineImage - - buildkitConfig := fmt.Sprintf(`[worker.oci] + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + }, + Command: test.RunCommand("builder", "prune", "--force"), + Expected: test.Expects(0, nil, nil), + }, + { + Description: "PruneForceAll", + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + }, + Command: test.RunCommand("builder", "prune", "--force", "--all"), + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Debug", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + cmd := helpers.Command("builder", "debug", buildCtx) + cmd.WithStdin(bytes.NewReader([]byte("c\n"))) + return cmd + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "WithPull", + Require: nerdtest.RootFul, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + if bkGC != nil { + bkGC() + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + buildkitConfig := fmt.Sprintf(`[worker.oci] enabled = false [worker.containerd] enabled = true namespace = "%s"`, testutil.Namespace) - cleanup := useBuildkitConfig(t, buildkitConfig) - defer cleanup() - - testCases := []struct { - name string - pull string - }{ - { - name: "build with local image", - pull: "false", - }, - { - name: "build with newest image", - pull: "true", - }, - { - name: "build with buildkit default", - // buildkit default pulls from remote - pull: "default", + bkGC = useBuildkitConfig(t, buildkitConfig) + oldImage := testutil.BusyboxImage + oldImageSha := "141c253bc4c3fd0a201d32dc1f493bcf3fff003b6df416dea4f41046e0f37d47" + newImage := testutil.AlpineImage + + helpers.Ensure("pull", oldImage) + helpers.Ensure("tag", oldImage, newImage) + + dockerfile := fmt.Sprintf(`FROM %s`, newImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + + data.Set("buildCtx", buildCtx) + data.Set("oldImageSha", oldImageSha) + }, + SubTests: []*test.Case{ + { + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx"), "--pull=false") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New(data.Get("oldImageSha"))}, + } + }, + }, + { + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx"), "--pull=true") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + { + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + }, + }, }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Cmd("image", "prune", "--force", "--all").AssertOK() - - base.Cmd("pull", oldImage).Run() - base.Cmd("tag", oldImage, newImage).Run() - - dockerfile := fmt.Sprintf(`FROM %s`, newImage) - tmpDir := t.TempDir() - err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644) - assert.NilError(t, err) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - buildCmd := []string{"build", buildCtx} - switch tc.pull { - case "false": - buildCmd = append(buildCmd, "--pull=false") - base.Cmd(buildCmd...).AssertErrContains(oldImageSha) - case "true": - buildCmd = append(buildCmd, "--pull=true") - base.Cmd(buildCmd...).AssertErrNotContains(oldImageSha) - case "default": - base.Cmd(buildCmd...).AssertErrNotContains(oldImageSha) - } - }) - } + testCase.Run(t) } func useBuildkitConfig(t *testing.T, config string) (cleanup func()) { diff --git a/cmd/nerdctl/completion/completion_linux_test.go b/cmd/nerdctl/completion/completion_linux_test.go index 234c9a21ebc..d35e6d8fc88 100644 --- a/cmd/nerdctl/completion/completion_linux_test.go +++ b/cmd/nerdctl/completion/completion_linux_test.go @@ -153,18 +153,14 @@ func TestCompletion(t *testing.T) { { Description: "no namespace --cgroup-manager", Command: func(data test.Data, helpers test.Helpers) test.Command { - cmd := helpers.Command() - cmd.Clear() - cmd.WithBinary("nerdctl") - cmd.WithArgs("__complete", "--cgroup-manager", "") - return cmd + return helpers.CustomCommand("nerdctl", "__complete", "--cgroup-manager", "") }, Expected: test.Expects(0, nil, test.Contains("cgroupfs\n")), }, { Description: "no namespace empty", Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("__complete", "") + return helpers.CustomCommand("nerdctl", "__complete", "") }, Expected: test.Expects(0, nil, test.Contains("run\t")), }, @@ -172,8 +168,8 @@ func TestCompletion(t *testing.T) { Description: "namespace space empty", Command: func(data test.Data, helpers test.Helpers) test.Command { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} - return helpers.Command().Clear().WithBinary("nerdctl"). - WithArgs("__complete", "--namespace", testutil.Namespace, "") + ns, _ := data.Surface(test.SystemKey(nerdtest.Namespace)) + return helpers.CustomCommand("nerdctl", "__complete", "--namespace", string(ns), "") }, Expected: test.Expects(0, nil, test.Contains("run\t")), }, @@ -196,8 +192,8 @@ func TestCompletion(t *testing.T) { Description: "namespace run -i", Command: func(data test.Data, helpers test.Helpers) test.Command { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} - return helpers.Command().Clear().WithBinary("nerdctl"). - WithArgs("__complete", "--namespace", testutil.Namespace, "run", "-i", "") + ns, _ := data.Surface(test.SystemKey(nerdtest.Namespace)) + return helpers.CustomCommand("nerdctl", "__complete", "--namespace", string(ns), "run", "-i", "") }, Expected: test.Expects(0, nil, test.Contains(testutil.AlpineImage+"\n")), }, diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index 71a74eae59e..325ba7a13d3 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -24,81 +24,98 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -// skipAttachForDocker should be called by attach-related tests that assert 'read detach keys' in stdout. -func skipAttachForDocker(t *testing.T) { - t.Helper() - if testutil.GetTarget() == testutil.Docker { - t.Skip("When detaching from a container, for a session started with 'docker attach'" + - ", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." + - " However, the flag is called '--detach-keys' in all cases" + - ", so nerdctl prints 'read detach keys' for all cases" + - ", and that's why this test is skipped for Docker.") - } -} - -// prepareContainerToAttach spins up a container (entrypoint = shell) with `-it` and detaches from it -// so that it can be re-attached to later. -func prepareContainerToAttach(base *testutil.Base, containerName string) { - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader( +func TestAttachDetachKeys(t *testing.T) { + nerdtest.Setup() + + setup := func(data test.Data, helpers test.Helpers) { + // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. + // unbuffer(1) can be installed with `apt-get install expect`. + // + // "-p" is needed because we need unbuffer to read from stdin, and from [1]: + // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. + // To use unbuffer in a pipeline, use the -p flag." + // + // [1] https://linux.die.net/man/1/unbuffer + + si := testutil.NewDelayOnceReader(bytes.NewReader( []byte{16, 17}, // ctrl+p,ctrl+q, see https://www.physics.udel.edu/~watson/scen103/ascii.html - ))), + )) + + helpers. + Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage). + WithWrapper("unbuffer", "-p"). + WithStdin(si). + Run(&test.Expected{ + Output: test.All( + // NOTE: + // When detaching from a container, for a session started with 'docker attach', + // it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing. + // However, the flag is called '--detach-keys' in all cases, and nerdctl does print read detach keys + // in all cases. + // Disabling the contains test here allow both cli to run the test. + // test.Contains("read detach keys"), + func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }), + }) } - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - // - // "-p" is needed because we need unbuffer to read from stdin, and from [1]: - // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. - // To use unbuffer in a pipeline, use the -p flag." - // - // [1] https://linux.die.net/man/1/unbuffer - base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage). - CmdOption(opts...).AssertOutContains("read detach keys") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) -} - -func TestAttach(t *testing.T) { - t.Parallel() - - skipAttachForDocker(t) - - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - prepareContainerToAttach(base, containerName) - - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("expr 1 + 1\nexit\n"))), + testGroup := &test.Group{ + { + Description: "TestAttachDefaultKeys", + Require: test.Binary("unbuffer"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: setup, + Command: func(data test.Data, helpers test.Helpers) test.Command { + si := testutil.NewDelayOnceReader(strings.NewReader("expr 1 + 1\nexit\n")) + // `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code, + // so the exit code cannot be easily tested here. + return helpers. + Command("attach", data.Identifier()). + WithStdin(si). + WithWrapper("unbuffer", "-p") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, false, info) + }, + } + }, + }, + { + Description: "TestAttachCustomKeys", + Require: test.Binary("unbuffer"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: setup, + Command: func(data test.Data, helpers test.Helpers) test.Command { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("attach", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, + }, } - // `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code, - // so the exit code cannot be easily tested here. - base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", containerName).CmdOption(opts...).AssertOutContains("2") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, false) -} - -func TestAttachDetachKeys(t *testing.T) { - t.Parallel() - skipAttachForDocker(t) - - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - prepareContainerToAttach(base, containerName) - - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader( - []byte{1, 2}, // https://www.physics.udel.edu/~watson/scen103/ascii.html - ))), - } - base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", "--detach-keys=ctrl-a,ctrl-b", containerName). - CmdOption(opts...).AssertOutContains("read detach keys") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testGroup.Run(t) } diff --git a/cmd/nerdctl/container/container_commit_test.go b/cmd/nerdctl/container/container_commit_test.go index f9f553d9ca1..39e9eb72143 100644 --- a/cmd/nerdctl/container/container_commit_test.go +++ b/cmd/nerdctl/container/container_commit_test.go @@ -21,35 +21,60 @@ import ( "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestCommit(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") - } - testContainer := testutil.Identifier(t) - testImage := testutil.Identifier(t) + "-img" - defer base.Cmd("rm", "-f", testContainer).Run() - defer base.Cmd("rmi", testImage).Run() - - for _, pause := range []string{ - "true", - "false", - } { - base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity").AssertOK() - base.EnsureContainerStarted(testContainer) - base.Cmd("exec", testContainer, "sh", "-euxc", `echo hello-test-commit > /foo`).AssertOK() - base.Cmd( - "commit", - "-c", `CMD ["/foo"]`, - "-c", `ENTRYPOINT ["cat"]`, - fmt.Sprintf("--pause=%s", pause), - testContainer, testImage).AssertOK() - base.Cmd("run", "--rm", testImage).AssertOutExactly("hello-test-commit\n") - base.Cmd("rm", "-f", testContainer).Run() - base.Cmd("rmi", testImage).Run() + nerdtest.Setup() + + testGroup := &test.Group{ + { + Description: "with pause", + Require: nerdtest.CGroup, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("exec", data.Identifier(), "sh", "-euxc", `echo hello-test-commit > /foo`) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + helpers.Ensure( + "commit", + "-c", `CMD ["/foo"]`, + "-c", `ENTRYPOINT ["cat"]`, + fmt.Sprintf("--pause=true"), + data.Identifier(), data.Identifier()) + return helpers.Command("run", "--rm", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("hello-test-commit\n")), + }, + { + Description: "no pause", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + helpers.Ensure("exec", data.Identifier(), "sh", "-euxc", `echo hello-test-commit > /foo`) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + helpers.Ensure( + "commit", + "-c", `CMD ["/foo"]`, + "-c", `ENTRYPOINT ["cat"]`, + fmt.Sprintf("--pause=false"), + data.Identifier(), data.Identifier()) + return helpers.Command("run", "--rm", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("hello-test-commit\n")), + }, } + + testGroup.Run(t) } diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index ec9fbd2ce70..5f9e3d30aa6 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -95,6 +95,8 @@ func TestLogsWithInheritedFlags(t *testing.T) { base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar").AssertOK() + // NOTE: seen with Docker: there are circumstances where this happens too fast and we get foo + time.Sleep(1 * time.Second) // test rootCmd alias `-n` already used in logs subcommand base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error { if !(stdout == "bar\n" || stdout == "") { diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index aca549d9446..ac271259e42 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -36,9 +36,10 @@ import ( "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestRunCustomRootfs(t *testing.T) { @@ -458,64 +459,96 @@ func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) { } func TestRunWithOOMScoreAdj(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("test skipped for rootless containers.") + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestStartDetachKeys", + Require: nerdtest.RootFul, + Command: test.RunCommand("run", "--rm", "--oom-score-adj", "-42", testutil.AlpineImage, "cat", "/proc/self/oom_score_adj"), + Expected: test.Expects(0, nil, test.Contains("-42")), } - t.Parallel() - base := testutil.NewBase(t) - var score = "-42" - base.Cmd("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj").AssertOutContains(score) + testCase.Run(t) } -func TestRunWithDetachKeys(t *testing.T) { - t.Parallel() +func TestRunDetachKeys(t *testing.T) { + nerdtest.Setup() - if testutil.GetTarget() == testutil.Docker { - t.Skip("When detaching from a container, for a session started with 'docker attach'" + - ", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." + - " However, the flag is called '--detach-keys' in all cases" + - ", so nerdctl prints 'read detach keys' for all cases" + - ", and that's why this test is skipped for Docker.") + testCase := &test.Case{ + Description: "TestStartDetachKeys", + Require: test.Binary("unbuffer"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))), // https://www.physics.udel.edu/~watson/scen103/ascii.html - } - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - // - // "-p" is needed because we need unbuffer to read from stdin, and from [1]: - // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. - // To use unbuffer in a pipeline, use the -p flag." - // - // [1] https://linux.die.net/man/1/unbuffer - base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", containerName, testutil.CommonImage). - CmdOption(opts...).AssertOutContains("read detach keys") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testCase.Run(t) } func TestRunWithTtyAndDetached(t *testing.T) { - base := testutil.NewBase(t) - imageName := testutil.CommonImage - withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) - withTtyContainerName := "with-terminal-" + testutil.Identifier(t) - - // without -t, fail - base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() - base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") - withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) - assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) - - // with -t, success - base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() - base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") - withTtyContainer := base.InspectContainer(withTtyContainerName) - assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) + nerdtest.Setup() + + testGroup := &test.Group{ + { + Description: "without terminal", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "stty") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("logs", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Errors: []error{errors.New("stty: standard input: Not a tty")}, + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.ExitCode, 1, info) + }, + } + }, + }, + { + Description: "with terminal", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "-t", "--name", data.Identifier(), testutil.CommonImage, "stty") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("logs", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains("speed 38400 baud; line = 0;"), + func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.ExitCode, 0, info) + }), + } + }, + }, + } + + testGroup.Run(t) } diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index 4fe6f2d249c..bff02cbd9c0 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -24,41 +24,52 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestStartDetachKeys(t *testing.T) { - t.Parallel() + nerdtest.Setup() - skipAttachForDocker(t) + testCase := &test.Case{ + Description: "TestStartDetachKeys", + Require: test.Binary("unbuffer"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + si := testutil.NewDelayOnceReader(strings.NewReader("exit\n")) - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - opts := []func(*testutil.Cmd){ - // If NewDelayOnceReader is not used, - // the container state will be Created instead of Exited. - // Maybe `unbuffer` exits too early in that case? - testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("exit\n"))), + cmd := helpers. + Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithWrapper("unbuffer", "-p") + cmd.WithStdin(si) + cmd.Run(&test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, false, info) + }), + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("start", "-a", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, } - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - // - // "-p" is needed because we need unbuffer to read from stdin, and from [1]: - // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. - // To use unbuffer in a pipeline, use the -p flag." - // - // [1] https://linux.die.net/man/1/unbuffer - base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage). - CmdOption(opts...).AssertOK() - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, false) - opts = []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))), // https://www.physics.udel.edu/~watson/scen103/ascii.html - } - base.CmdWithHelper([]string{"unbuffer", "-p"}, "start", "-a", "--detach-keys=ctrl-a,ctrl-b", containerName). - CmdOption(opts...).AssertOutContains("read detach keys") - container = base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testCase.Run(t) + } diff --git a/cmd/nerdctl/helpers/testing.go b/cmd/nerdctl/helpers/testing.go index a9c633faa13..c7f460b19a0 100644 --- a/cmd/nerdctl/helpers/testing.go +++ b/cmd/nerdctl/helpers/testing.go @@ -17,11 +17,22 @@ package helpers import ( + "context" + "encoding/json" + "errors" + "fmt" "os" + "os/exec" "path/filepath" "testing" "gotest.tools/v3/assert" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" ) func CreateBuildContext(t *testing.T, dockerfile string) string { @@ -30,3 +41,97 @@ func CreateBuildContext(t *testing.T, dockerfile string) string { assert.NilError(t, err) return tmpDir } + +func RmiAll(base *testutil.Base) { + base.T.Logf("Pruning images") + imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + // remove empty output line at the end + imageIDs = imageIDs[:len(imageIDs)-1] + // use `Run` on purpose (same below) because `rmi all` may fail on individual + // image id that has an expected running container (e.g. a registry) + base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() + + base.T.Logf("Pruning build caches") + if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { + base.Cmd("builder", "prune", "--force").AssertOK() + } + + // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs + // https://github.com/containerd/nerdctl/pull/1833 + if base.Target == testutil.Nerdctl { + base.T.Logf("Pruning all content blobs") + addr := base.ContainerdAddress() + client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) + assert.NilError(base.T, err) + cs := client.ContentStore() + ctx := context.TODO() + wf := func(info content.Info) error { + base.T.Logf("Pruning blob %+v", info) + if err := cs.Delete(ctx, info.Digest); err != nil { + base.T.Log(err) + } + return nil + } + if err := cs.Walk(ctx, wf); err != nil { + base.T.Log(err) + } + + base.T.Logf("Pruning all images (again?)") + imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + base.T.Logf("pruning following images: %+v", imageIDs) + base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() + } +} + +func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { + if err := os.MkdirAll(rootfsPath, 0755); err != nil { + return err + } + workDir, err := os.MkdirTemp("", "extract-docker-archive") + if err != nil { + return err + } + defer os.RemoveAll(workDir) + if err := ExtractTarFile(workDir, archiveTarPath); err != nil { + return err + } + manifestJSONPath := filepath.Join(workDir, "manifest.json") + manifestJSONBytes, err := os.ReadFile(manifestJSONPath) + if err != nil { + return err + } + var mani DockerArchiveManifestJSON + if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { + return err + } + if len(mani) > 1 { + return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani)) + } + if len(mani) < 1 { + return errors.New("invalid archive") + } + ent := mani[0] + for _, l := range ent.Layers { + layerTarPath := filepath.Join(workDir, l) + if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil { + return err + } + } + return nil +} + +type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry + +type DockerArchiveManifestJSONEntry struct { + Config string + RepoTags []string + Layers []string +} + +func ExtractTarFile(dirPath, tarFilePath string) error { + cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + } + return nil +} diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go index ec638021b11..c50e16ae06c 100644 --- a/cmd/nerdctl/helpers/testing_linux.go +++ b/cmd/nerdctl/helpers/testing_linux.go @@ -17,9 +17,6 @@ package helpers import ( - "context" - "encoding/json" - "errors" "fmt" "io" "net" @@ -32,10 +29,6 @@ import ( "gotest.tools/v3/assert" - containerd "github.com/containerd/containerd/v2/client" - "github.com/containerd/containerd/v2/core/content" - - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) @@ -137,47 +130,6 @@ func NewCosignKeyPair(t testing.TB, path string, password string) *CosignKeyPair } } -func RmiAll(base *testutil.Base) { - base.T.Logf("Pruning images") - imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() - // remove empty output line at the end - imageIDs = imageIDs[:len(imageIDs)-1] - // use `Run` on purpose (same below) because `rmi all` may fail on individual - // image id that has an expected running container (e.g. a registry) - base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() - - base.T.Logf("Pruning build caches") - if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { - base.Cmd("builder", "prune", "--force").AssertOK() - } - - // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs - // https://github.com/containerd/nerdctl/pull/1833 - if base.Target == testutil.Nerdctl { - base.T.Logf("Pruning all content blobs") - addr := base.ContainerdAddress() - client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) - assert.NilError(base.T, err) - cs := client.ContentStore() - ctx := context.TODO() - wf := func(info content.Info) error { - base.T.Logf("Pruning blob %+v", info) - if err := cs.Delete(ctx, info.Digest); err != nil { - base.T.Log(err) - } - return nil - } - if err := cs.Walk(ctx, wf); err != nil { - base.T.Log(err) - } - - base.T.Logf("Pruning all images (again?)") - imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() - base.T.Logf("pruning following images: %+v", imageIDs) - base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() - } -} - func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) { comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -228,56 +180,3 @@ func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail() base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() } - -func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { - if err := os.MkdirAll(rootfsPath, 0755); err != nil { - return err - } - workDir, err := os.MkdirTemp("", "extract-docker-archive") - if err != nil { - return err - } - defer os.RemoveAll(workDir) - if err := ExtractTarFile(workDir, archiveTarPath); err != nil { - return err - } - manifestJSONPath := filepath.Join(workDir, "manifest.json") - manifestJSONBytes, err := os.ReadFile(manifestJSONPath) - if err != nil { - return err - } - var mani DockerArchiveManifestJSON - if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { - return err - } - if len(mani) > 1 { - return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani)) - } - if len(mani) < 1 { - return errors.New("invalid archive") - } - ent := mani[0] - for _, l := range ent.Layers { - layerTarPath := filepath.Join(workDir, l) - if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil { - return err - } - } - return nil -} - -type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry - -type DockerArchiveManifestJSONEntry struct { - Config string - RepoTags []string - Layers []string -} - -func ExtractTarFile(dirPath, tarFilePath string) error { - cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) - } - return nil -} diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index ae90cca5af9..8110ca071b4 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -20,63 +20,125 @@ import ( "fmt" "testing" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestImageConvertNydus(t *testing.T) { - testutil.RequireExecutable(t, "nydus-image") - testutil.DockerIncompatible(t) +func TestImageConvert(t *testing.T) { + nerdtest.Setup() - base := testutil.NewBase(t) - t.Parallel() + testCase := &test.Case{ + Description: "Test image conversion", + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "esgz", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--estargz", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "nydus", + Require: test.Require( + test.Binary("nydus-image"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--nydus", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstd", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstdchunked", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + } - convertedImage := testutil.Identifier(t) + ":nydus" - base.Cmd("rmi", convertedImage).Run() - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("image", "convert", "--nydus", "--oci", - testutil.CommonImage, convertedImage).AssertOK() - defer base.Cmd("rmi", convertedImage).Run() + testCase.Run(t) - // use `nydusify` check whether the convertd nydus image is valid +} - // skip if rootless - if rootlessutil.IsRootless() { - t.Skip("Nydusify check is not supported rootless mode.") - } +func TestImageConvertNydusVerify(t *testing.T) { + nerdtest.Setup() - // skip if nydusify and nydusd are not installed - testutil.RequireExecutable(t, "nydusify") - testutil.RequireExecutable(t, "nydusd") + const remoteImageKey = "remoteImageKey" - // setup local docker registry - registry := testregistry.NewWithNoAuth(base, 0, false) - remoteImage := fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port) - t.Cleanup(func() { - base.Cmd("rmi", remoteImage).Run() - registry.Cleanup(nil) - }) + var registry *testregistry.RegistryServer - base.Cmd("tag", convertedImage, remoteImage).AssertOK() - base.Cmd("push", remoteImage).AssertOK() - nydusifyCmd := testutil.Cmd{ - Cmd: icmd.Command( - "nydusify", - "check", - "--source", - testutil.CommonImage, - "--target", - remoteImage, - "--source-insecure", - "--target-insecure", + testCase := &test.Case{ + Description: "TestImageConvertNydusVerify", + Require: test.Require( + test.Linux, + test.Binary("nydus-image"), + test.Binary("nydusify"), + test.Binary("nydusd"), + test.Not(nerdtest.Docker), + nerdtest.RootFul, ), - Base: base, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + data.Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port)) + helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image")) + helpers.Ensure("tag", data.Identifier("converted-image"), data.Get(remoteImageKey)) + helpers.Ensure("push", data.Get(remoteImageKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + if registry != nil { + registry.Cleanup(nil) + helpers.Anyhow("rmi", data.Get(remoteImageKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("nydusify", + "check", + "--source", + testutil.CommonImage, + "--target", + data.Get(remoteImageKey), + "--source-insecure", + "--target-insecure", + ) + }, + Expected: test.Expects(0, nil, nil), } - // nydus is creating temporary files - make sure we are in a proper location for that - nydusifyCmd.Cmd.Dir = base.T.TempDir() - nydusifyCmd.AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_convert_test.go b/cmd/nerdctl/image/image_convert_test.go deleted file mode 100644 index ca5780597d3..00000000000 --- a/cmd/nerdctl/image/image_convert_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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 image - -import ( - "runtime" - "testing" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestImageConvert(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("no windows support yet") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - t.Parallel() - - base.Cmd("pull", testutil.CommonImage).AssertOK() - - testCases := []struct { - identifier string - args []string - }{ - { - "esgz", - []string{"--estargz"}, - }, - { - "zstd", - []string{"--zstd", "--zstd-compression-level", "3"}, - }, - { - "zstdchunked", - []string{"--zstdchunked", "--zstdchunked-compression-level", "3"}, - }, - } - - for _, tc := range testCases { - convertedImage := testutil.Identifier(t) + ":" + tc.identifier - args := append([]string{"image", "convert", "--oci"}, tc.args...) - args = append(args, testutil.CommonImage, convertedImage) - - t.Run(tc.identifier, func(t *testing.T) { - t.Parallel() - - base.Cmd("rmi", convertedImage).Run() - t.Cleanup(func() { - base.Cmd("rmi", convertedImage).Run() - }) - - base.Cmd(args...).AssertOK() - }) - } -} diff --git a/cmd/nerdctl/image/image_encrypt_linux_test.go b/cmd/nerdctl/image/image_encrypt_linux_test.go index 80ff117c007..0ec2ccf43ed 100644 --- a/cmd/nerdctl/image/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image/image_encrypt_linux_test.go @@ -18,39 +18,66 @@ package image import ( "fmt" + "strings" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) func TestImageEncryptJWE(t *testing.T) { - testutil.RequiresBuild(t) - testutil.DockerIncompatible(t) - keyPair := helpers.NewJWEKeyPair(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - - defer keyPair.Cleanup() - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.Port, tID) - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef).AssertOK() - base.Cmd("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef).AssertOutExactly("1\n") - base.Cmd("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") - base.Cmd("push", encryptImageRef).AssertOK() - - defer base.Cmd("rmi", encryptImageRef).Run() - - // remove all local images (in the nerdctl-test namespace), to ensure that we do not have blobs of the original image. - helpers.RmiAll(base) - base.Cmd("pull", encryptImageRef).AssertFail() // defaults to --unpack=true, and fails due to missing prv key - base.Cmd("pull", "--unpack=false", encryptImageRef).AssertOK() - decryptImageRef := tID + ":decrypted" - defer base.Cmd("rmi", decryptImageRef).Run() - base.Cmd("image", "decrypt", "--key="+keyPair.Pub, encryptImageRef, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.Prv, encryptImageRef, decryptImageRef).AssertOK() + nerdtest.Setup() + + var registry *testregistry.RegistryServer + var keyPair *testhelpers.JweKeyPair + + const remoteImageKey = "remoteImageKey" + + testCase := &test.Case{ + Description: "TestImageEncryptJWE", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + // This test needs to rmi the common image + nerdtest.Private, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + keyPair.Cleanup() + helpers.Anyhow("rmi", "-f", data.Get(remoteImageKey)) + } + helpers.Anyhow("rmi", "-f", data.Identifier("decrypted")) + }, + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 0, false) + keyPair = testhelpers.NewJWEKeyPair(t) + helpers.Ensure("pull", testutil.CommonImage) + encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", registry.Port, data.Identifier()) + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef) + inspector := helpers.Capture("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef) + assert.Equal(t, inspector, "1\n") + inspector = helpers.Capture("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef) + assert.Assert(t, strings.Contains(inspector, "org.opencontainers.image.enc.keys.jwe")) + helpers.Ensure("push", encryptImageRef) + helpers.Anyhow("rmi", "-f", encryptImageRef) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + data.Set(remoteImageKey, encryptImageRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + helpers.Fail("pull", data.Get(remoteImageKey)) + helpers.Ensure("pull", "--unpack=false", data.Get(remoteImageKey)) + helpers.Fail("image", "decrypt", "--key="+keyPair.Pub, data.Get(remoteImageKey), data.Identifier("decrypted")) // decryption needs prv key, not pub key + return helpers.Command("image", "decrypt", "--key="+keyPair.Prv, data.Get(remoteImageKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(0, nil, nil), + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index 21bef4f5692..5241e08f66c 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -18,9 +18,8 @@ package image import ( "encoding/json" - "fmt" + "errors" "io" - "runtime" "strings" "testing" "time" @@ -28,6 +27,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) type historyObj struct { @@ -39,53 +40,20 @@ type historyObj struct { Comment string } -func imageHistoryJSONHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) []historyObj { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, "--format", "json") - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - fmt.Println(cmdResult.Stderr()) - - dec := json.NewDecoder(strings.NewReader(cmdResult.Stdout())) +func decode(stdout string) ([]historyObj, error) { + dec := json.NewDecoder(strings.NewReader(stdout)) object := []historyObj{} for { var v historyObj if err := dec.Decode(&v); err == io.EOF { break } else if err != nil { - base.T.Fatal(err) + return nil, errors.New("failed to decode history object") } object = append(object, v) } - return object -} - -func imageHistoryRawHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) string { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - return cmdResult.Stdout() + return object, nil } func TestImageHistory(t *testing.T) { @@ -97,69 +65,88 @@ func TestImageHistory(t *testing.T) { // possibly one is unpacked on the filessystem while the other is the tar file size? // - we do not truncate ids when --quiet has been provided // this is a conscious decision here - truncating with --quiet does not make much sense - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - - // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? - // Disabling for now - if runtime.GOOS == "windows" { - t.Skip("Windows is not supported for this test right now") - } - // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms - if runtime.GOARCH != "arm64" { - t.Skip("Windows is not supported for this test right now") + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImageHistory", + Require: test.Require( + test.Not(nerdtest.Docker), + // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? + test.Not(test.Windows), + // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms + test.Arm64, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--platform", "linux/arm64", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "trunc, no quiet, human", + Command: test.RunCommand("image", "history", "--human=true", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, len(history), 2, info) + assert.Equal(t, history[0].Size, "0B", info) + // FIXME: how is this going to age? + assert.Equal(t, history[0].CreatedSince, "3 years ago", info) + assert.Equal(t, history[0].Snapshot, "", info) + assert.Equal(t, history[0].Comment, "", info) + + localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") + localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") + compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) + compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) + assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String(), info) + assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", info) + assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String(), info) + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", info) + + assert.Equal(t, history[1].Size, "5.947MB", info) + assert.Equal(t, history[1].CreatedSince, "3 years ago", info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info) + assert.Equal(t, history[1].Comment, "", info) + }), + }, + { + Description: "no human - dates and sizes and not prettyfied", + Command: test.RunCommand("image", "history", "--human=false", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[0].Size, "0", info) + assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt, info) + assert.Equal(t, history[1].Size, "5947392", info) + assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt, info) + }), + }, + { + Description: "no trunc - do not truncate sha or cmd", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") + }), + }, + { + Description: "Quiet has no effect with format, so, go no-json, no-trunc", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + { + Description: "With quiet, trunc has no effect", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + }, } - base.Cmd("pull", "--platform", "linux/arm64", testutil.CommonImage).AssertOK() - - localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") - localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") - - // Human, no quiet, truncate - history := imageHistoryJSONHelper(base, testutil.CommonImage, false, false, true) - compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) - compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) - - // Two layers - assert.Equal(base.T, len(history), 2) - // First layer is a comment - zero size, no snap, - assert.Equal(base.T, history[0].Size, "0B") - assert.Equal(base.T, history[0].CreatedSince, "3 years ago") - assert.Equal(base.T, history[0].Snapshot, "") - assert.Equal(base.T, history[0].Comment, "") - - assert.Equal(base.T, compTime1.UTC().String(), localTimeL1.UTC().String()) - assert.Equal(base.T, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]") - - assert.Equal(base.T, compTime2.UTC().String(), localTimeL2.UTC().String()) - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…") - - assert.Equal(base.T, history[1].Size, "5.947MB") - assert.Equal(base.T, history[1].CreatedSince, "3 years ago") - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…") - assert.Equal(base.T, history[1].Comment, "") - - // No human - dates and sizes and not prettyfied - history = imageHistoryJSONHelper(base, testutil.CommonImage, false, false, false) - - assert.Equal(base.T, history[0].Size, "0") - assert.Equal(base.T, history[0].CreatedSince, history[0].CreatedAt) - - assert.Equal(base.T, history[1].Size, "5947392") - assert.Equal(base.T, history[1].CreatedSince, history[1].CreatedAt) - - // No trunc - do not truncate sha or cmd - history = imageHistoryJSONHelper(base, testutil.CommonImage, true, false, true) - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") - - // Quiet has no effect with format, so, go no-json, no-trunc - rawHistory := imageHistoryRawHelper(base, testutil.CommonImage, true, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") - - // With quiet, trunc has no effect - rawHistory = imageHistoryRawHelper(base, testutil.CommonImage, false, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index fceb83a0426..9d897176c67 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -24,143 +24,302 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestImagesWithNames(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("images", "--names", testutil.CommonImage).AssertOutContains(testutil.CommonImage) - base.Cmd("images", "--names", testutil.CommonImage).AssertOutWithFunc(func(out string) error { - lines := strings.Split(strings.TrimSpace(out), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - name, _ := tab.ReadRow(lines[1], "NAME") - assert.Equal(t, name, testutil.CommonImage) - return nil - }) -} - func TestImages(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" - if base.Target == testutil.Docker { - header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImages", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("pull", testutil.NginxAlpineImage) + }, + SubTests: []*test.Case{ + { + Description: "No params", + Command: test.RunCommand("images"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" + if nerdtest.GetTarget() == nerdtest.TargetDocker { + header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" + } + tab := tabutil.NewReader(header) + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, info) + found := false + for _, line := range lines[1:] { + repo, _ := tab.ReadRow(line, "REPOSITORY") + tag, _ := tab.ReadRow(line, "TAG") + if repo+":"+tag == testutil.CommonImage { + found = true + break + } + } + assert.Assert(t, found, info) + }, + } + }, + }, + { + Description: "With names", + Command: test.RunCommand("images", "--names", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(testutil.CommonImage), + func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, info) + found := false + for _, line := range lines[1:] { + name, _ := tab.ReadRow(line, "NAME") + if name == testutil.CommonImage { + found = true + break + } + } + + assert.Assert(t, found, info) + }, + ), + } + }, + }, + { + Description: "CheckCreatedTime", + Command: test.RunCommand("images", "--format", "'{{json .CreatedAt}}'"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + createdTimes := lines + slices.Reverse(createdTimes) + assert.Assert(t, slices.IsSorted(createdTimes), info) + }, + } + }, + }, + }, } - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("images", testutil.CommonImage).AssertOutWithFunc(func(out string) error { - lines := strings.Split(strings.TrimSpace(out), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - tab := tabutil.NewReader(header) - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - repo, _ := tab.ReadRow(lines[1], "REPOSITORY") - tag, _ := tab.ReadRow(lines[1], "TAG") - assert.Equal(t, repo+":"+tag, testutil.CommonImage) - return nil - }) + testCase.Run(t) } func TestImagesFilter(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - t.Parallel() - base := testutil.NewBase(t) - tempName := testutil.Identifier(base.T) - base.Cmd("pull", testutil.CommonImage).AssertOK() - - dockerfile := fmt.Sprintf(`FROM %s + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagesFilter", + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + // FIXME: we might want to get rid of these and just use tag + helpers.Ensure("pull", "busybox:glibc") + helpers.Ensure("pull", "busybox:uclibc") + + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] \n LABEL foo=bar -LABEL version=0.1`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - base.Cmd("build", "-t", tempName, "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() - defer base.Cmd("rmi", tempName).AssertOK() - - busyboxGlibc, busyboxUclibc := "busybox:glibc", "busybox:uclibc" - base.Cmd("pull", busyboxGlibc).AssertOK() - defer base.Cmd("rmi", busyboxGlibc).AssertOK() - - base.Cmd("pull", busyboxUclibc).AssertOK() - defer base.Cmd("rmi", busyboxUclibc).AssertOK() - - // before/since filters are not compatible with DOCKER_BUILDKIT=1? (but still compatible with DOCKER_BUILDKIT=0) - if base.Target == testutil.Nerdctl { - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutNotContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s:%s", "non-exists-image", "non-exists-image")).AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", "non-exists-image", "non-exists-image")).AssertOutContains(tempName) +LABEL version=0.1 +RUN echo "actually creating a layer so that docker sets the createdAt time" +`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + data.Set("buildCtx", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "busybox:glibc") + helpers.Anyhow("rmi", "busybox:uclibc") + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + data.Set("builtImageID", data.Identifier()) + return helpers.Command("build", "-t", data.Identifier(), data.Get("buildCtx")) + }, + Expected: test.Expects(0, nil, nil), + SubTests: []*test.Case{ + { + Description: "label=foo=bar", + Command: test.RunCommand("images", "--filter", "label=foo=bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar1", + Command: test.RunCommand("images", "--filter", "label=foo=bar1"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.DoesNotContain(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar label=version=0.1", + Command: test.RunCommand("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar label=version=0.1", + Command: test.RunCommand("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.DoesNotContain(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=version", + Command: test.RunCommand("images", "--filter", "label=version"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "reference=ID*", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("images", "--filter", fmt.Sprintf("reference=%s*", data.Get("builtImageID"))) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "reference=busy*:*libc*", + Command: test.RunCommand("images", "--filter", "reference=busy*:*libc*"), + Expected: test.Expects(0, nil, test.All( + test.Contains("glibc"), + test.Contains("uclibc"), + )), + }, + { + Description: "before=ID:latest", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("images", "--filter", fmt.Sprintf("before=%s:latest", data.Get("builtImageID"))) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(testutil.ImageRepo(testutil.CommonImage)), + test.DoesNotContain(data.Get("builtImageID")), + ), + } + }, + }, + { + Description: "since=" + testutil.CommonImage, + Command: test.RunCommand("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "since=" + testutil.CommonImage + " " + testutil.CommonImage, + Command: test.RunCommand("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "since=non-exists-image", + Command: test.RunCommand("images", "--filter", "since=non-exists-image"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "before=non-exists-image", + Command: test.RunCommand("images", "--filter", "before=non-exists-image"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + }, } - base.Cmd("images", "--filter", "label=foo=bar").AssertOutContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar1").AssertOutNotContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1").AssertOutContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2").AssertOutNotContains(tempName) - base.Cmd("images", "--filter", "label=version").AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("reference=%s*", tempName)).AssertOutContains(tempName) - base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("glibc") - base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("uclibc") + + testCase.Run(t) } func TestImagesFilterDangling(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Cmd("container", "prune", "-f").AssertOK() - base.Cmd("image", "prune", "--all", "-f").AssertOK() + nerdtest.Setup() - dockerfile := fmt.Sprintf(`FROM %s + testCase := &test.Case{ + Description: "TestImagesFilterDangling", + // This test relies on a clean slate and the ability to GC everything + NoParallel: true, + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() - - // dangling image test - base.Cmd("images", "--filter", "dangling=true").AssertOutContains("") - base.Cmd("images", "--filter", "dangling=false").AssertOutNotContains("") -} - -func TestImageListCheckCreatedTime(t *testing.T) { - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("pull", testutil.NginxAlpineImage).AssertOK() - - var createdTimes []string - - base.Cmd("images", "--format", "'{{json .CreatedAt}}'").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 4 lines, got %d", len(lines)) - } - createdTimes = append(createdTimes, lines...) - return nil - }) - - slices.Reverse(createdTimes) - if !slices.IsSorted(createdTimes) { - t.Errorf("expected images in decending order") + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + data.Set("buildCtx", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "prune", "-f") + helpers.Anyhow("image", "prune", "--all", "-f") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx")) + }, + Expected: test.Expects(0, nil, nil), + SubTests: []*test.Case{ + { + Description: "dangling", + Command: test.RunCommand("images", "--filter", "dangling=true"), + Expected: test.Expects(0, nil, test.Contains("")), + }, + { + Description: "not dangling", + Command: test.RunCommand("images", "--filter", "dangling=false"), + Expected: test.Expects(0, nil, test.DoesNotContain("")), + }, + }, } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_load_linux_test.go b/cmd/nerdctl/image/image_load_linux_test.go deleted file mode 100644 index 4d7b0f83dce..00000000000 --- a/cmd/nerdctl/image/image_load_linux_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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 image - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestLoadStdinFromPipe(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - img := testutil.Identifier(t) - tmp := t.TempDir() - output := filepath.Join(tmp, "output") - - setup := func() { - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("tag", testutil.CommonImage, img).AssertOK() - base.Cmd("save", img, "-o", filepath.Join(tmp, "common.tar")).AssertOK() - base.Cmd("rmi", "-f", img).AssertOK() - } - - tearDown := func() { - base.Cmd("rmi", "-f", img).AssertOK() - } - - t.Cleanup(tearDown) - tearDown() - - setup() - - loadCmd := strings.Join(base.Cmd("load").Command, " ") - combined, err := exec.Command("sh", "-euxc", fmt.Sprintf("`cat %s/common.tar | %s > %s`", tmp, loadCmd, output)).CombinedOutput() - assert.NilError(t, err, "failed with error %s and combined output is %s", err, string(combined)) - - fb, err := os.ReadFile(output) - assert.NilError(t, err) - - assert.Assert(t, strings.Contains(string(fb), fmt.Sprintf("Loaded image: %s:latest", img))) - base.Cmd("images").AssertOutContains(img) -} - -func TestLoadStdinEmpty(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("load").AssertFail() -} diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go new file mode 100644 index 00000000000..5619d829018 --- /dev/null +++ b/cmd/nerdctl/image/image_load_test.go @@ -0,0 +1,81 @@ +/* + 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 image + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestLoadStdinFromPipe(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinFromPipe", + Require: test.Linux, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + helpers.Ensure("save", data.Identifier(), "-o", filepath.Join(data.TempDir(), "common.tar")) + helpers.Ensure("rmi", "-f", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + cmd := helpers.Command("load") + reader, err := os.Open(filepath.Join(data.TempDir(), "common.tar")) + assert.NilError(t, err, "failed to open common.tar") + cmd.WithStdin(reader) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(fmt.Sprintf("Loaded image: %s:latest", data.Identifier())), + func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("images"), data.Identifier())) + }, + ), + } + }, + } + + testCase.Run(t) +} + +func TestLoadStdinEmpty(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinEmpty", + Require: test.Linux, + Command: test.RunCommand("load"), + Expected: test.Expects(1, nil, nil), + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index 94ef0c625f2..6355803537c 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -18,117 +18,210 @@ package image import ( "fmt" + "strings" "testing" "time" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestImagePrune(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).AssertOK() - - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", buildCtx).AssertOK() - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images").AssertOutContainsAll(imageName, "") - - base.Cmd("image", "prune", "--force").AssertOutNotContains(imageName) - base.Cmd("images").AssertOutNotContains("") - base.Cmd("images").AssertOutContains(imageName) -} - -func TestImagePruneAll(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - // The following commands will clean up all images, so it should fail at this point. - defer base.Cmd("rmi", imageName).AssertFail() - base.Cmd("images").AssertOutContains(imageName) - - tID := testutil.Identifier(t) - base.Cmd("run", "--name", tID, imageName).AssertOK() - base.Cmd("image", "prune", "--force", "--all").AssertOutNotContains(imageName) - base.Cmd("images").AssertOutContains(imageName) - - base.Cmd("rm", "-f", tID).AssertOK() - base.Cmd("image", "prune", "--force", "--all").AssertOutContains(imageName) - base.Cmd("images").AssertOutNotContains(imageName) -} - -func TestImagePruneFilterLabel(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) }) - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + testCase.NoParallel = true + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // We need to delete everything here for prune to make any sense + base := testutil.NewBase(t) + testhelpers.RmiAll(base) + } + testCase.SubTests = []*test.Case{ + { + Description: "without all", + NoParallel: true, + Require: test.Require( + // This never worked with Docker - the only reason we ever got was side effects from other tests + // See inline comments. + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s + CMD ["echo", "nerdctl-test-image-prune"] + `, testutil.CommonImage) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + // After we rebuild with tag, docker will no longer show the version from above + // Swapping order does not change anything. + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with all", + Require: test.Require( + // Same as above + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s + CMD ["echo", "nerdctl-test-image-prune"] + `, testutil.CommonImage) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + helpers.Ensure("run", "--name", data.Identifier(), data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + helpers.Ensure("rm", "-f", data.Identifier()) + removed := helpers.Capture("image", "prune", "--force", "--all") + assert.Assert(t, strings.Contains(removed, data.Identifier()), info) + imgList = helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with filter label", + Require: nerdtest.Build, + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=baz").AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=bar").AssertOK() - base.Cmd("images", "--all").AssertOutNotContains(imageName) -} - -func TestImagePruneFilterUntil(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - // For deterministically testing the filter, set the image's created timestamp to 2 hours in the past. - base.Env = append(base.Env, fmt.Sprintf("SOURCE_DATE_EPOCH=%d", time.Now().Add(-2*time.Hour).Unix())) - - imageName := testutil.Identifier(t) - teardown := func() { - // Image should have been pruned; but cleanup on failure. - base.Cmd("rmi", "--force", imageName).Run() + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all", "--filter", "label=foo=baz"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") + assert.Assert(t, strings.Contains(prune, data.Identifier()), info) + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with until", + Require: nerdtest.Build, + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +RUN echo "Anything, so that we create actual content for docker to set the current time for CreatedAt" +CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + data.Set("imageID", data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all", "--filter", "until=12h"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("imageID")), + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Get("imageID")), info) + }, + ), + } + }, + SubTests: []*test.Case{ + { + Description: "Wait and remove until=10ms", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + time.Sleep(1 * time.Second) + }, + Command: test.RunCommand("image", "prune", "--force", "--all", "--filter", "until=10ms"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("imageID")), + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Get("imageID")), imgList, info) + }, + ), + } + }, + }, + }, + }, } - t.Cleanup(teardown) - teardown() - - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-test-image-prune-filter-until"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "until=12h").AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - // Pause to ensure enough time has passed for the image to be cleaned on next prune. - time.Sleep(3 * time.Second) - base.Cmd("image", "prune", "--force", "--all", "--filter", "until=10ms").AssertOK() - base.Cmd("images", "--all").AssertOutNotContains(imageName) + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index d3e956238cf..a3fcf0e9842 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -18,152 +18,217 @@ package image import ( "fmt" - "os/exec" + "strconv" "strings" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestImageVerifyWithCosign(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - testImageRef := fmt.Sprintf("%s:%d/%s", - localhostIP, reg.Port, tID) - t.Logf("testImageRef=%q", testImageRef) - - dockerfile := fmt.Sprintf(`FROM %s +func TestImagePullWithCosign(t *testing.T) { + nerdtest.Setup() + + var registry *testregistry.RegistryServer + var keyPair *testhelpers.CosignKeyPair + + testCase := &test.Case{ + Description: "TestImagePullWithCosign", + Require: test.Require( + test.Linux, + nerdtest.Build, + test.Binary("cosign"), + test.Not(nerdtest.Docker), + ), + Env: map[string]string{ + "COSIGN_PASSWORD": "1", + }, + Setup: func(data test.Data, helpers test.Helpers) { + keyPair = testhelpers.NewCosignKeyPair(t, "cosign-key-pair", "1") + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", testImageRef, buildCtx) + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":one") + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":two") + helpers.Ensure("rmi", "-f", testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if keyPair != nil { + keyPair.Cleanup() + } + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + helpers.Anyhow("rmi", "-f", testImageRef) + } + }, + SubTests: []*test.Case{ + { + Description: "Pull with the correct key", + Command: func(data test.Data, helpers test.Helpers) test.Command { + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, testImageRef+":one") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Pull with unrelated key", + Env: map[string]string{ + "COSIGN_PASSWORD": "2", + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + newKeyPair := testhelpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey, testImageRef+":two") + }, + Expected: test.Expects(1, nil, nil), + }, + }, + } - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() + testCase.Run(t) } func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 80, false) - defer reg.Cleanup(nil) - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - t.Logf("testImageRef=%q", testImageRef) - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() - base.Cmd("--insecure-registry", "pull", testImageRef).AssertOK() -} - -func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - testImageRef := fmt.Sprintf("%s:%d/%s", - localhostIP, reg.Port, tID) - t.Logf("testImageRef=%q", testImageRef) - - dockerfile := fmt.Sprintf(`FROM %s + nerdtest.Setup() + + var registry *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "TestImagePullPlainHttpWithDefaultPort", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() - - base.Env = append(base.Env, "COSIGN_PASSWORD=2") - newKeyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey).AssertFail() -} - -func TestPullSoci(t *testing.T) { - testutil.DockerIncompatible(t) - tests := []struct { - name string - sociIndexDigest string - image string - remoteSnapshotsExpectedCount int - }{ - { - name: "Run without specifying SOCI index", - sociIndexDigest: "", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", testImageRef, buildCtx) + helpers.Ensure("--insecure-registry", "push", testImageRef) + helpers.Ensure("rmi", "-f", testImageRef) }, - { - name: "Run with bad SOCI index", - sociIndexDigest: "sha256:thisisabadindex0000000000000000000000000000000000000000000000000", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + Command: func(data test.Data, helpers test.Helpers) test.Command { + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + return helpers.Command("--insecure-registry", "pull", testImageRef) }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - helpers.RequiresSoci(base) - - //counting initial snapshot mounts - initialMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) + Expected: test.Expects(0, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + helpers.Anyhow("rmi", "-f", testImageRef) } + }, + } - remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge") - - pullOutput := base.Cmd("--snapshotter=soci", "pull", tt.image).Out() - base.T.Logf("pull output: %s", pullOutput) - - actualMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) - } - remoteSnapshotsActualCount := strings.Count(string(actualMounts), "fuse.rawBridge") - base.T.Logf("number of actual mounts: %v", remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - - rmiOutput := base.Cmd("rmi", testutil.FfmpegSociImage).Out() - base.T.Logf("rmi output: %s", rmiOutput) - - base.T.Logf("number of expected mounts: %v", tt.remoteSnapshotsExpectedCount) + testCase.Run(t) +} - if tt.remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) { - t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d", - tt.remoteSnapshotsExpectedCount, remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - } - }) +func TestImagePullSoci(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagePullSoci", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Soci, + ), + + // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount` + // They also feel prone to raciness... + SubTests: []*test.Case{ + { + Description: "Run without specifying SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", ""), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.CustomCommand("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), + info) + }, + } + }, + }, + { + Description: "Run with bad SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", "sha256:thisisabadindex0000000000000000000000000000000000000000000000000"), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.CustomCommand("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), + info) + }, + } + }, + }, + }, } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 17f757703cf..bc5dc1e41d3 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -17,6 +17,7 @@ package image import ( + "errors" "fmt" "net/http" "strings" @@ -24,171 +25,230 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestPushPlainHTTPFails(t *testing.T) { - testutil.RequiresBuild(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - res := base.Cmd("push", testImageRef).Run() - resCombined := res.Combined() - t.Logf("result: exitCode=%d, out=%q", res.ExitCode, res) - assert.Assert(t, res.ExitCode != 0) - assert.Assert(t, strings.Contains(resCombined, "server gave HTTP response to HTTPS client")) -} - -func TestPushPlainHTTPLocalhost(t *testing.T) { - testutil.RequiresBuild(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - localhostIP, reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("push", testImageRef).AssertOK() -} - -func TestPushPlainHTTPInsecure(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushPlainHttpInsecureWithDefaultPort(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 80, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushInsecureWithLogin(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) - defer reg.Cleanup(nil) - - base.Cmd("--insecure-registry", "login", "-u", "admin", "-p", "badmin", - fmt.Sprintf("%s:%d", reg.IP.String(), reg.Port)).AssertOK() - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("push", testImageRef).AssertFail() - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushWithHostsDir(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because Docker doesn't have `--hosts-dir` option, and we don't want to contaminate the global /etc/docker/certs.d during this test - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) - defer reg.Cleanup(nil) - - base.Cmd("--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", reg.IP.String(), reg.Port)).AssertOK() - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--debug", "--hosts-dir", reg.HostsDir, "push", testImageRef).AssertOK() -} - -func TestPushNonDistributableArtifacts(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - // Skip docker, because "--allow-nondistributable-artifacts" is a daemon-only option and requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.NonDistBlobImage).AssertOK() - - testImgRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.NonDistBlobImage, ":")[1]) - base.Cmd("tag", testutil.NonDistBlobImage, testImgRef).AssertOK() - - base.Cmd("--debug", "--insecure-registry", "push", testImgRef).AssertOK() - - blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", reg.IP.String(), reg.Port, testutil.Identifier(t), testutil.NonDistBlobDigest) - resp, err := http.Get(blobURL) - assert.Assert(t, err, "error making http request") - if resp.Body != nil { - resp.Body.Close() - } - assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") - - base.Cmd("--debug", "--insecure-registry", "push", "--allow-nondistributable-artifacts", testImgRef).AssertOK() - resp, err = http.Get(blobURL) - assert.Assert(t, err, "error making http request") - if resp.Body != nil { - resp.Body.Close() +func TestPush(t *testing.T) { + nerdtest.Setup() + + var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "Test push", + + Require: test.Linux, + + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registryNoAuthHTTPRandom = testregistry.NewWithNoAuth(base, 0, false) + registryNoAuthHTTPDefault = testregistry.NewWithNoAuth(base, 80, false) + registryTokenAuthHTTPSRandom = testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) + }, + + Cleanup: func(data test.Data, helpers test.Helpers) { + if registryNoAuthHTTPRandom != nil { + registryNoAuthHTTPRandom.Cleanup(nil) + // XXX might crash + registryNoAuthHTTPDefault.Cleanup(nil) + registryTokenAuthHTTPSRandom.Cleanup(nil) + } + }, + + SubTests: []*test.Case{ + { + Description: "plain http", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", data.Get("testImageRef")) + }, + Expected: test.Expects(1, []error{errors.New("server gave HTTP response to HTTPS client")}, nil), + }, + { + Description: "plain http with insecure", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "plain http with localhost", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "plain http with insecure, default port", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s/%s:%s", + registryNoAuthHTTPDefault.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with insecure, with login", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + helpers.Ensure("--insecure-registry", "login", "-u", "admin", "-p", "badmin", + fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) + + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with hosts dir, with login", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", + fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) + + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "non distributable artifacts", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NonDistBlobImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) + resp, err := http.Get(blobURL) + assert.Assert(t, err, "error making http request") + if resp.Body != nil { + resp.Body.Close() + } + assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") + }, + } + }, + }, + { + Description: "non distributable artifacts (with)", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NonDistBlobImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Get("testImageRef")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) + resp, err := http.Get(blobURL) + assert.Assert(t, err, "error making http request") + if resp.Body != nil { + resp.Body.Close() + } + assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") + }, + } + }, + }, + { + Description: "soci", + Require: test.Require( + nerdtest.Soci, + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.UbuntuImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.UbuntuImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.UbuntuImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, } - assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") -} - -func TestPushSoci(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresSoci(base) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.UbuntuImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.UbuntuImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.UbuntuImage, testImageRef).AssertOK() - - base.Cmd("--snapshotter=soci", "--insecure-registry", "push", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", testImageRef).AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_remove_linux_test.go b/cmd/nerdctl/image/image_remove_linux_test.go deleted file mode 100644 index 5752aa04aa4..00000000000 --- a/cmd/nerdctl/image/image_remove_linux_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - 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 image - -import ( - "testing" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestRemoveImage(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - base.Cmd("image", "prune", "--force", "--all").AssertOK() - - // ignore error - base.Cmd("rmi", "-f", tID).AssertOK() - - base.Cmd("run", "--name", tID, testutil.CommonImage).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - defer base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemoveRunningImage(t *testing.T) { - // If an image is associated with a running/paused containers, `docker rmi -f imageName` - // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. - // In both cases, `nerdctl rmi -f` will fail. - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail() - base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - - base.Cmd("kill", tID).AssertOK() - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemovePausedImage(t *testing.T) { - // If an image is associated with a running/paused containers, `docker rmi -f imageName` - // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. - // In both cases, `nerdctl rmi -f` will fail. - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") - } - tID := testutil.Identifier(t) - - base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK() - base.Cmd("pause", tID).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail() - base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - - base.Cmd("kill", tID).AssertOK() - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemoveImageWithCreatedContainer(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("pull", testutil.AlpineImage).AssertOK() - base.Cmd("pull", testutil.NginxAlpineImage).AssertOK() - - base.Cmd("create", "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.AlpineImage).AssertFail() - base.Cmd("rmi", "-f", testutil.AlpineImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.AlpineImage)) - - // a created container with removed image doesn't impact other `rmi` command - base.Cmd("rmi", "-f", testutil.NginxAlpineImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.NginxAlpineImage)) -} diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go new file mode 100644 index 00000000000..ffa661176b1 --- /dev/null +++ b/cmd/nerdctl/image/image_remove_test.go @@ -0,0 +1,304 @@ +/* + 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 image + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestRemove(t *testing.T) { + testCase := nerdtest.Setup() + + repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) + nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) + // NOTES: + // - since all of these are rmi-ing the common image, we need private mode + testCase.Require = nerdtest.Private + + testCase.SubTests = []*test.Case{ + { + Description: "Remove image with stopped container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with stopped container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - with -f", + NoParallel: true, + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - without -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NginxAlpineImage) + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("rmi", testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.All( + test.DoesNotContain(repoName), + // a created container with removed image doesn't impact other `rmi` command + test.DoesNotContain(nginxRepoName), + ), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + nerdtest.CGroup, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - with -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + nerdtest.CGroup, + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/image/image_save_linux_test.go b/cmd/nerdctl/image/image_save_linux_test.go deleted file mode 100644 index 0c7c722e97e..00000000000 --- a/cmd/nerdctl/image/image_save_linux_test.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - 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 image - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestSave(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.AlpineImage).AssertOK() - archiveTarPath := filepath.Join(t.TempDir(), "a.tar") - base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK() - rootfsPath := filepath.Join(t.TempDir(), "rootfs") - err := helpers.ExtractDockerArchive(archiveTarPath, rootfsPath) - assert.NilError(t, err) - etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") - etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) - assert.NilError(t, err) - etcOSRelease := string(etcOSReleaseBytes) - t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage) - t.Log(etcOSRelease) - assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) -} diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index c8078967477..1dc757a4eeb 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -17,54 +17,119 @@ package image import ( + "os" "path/filepath" "strings" "testing" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestSaveById(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] +func TestSaveContent(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "Test content (linux only)", + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("save", "-o", filepath.Join(data.TempDir(), "out.tar"), testutil.CommonImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + rootfsPath := filepath.Join(data.TempDir(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.TempDir(), "out.tar"), rootfsPath) + assert.NilError(t, err) + etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") + etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) + assert.NilError(t, err) + etcOSRelease := string(etcOSReleaseBytes) + assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) + }, + } + }, } - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() + + testCase.Run(t) } -func TestSaveByIdWithDifferentNames(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] - } +func TestSave(t *testing.T) { + testCase := nerdtest.Setup() + + // This test relies on the fact that we can remove the common image, which definitely conflicts with others, + // hence the private mode. + // Further note though, that this will hide the fact this the save command could fail if some layers are missing. + // See https://github.com/containerd/nerdctl/issues/3425 and others for details. + testCase.Require = nerdtest.Private - base.Cmd("tag", testutil.CommonImage, "foobar").AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "Single image, by id", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + // Docker and Nerdctl do not agree on what is the definition of an image ID + if nerdtest.GetTarget() == nerdtest.TargetDocker { + id = img.ID + } else { + id = strings.Split(img.RepoDigests[0], ":")[1] + } + tarPath := filepath.Join(data.TempDir(), "out.tar") + helpers.Ensure("save", "-o", tarPath, id) + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("load", "-i", tarPath) + data.Set("id", id) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + { + Description: "Image with different names, by id", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if nerdtest.GetTarget() == nerdtest.TargetDocker { + id = img.ID + } else { + id = strings.Split(img.RepoDigests[0], ":")[1] + } + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + tarPath := filepath.Join(data.TempDir(), "out.tar") + helpers.Ensure("save", "-o", tarPath, id) + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("load", "-i", tarPath) + data.Set("id", id) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + } - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go b/cmd/nerdctl/ipfs/ipfs_build_linux_test.go deleted file mode 100644 index 7ca00803bdb..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - 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 ipfs - -import ( - "fmt" - "strings" - "testing" - "time" - - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestIPFSBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") - - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s -CMD ["echo", "nerdctl-build-test-string"] - `, ipfsCIDBase) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - done := ipfsRegistryUp(t, base) - defer done() - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName).AssertOK() - - base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-build-test-string") -} - -func ipfsRegistryUp(t *testing.T, base *testutil.Base, args ...string) (done func() error) { - res := icmd.StartCmd(base.Cmd(append([]string{"ipfs", "registry", "serve"}, args...)...).Cmd) - time.Sleep(time.Second) - assert.Assert(t, res.Cmd.Process != nil) - assert.NilError(t, res.Error) - return func() error { - res.Cmd.Process.Kill() - icmd.WaitOnCmd(3*time.Second, res) - return nil - } -} diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index 1ac2b682482..7411b4a3e6a 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -19,71 +19,89 @@ package ipfs import ( "fmt" "io" + "os" + "path/filepath" "strings" "testing" + "time" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSComposeUp(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - tests := []struct { - name string - snapshotter string - pushOptions []string - composeOptions []string - requiresStargz bool - }{ - { - name: "overlayfs", - snapshotter: "overlayfs", - }, - { - name: "stargz", - snapshotter: "stargz", - pushOptions: []string{"--estargz"}, - requiresStargz: true, - }, - { - name: "ipfs-address", - snapshotter: "overlayfs", - pushOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - composeOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - }, +func composeUpNoBuild(t *testing.T, stargz bool, byAddr bool) { + testCase := nerdtest.Setup() + + const ipfsAddrKey = "ipfsAddrKey" + const mariaImageCIDKey = "mariaImageCIDKey" + const wordpressImageCIDKey = "wordpressImageCIDKey" + const composeExtraKey = "composeExtraKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + // Linux only + test.Linux, + // Obviously not docker supported + test.Not(nerdtest.Docker), + nerdtest.Registry, + ) + + if stargz { + testCase.Env = map[string]string{ + "CONTAINERD_SNAPSHOTTER": "stargz", + } + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Start Kubo + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + data.Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)) + + helpers.Ensure("pull", "--quiet", testutil.WordpressImage) + helpers.Ensure("pull", "--quiet", testutil.MariaDBImage) + var ipfsCIDWP, ipfsCIDMD string + if stargz { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--estargz") + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--estargz") + } else if byAddr { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + data.Set(composeExtraKey, "--ipfs-address="+data.Get(ipfsAddrKey)) + } else { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage) + } + data.Set(wordpressImageCIDKey, ipfsCIDWP) + data.Set(mariaImageCIDKey, ipfsCIDMD) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - if tt.requiresStargz { - helpers.RequiresStargz(base) - } - ipfsImgs := make([]string, 2) - for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} { - ipfsImgs[i] = pushImageToIPFS(t, base, img, tt.pushOptions...) - } - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER="+tt.snapshotter) - helpers.ComposeUp(t, base, fmt.Sprintf(` + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + helpers.Anyhow("rmi", data.Get(mariaImageCIDKey)) + helpers.Anyhow("rmi", data.Get(wordpressImageCIDKey)) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.Command { + // FIXME: right t, but brittle + composeUP(data, helpers, t, fmt.Sprintf(` version: '3.1' services: wordpress: - image: %s + image: ipfs://%s restart: always ports: + # FIXME: this is really bad and likely to collide with other tests - 8080:80 environment: WORDPRESS_DB_HOST: db @@ -96,7 +114,7 @@ services: - wordpress:/var/www/html db: - image: %s + image: ipfs://%s restart: always environment: MYSQL_DATABASE: exampledb @@ -111,46 +129,163 @@ services: volumes: wordpress: db: -`, ipfsImgs[0], ipfsImgs[1]), tt.composeOptions...) - }) +`, data.Get(wordpressImageCIDKey), data.Get(mariaImageCIDKey)), data.Get(composeExtraKey)) + return helpers.Command("info") } + + testCase.Expected = test.Expects(0, nil, test.Equals("hello\n")) + + testCase.Run(t) +} + +func TestIPFSComposeUpNoBuildDefault(t *testing.T) { + composeUpNoBuild(t, false, false) +} + +func TestIPFSComposeUpNoBuildWithStargz(t *testing.T) { + composeUpNoBuild(t, true, false) +} + +func TestIPFSComposeUpNoBuildWithAddr(t *testing.T) { + composeUpNoBuild(t, false, true) } func TestIPFSComposeUpBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.NginxAlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") - - const dockerComposeYAML = ` + testCase := nerdtest.Setup() + + var ipfsServer test.Command + var comp *testutil.ComposeDir + + const mainImageCIDKey = "mainImageCIDKey" + // FIXME: this is bad and likely to collide with other tests + const listenAddr = "localhost:5556" + + testCase.Require = test.Require( + // Linux only + test.Linux, + // Obviously not docker supported + test.Not(nerdtest.Docker), + nerdtest.Build, + // FIXME: requiring a lot more than that - we need a working ipfs daemon + test.Binary("ipfs"), + ) + + testCase.Env = map[string]string{ + // Point IPFS_PATH to the expected location + "IPFS_PATH": filepath.Join(os.Getenv("HOME"), ".local/share/ipfs"), + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Get alpine + helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) + // Start a local ipfs backed registry + // FIXME: this is bad and likely to collide with other tests + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) + + // Save nginx to ipfs + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) + + const dockerComposeYAML = ` services: web: build: . ports: - - 8080:80 + - 8081:80 ` - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s + dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s COPY index.html /usr/share/nginx/html/index.html -`, ipfsCIDBase) - indexHTML := t.Name() +`, listenAddr, data.Get(mainImageCIDKey)) + + comp = testutil.NewComposeDir(t, dockerComposeYAML) + comp.WriteFile("Dockerfile", dockerfile) + comp.WriteFile("index.html", data.Identifier("indexhtml")) + + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + ipfsServer.Run(nil) + comp.CleanUp() + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("compose", "-f", comp.YAMLFullPath(), "up", "-d", "--build") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + t.Logf("respBody=%q", respBody) + assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml"))) + }, + } + } + + testCase.Run(t) +} +func composeUP(data test.Data, helpers test.Helpers, t *testing.T, dockerComposeYAML string, opts string) { comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - comp.WriteFile("Dockerfile", dockerfile) - comp.WriteFile("index.html", indexHTML) - - done := ipfsRegistryUp(t, base) - defer done() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - t.Logf("respBody=%q", respBody) - assert.Assert(t, strings.Contains(string(respBody), indexHTML)) + // defer comp.CleanUp() + + projectName := comp.ProjectName() + + args := []string{"compose", "-f", comp.YAMLFullPath()} + if opts != "" { + args = append(args, opts) + } + helpers.Ensure(append(args, "up", "--quiet-pull", "-d")...) + + helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName)) + + defer helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + + checkWordpress := func() error { + // FIXME: see other notes on using the same port repeatedly + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 5, false) + if err != nil { + return err + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { + return fmt.Errorf("respBody does not contain %q (%s)", testutil.WordpressIndexHTMLSnippet, string(respBody)) + } + return nil + } + + var wordpressWorking bool + var err error + // 15 seconds is long enough + for i := 0; i < 5; i++ { + err = checkWordpress() + if err == nil { + wordpressWorking = true + break + } + time.Sleep(3 * time.Second) + } + + if !wordpressWorking { + t.Fatalf("wordpress is not working %v", err) + } + + helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v") + helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName)) } diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go new file mode 100644 index 00000000000..c3cc9e0401a --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -0,0 +1,107 @@ +/* + 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 ipfs + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSAddrWithKubo(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImagemainImageCIDKey" + const ipfsAddrKey = "ipfsAddrKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Registry, + nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + ipfsAddr := fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port) + data.Set(ipfsAddrKey, ipfsAddr) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey))) + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.Private, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Env: map[string]string{ + "CONTAINERD_SNAPSHOTTER": "stargz", + }, + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey)), "--estargz") + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/ipfs/ipfs_linux_test.go b/cmd/nerdctl/ipfs/ipfs_linux_test.go deleted file mode 100644 index a50e426ae0f..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_linux_test.go +++ /dev/null @@ -1,148 +0,0 @@ -/* - 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 ipfs - -import ( - "fmt" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" -) - -func TestIPFS(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - - // encryption - keyPair := helpers.NewJWEKeyPair(t) - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - encryptImageRef := tID + ":enc" - layersNum := 1 - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, ipfsCID, encryptImageRef).AssertOK() - base.Cmd("image", "inspect", "--mode=native", "--format={{len .Manifest.Layers}}", encryptImageRef).AssertOutExactly(fmt.Sprintf("%d\n", layersNum)) - for i := 0; i < layersNum; i++ { - base.Cmd("image", "inspect", "--mode=native", fmt.Sprintf("--format={{json (index .Manifest.Layers %d) }}", i), encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") - } - ipfsCIDEnc := cidOf(t, base.Cmd("push", "ipfs://"+encryptImageRef).OutLines()) - helpers.RmiAll(base) - - decryptImageRef := tID + ":dec" - base.Cmd("pull", "--unpack=false", ipfsCIDEnc).AssertOK() - base.Cmd("image", "decrypt", "--key="+keyPair.Pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.Prv, ipfsCIDEnc, decryptImageRef).AssertOK() - base.Cmd("run", "--rm", decryptImageRef, "/bin/sh", "-c", "echo hello").AssertOK() -} - -func TestIPFSAddress(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", ipfsaddr)) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", "--ipfs-address", ipfsaddr, ipfsCID).AssertOK() - base.Cmd("run", "--ipfs-address", ipfsaddr, "--rm", ipfsCID, "echo", "hello").AssertOK() -} - -func TestIPFSCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK() -} - -func TestIPFSWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() -} - -func TestIPFSWithLazyPullingCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "ls /.stargz-snapshotter && cat /hello").AssertOK() - base.Cmd("image", "rm", ipfsCID2).AssertOK() -} - -func pushImageToIPFS(t *testing.T, base *testutil.Base, name string, opts ...string) string { - base.Cmd("pull", name).AssertOK() - ipfsCID := cidOf(t, base.Cmd(append([]string{"push"}, append(opts, "ipfs://"+name)...)...).OutLines()) - base.Cmd("rmi", name).Run() - return ipfsCID -} - -func cidOf(t *testing.T, lines []string) string { - assert.Equal(t, len(lines) >= 2, true) - return "ipfs://" + lines[len(lines)-2] -} diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index d367d0d9f86..eadcfabf8a9 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -17,44 +17,141 @@ package ipfs import ( + "fmt" + "os" + "path/filepath" "strings" "testing" + "time" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSRegistry(t *testing.T) { - testutil.DockerIncompatible(t) +func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { + var ipfsCID string + cmd := helpers.Command("push", "ipfs://"+name) + cmd.WithArgs(opts...) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(stdout, "\n") + assert.Equal(t, len(lines) >= 2, true) + ipfsCID = lines[len(lines)-2] + }, + }) + return ipfsCID +} - base := testutil.NewBase(t) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) +func TestIPFSNerdctlRegistry(t *testing.T) { + testCase := nerdtest.Setup() - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "echo", "hello").AssertOK() -} + // FIXME: this is bad and likely to collide with other tests + const listenAddr = "localhost:5555" -func TestIPFSRegistryWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) + const ipfsImageURLKey = "ipfsImageURLKey" - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) + var ipfsServer test.Command - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "ls", "/.stargz-snapshotter").AssertOK() -} + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + // FIXME: requiring a lot more than that - we need a working ipfs daemon + test.Binary("ipfs"), + ) + + testCase.Env = map[string]string{ + // Point IPFS_PATH to the expected location + "IPFS_PATH": filepath.Join(os.Getenv("HOME"), ".local/share/ipfs"), + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + // Start a local ipfs backed registry + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + ipfsServer.Run(nil) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, data.Get(listenAddr)+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotterr", + NoParallel: true, + Require: nerdtest.Stargz, + Env: map[string]string{ + "CONTAINERD_SNAPSHOTTER": "stargz", + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, data.Get(listenAddr)+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with build", + NoParallel: true, + Require: nerdtest.Build, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("built-image")) + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, data.Get(listenAddr)+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, data.Get(ipfsImageURLKey)) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + + helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Identifier("built-image")) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + } -func ipfsRegistryReference(addr string, c string) string { - return addr + "/ipfs/" + strings.TrimPrefix(c, "ipfs://") + testCase.Run(t) } diff --git a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go new file mode 100644 index 00000000000..399cba7d6e7 --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go @@ -0,0 +1,240 @@ +/* + 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 ipfs + +import ( + "os" + "path/filepath" + "testing" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSSimple(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImageCIDKey" + const transformedImageCIDKey = "transformedImageCIDKey" + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + // FIXME: requiring a lot more than that - we need a working ipfs daemon + test.Binary("ipfs"), + // We constantly rmi the image by its CID which is shared across tests, so, we make this group private + // and every subtest NoParallel + nerdtest.Private, + ) + + testCase.Env = map[string]string{ + // Point IPFS_PATH to the expected location + "IPFS_PATH": filepath.Join(os.Getenv("HOME"), ".local/share/ipfs"), + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Env: map[string]string{ + "CONTAINERD_SNAPSHOTTER": "stargz", + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with commit and push", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the committed image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "cat", "/hello") + }, + + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with commit and push, stargz lazy pulling", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Env: map[string]string{ + "CONTAINERD_SNAPSHOTTER": "stargz", + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter") + }, + + Expected: test.Expects(0, nil, test.Equals("hello\nsha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with encryption", + NoParallel: true, + Require: test.Binary("openssl"), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Prep a key pair + keyPair := testhelpers.NewJWEKeyPair(t) + // FIXME: this will only cleanup when the group is done, not right, but it works + t.Cleanup(keyPair.Cleanup) + data.Set("pub", keyPair.Pub) + data.Set("prv", keyPair.Prv) + + // Encrypt the image, and verify it is encrypted + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, data.Get(mainImageCIDKey), data.Identifier("encrypted")) + cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Equals("1\n"), + }) + cmd = helpers.Command("image", "inspect", "--mode=native", "--format={{json (index .Manifest.Layers 0) }}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Contains("org.opencontainers.image.enc.keys.jwe"), + }) + + // Push the encrypted image and save the CID + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted"))) + + // Remove both images locally + helpers.Ensure("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Ensure("rmi", "-f", data.Get(transformedImageCIDKey)) + + // Pull back without unpacking + helpers.Ensure("pull", "--unpack=false", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey)) + } + }, + SubTests: []*test.Case{ + { + Description: "decrypt with pub key does not work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "decrypt", "--key="+data.Get("pub"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "decrypt with priv key does work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "decrypt", "--key="+data.Get("prv"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/main_linux_test.go b/cmd/nerdctl/issues/main_linux_test.go similarity index 85% rename from cmd/nerdctl/main_linux_test.go rename to cmd/nerdctl/issues/main_linux_test.go index 274604e27b9..c5fe5455f87 100644 --- a/cmd/nerdctl/main_linux_test.go +++ b/cmd/nerdctl/issues/main_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package issues import ( "testing" @@ -24,6 +24,10 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) +func TestMain(m *testing.M) { + testutil.M(m) +} + // TestIssue108 tests https://github.com/containerd/nerdctl/issues/108 // ("`nerdctl run --net=host -it` fails while `nerdctl run -it --net=host` works") func TestIssue108(t *testing.T) { @@ -34,9 +38,10 @@ func TestIssue108(t *testing.T) { Description: "-it --net=host", Require: test.Binary("unbuffer"), Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers. - Command("run", "-it", "--rm", "--net=host", testutil.AlpineImage, "echo", "this was always working"). - WithWrapper("unbuffer") + cmd := helpers. + Command("run", "-it", "--rm", "--net=host", testutil.AlpineImage, "echo", "this was always working") + cmd.WithWrapper("unbuffer") + return cmd }, // Note: unbuffer will merge stdout and stderr, preventing exact match here Expected: test.Expects(0, nil, test.Contains("this was always working")), @@ -45,9 +50,10 @@ func TestIssue108(t *testing.T) { Description: "--net=host -it", Require: test.Binary("unbuffer"), Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers. - Command("run", "--rm", "--net=host", "-it", testutil.AlpineImage, "echo", "this was not working due to issue #108"). - WithWrapper("unbuffer") + cmd := helpers. + Command("run", "--rm", "--net=host", "-it", testutil.AlpineImage, "echo", "this was not working due to issue #108") + cmd.WithWrapper("unbuffer") + return cmd }, // Note: unbuffer will merge stdout and stderr, preventing exact match here Expected: test.Expects(0, nil, test.Contains("this was not working due to issue #108")), diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 13a68c4900a..8651c3fa0e0 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -21,17 +21,14 @@ package login import ( + "errors" "fmt" - "net" - "os" "strconv" "testing" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) @@ -65,6 +62,12 @@ func (ag *Client) WithConfigPath(value string) *Client { return ag } +func (ag *Client) RunIt(host string) []string { + args := append([]string{"login"}, ag.args...) + return append(args, host) +} + +/* func (ag *Client) GetConfigPath() string { return ag.configPath } @@ -87,84 +90,376 @@ func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { } } -func TestLoginPersistence(t *testing.T) { - base := testutil.NewBase(t) - t.Parallel() +*/ - // Retrieve from the store - testCases := []struct { - auth string - }{ - { - "basic", - }, - { - "token", - }, +func TestFoo(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Registry + + var reg *registry.Server + var token *registry.TokenAuthServer + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var username, password string + username = test.RandomStringBase64(30) + "∞" + password = test.RandomStringBase64(30) + ":∞" + reg, token = nerdtest.RegistryWithTokenAuth(data, helpers, t, username, password, 0, true) + // reg = nerdtest.RegistryWithBasicAuth(data, helpers, t, username, password, 0, false) + + reg.Setup(data, helpers) + token.Setup(data, helpers) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + data.Set("registryHost", reg.IP.String()) + data.Set("registryPort", strconv.Itoa(reg.Port)) + data.WithConfig(nerdtest.HostsDir, test.ConfigValue(reg.HostsDir)) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if reg != nil { + reg.Cleanup(data, helpers) + token.Cleanup(data, helpers) + } + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.Command { + cl := &Client{} + cl.WithCredentials(data.Get("registryUsername"), data.Get("registryPassword")) + // cl.WithInsecure(true) + ex := cl.RunIt(fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + return helpers.Command(ex...) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + // reg.Logs(data, helpers) + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + //token.Logs(data, helpers) + //reg.Logs(data, helpers) + }, + } } + testCase.Run(t) +} - for _, tc := range testCases { - tc := tc - t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) { - t.Parallel() +/* - username := testregistry.SafeRandomString(30) + "∞" - password := testregistry.SafeRandomString(30) + ":∞" +func WithRegistry(data test.Data, helpers test.Helpers, t *testing.T, auth string, port int, tls bool) (string, string, *registry.Server) { + username := test.RandomStringBase64(30) + "∞" + password := test.RandomStringBase64(30) + ":∞" + switch auth { + case "basic": + return username, password, nerdtest.RegistryWithBasicAuth(data, helpers, t, username, password, port, tls) + case "token": + return username, password, nerdtest.RegistryWithTokenAuth(data, helpers, t, username, password, port, tls) + default: + return "", "", nerdtest.RegistryWithNoAuth(data, helpers, t, port, tls) + } +} - // Add the requested authentication - var auth testregistry.Auth - var dependentCleanup func(error) +*/ - auth = &testregistry.NoAuth{} - if tc.auth == "basic" { - auth = &testregistry.BasicAuth{ - Username: username, - Password: password, - } - } else if tc.auth == "token" { - authCa := testca.New(base.T) - as := testregistry.NewAuthServer(base, authCa, 0, username, password, false) - auth = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, - } - dependentCleanup = as.Cleanup - } +type RegistryTestDescriptor struct { + Port int + TLS bool + AuthType string - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) + registry *registry.Server + token *registry.TokenAuthServer + username string + password string + t *testing.T +} - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) +func (rtd *RegistryTestDescriptor) Description() string { + desc := "registry port: " + if rtd.Port == 0 { + desc += "random" + } else { + desc += strconv.Itoa(rtd.Port) + } + desc += " auth: " + rtd.AuthType + desc += " tls: " + strconv.FormatBool(rtd.TLS) + return desc +} - // First, login successfully - c := (&Client{}). - WithCredentials(username, password) +func (rtd *RegistryTestDescriptor) Setup(data test.Data, helpers test.Helpers) { + var username, password string + username = test.RandomStringBase64(30) + "∞" + password = test.RandomStringBase64(30) + ":∞" + switch rtd.AuthType { + case "basic": + rtd.registry = nerdtest.RegistryWithBasicAuth(data, helpers, rtd.t, username, password, rtd.Port, rtd.TLS) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + case "token": + rtd.registry, rtd.token = nerdtest.RegistryWithTokenAuth(data, helpers, rtd.t, username, password, rtd.Port, rtd.TLS) + rtd.token.Setup(data, helpers) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + default: + rtd.registry = nerdtest.RegistryWithNoAuth(data, helpers, rtd.t, rtd.Port, rtd.TLS) + } + rtd.registry.Setup(data, helpers) + data.Set("registryHostsDir", rtd.registry.HostsDir) + data.Set("registryHost", rtd.registry.IP.String()) + data.Set("registryPort", strconv.Itoa(rtd.registry.Port)) +} + +func (rtd *RegistryTestDescriptor) Cleanup(data test.Data, helpers test.Helpers) { + if rtd.registry != nil { + //rtd.registry.Logs(data, helpers) + rtd.registry.Cleanup(data, helpers) + } + if rtd.token != nil { + //rtd.token.Logs(data, helpers) + rtd.token.Cleanup(data, helpers) + } +} - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() +func WithNothing(username string, password string, host string, port string) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // Now, log in successfully without passing any explicit credentials - nc := (&Client{}). - WithConfigPath(c.GetConfigPath()) - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() +func WithHosts(username string, password string, host string, port string, hostsDir string) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--hosts-dir", hostsDir, + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // Now fail while using invalid credentials - nc.WithCredentials("invalid", "invalid"). - Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertFail() +func WithInsecure(username string, password string, host string, port string, insecure bool) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--insecure-registry=" + strconv.FormatBool(insecure), + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // And login again without, reverting to the last saved good state - nc = (&Client{}). - WithConfigPath(c.GetConfigPath()) +func TestLoginPersistence(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Registry + // Use a custom docker config to avoid cross test pollution + testCase.Data = test.WithConfig(nerdtest.DockerConfig, "{}") + testCase.SubTests = []*test.Case{} - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() + testDescriptors := []*RegistryTestDescriptor{ + { + Port: 0, + TLS: true, + AuthType: "token", + t: t, + }, + { + Port: 0, + TLS: true, + AuthType: "basic", + t: t, + }, + } + + for _, testDesc := range testDescriptors { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: testDesc.Description() + "-nothing", + Setup: testDesc.Setup, + Cleanup: testDesc.Cleanup, + SubTests: []*test.Case{ + { + Description: "with hostsdir, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithHosts( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + data.Get("registryHostsDir"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with hostsdir, invalid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithHosts( + "bogus", + "bogus", + data.Get("registryHost"), + data.Get("registryPort"), + data.Get("registryHostsDir"), + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "with insecure, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithInsecure( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + true, + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with insecure, invalid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithInsecure( + "bogus", + "bogus", + data.Get("registryHost"), + data.Get("registryPort"), + true, + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "with nothing, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, []error{errors.New("failed to verify certificate")}, nil), + }, + { + Description: "with nothing, valid credentials, localhost", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, []error{errors.New("failed to verify certificate")}, nil), + }, + /* + { + Description: "no options, ip", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "no options, localhost", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "no options, 127.0.0.1", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + + */ + }, }) } + + testCase.Run(t) +} + +func TestLoginVariants(t *testing.T) { + nerdtest.Setup() + + _ = func(description string, registrySetup func(data test.Data, helpers test.Helpers)) *test.Case { + var registry *testregistry.RegistryServer + + return &test.Case{ + Description: description, + + Setup: registrySetup, + + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + } + }, + + SubTests: []*test.Case{ + { + Description: "", + // Use a custom docker config to avoid cross test pollution + Data: test.WithConfig(nerdtest.DockerConfig, "{}"), + Setup: func(data test.Data, helpers test.Helpers) { + // First, login successfully + helpers.Ensure("login", + "--username", data.Get("registryUsername"), + "--password", data.Get("registryPassword"), + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("login", fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + }, + Expected: test.Expects(0, nil, test.Contains("Login Succeeded")), + }, + { + Description: "", + // Use a custom docker config to avoid cross test pollution + Data: test.WithConfig(nerdtest.DockerConfig, "{}"), + Setup: func(data test.Data, helpers test.Helpers) { + // First, login successfully + helpers.Ensure("login", + "--username", data.Get("registryUsername"), + "--password", data.Get("registryPassword"), + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + + // Fail to login with invalid credentials + helpers.Fail("login", + "--username", "bogus", + "--password", "bogus", + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("login", fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + }, + Expected: test.Expects(0, nil, test.Contains("Login Succeeded")), + }, + }, + } + } + } /* @@ -196,6 +491,7 @@ func TestAgainstNoAuth(t *testing.T) { */ +/* func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication @@ -537,3 +833,6 @@ func TestLoginAgainstVariants(t *testing.T) { }) } } + + +*/ diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index c1e3caf94fd..85c9180478b 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -80,53 +80,51 @@ func TestUnknownCommand(t *testing.T) { // TestNerdctlConfig validates the configuration precedence [CLI, Env, TOML, Default] and broken config rejection func TestNerdctlConfig(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() + + // Docker does not support nerdctl.toml obviously + testCase.Require = test.Not(nerdtest.Docker) - tc := &test.Case{ - Description: "Nerdctl configuration", - // Docker does not support nerdctl.toml obviously - Require: test.Not(nerdtest.Docker), - SubTests: []*test.Case{ - { - Description: "Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Expected: test.Expects(0, nil, test.Equals(defaults.DefaultSnapshotter+"\n")), - }, - { - Description: "TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-toml\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Cli > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Env > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-env\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Cli > Env > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), - Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Broken config", - Command: test.RunCommand("info"), - Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil), - Data: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config + testCase.SubTests = []*test.Case{ + { + Description: "Default", + Command: test.RunCommand("info", "-f", "{{.Driver}}"), + Expected: test.Expects(0, nil, test.Equals(defaults.DefaultSnapshotter+"\n")), + }, + { + Description: "TOML > Default", + Command: test.RunCommand("info", "-f", "{{.Driver}}"), + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-toml\n")), + Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Cli > TOML > Default", + Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), + Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Env > TOML > Default", + Command: test.RunCommand("info", "-f", "{{.Driver}}"), + Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-env\n")), + Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Cli > Env > TOML > Default", + Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), + Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), + Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Broken config", + Command: test.RunCommand("info"), + Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil), + Data: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config version = 2`), - }, }, } - tc.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/main_test_test.go b/cmd/nerdctl/main_test_test.go index a515df48536..e9f2a4496da 100644 --- a/cmd/nerdctl/main_test_test.go +++ b/cmd/nerdctl/main_test_test.go @@ -80,8 +80,7 @@ func TestTest(t *testing.T) { return cmd }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get("first-run") == "" { - data.Set("first-run", "first cleanup") + if data.Get("status") == "uninitialized" { return } if data.Get("status") != "uninitialized-setup-command" { diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index ae590d94836..a5cd79a2052 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -41,14 +41,14 @@ func TestNetworkCreate(t *testing.T) { assert.Equal(t, len(netw.IPAM.Config), 1) data.Set("subnet", netw.IPAM.Config[0].Subnet) - helpers.Ensure("network", "create", data.Identifier()+"-1") + helpers.Ensure("network", "create", data.Identifier("1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()+"-1") + helpers.Anyhow("network", "rm", data.Identifier("1")) }, Command: func(data test.Data, helpers test.Helpers) test.Command { - data.Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier()+"-1", testutil.AlpineImage, "ip", "route")) + data.Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier("1"), testutil.AlpineImage, "ip", "route")) return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.AlpineImage, "ip", "route") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 4aa6147d063..a42516111c8 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -39,7 +39,7 @@ func TestNetworkInspect(t *testing.T) { testGroup := &test.Group{ { - Description: "Test network inspect", + Description: "basic", // IPAMConfig is not implemented on Windows yet Require: test.Not(test.Windows), Setup: func(data test.Data, helpers test.Helpers) { @@ -74,7 +74,7 @@ func TestNetworkInspect(t *testing.T) { }, }, { - Description: "Test network with namespace", + Description: "with namespace", Require: test.Not(nerdtest.Docker), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) @@ -88,23 +88,31 @@ func TestNetworkInspect(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - cmd := helpers.Command().Clear().WithBinary("nerdctl").WithArgs("--namespace", data.Identifier()) + cmd := helpers.CustomCommand("nerdctl", "--namespace", data.Identifier()) - cmd.Clone().WithArgs("network", "inspect", data.Identifier()).Run(&test.Expected{ + com := cmd.Clone() + com.WithArgs("network", "inspect", data.Identifier()) + com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no such network")}, }) - cmd.Clone().WithArgs("network", "remove", data.Identifier()).Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "remove", data.Identifier()) + com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no such network")}, }) - cmd.Clone().WithArgs("network", "ls").Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "ls") + com.Run(&test.Expected{ Output: test.DoesNotContain(data.Identifier()), }) - cmd.Clone().WithArgs("network", "prune", "-f").Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "prune", "-f") + com.Run(&test.Expected{ Output: test.DoesNotContain(data.Identifier()), }) }, diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index a9d2f0124eb..11efae88931 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -27,64 +27,65 @@ import ( ) func TestNetworkLsFilter(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "Test network list", - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("identifier", data.Identifier()) - data.Set("label", data.Identifier()+"=label-1") - data.Set("netID1", helpers.Capture("network", "create", "--label="+data.Get("label"), data.Identifier()+"-1")) - data.Set("netID2", helpers.Capture("network", "create", data.Identifier()+"-2")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()+"-1") - helpers.Anyhow("network", "rm", data.Identifier()+"-2") - }, - SubTests: []*test.Case{ - { - Description: "filter label", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) - netNames := map[string]struct{}{ - data.Get("netID1")[:12]: {}, - } + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Set("identifier", data.Identifier()) + data.Set("label", "mylabel=label-1") + data.Set("net1", data.Identifier("1")) + data.Set("net2", data.Identifier("2")) + data.Set("netID1", helpers.Capture("network", "create", "--label="+data.Get("label"), data.Get("net1"))) + data.Set("netID2", helpers.Capture("network", "create", data.Get("net2"))) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("1")) + helpers.Anyhow("network", "rm", data.Identifier("2")) + } - for _, name := range lines { - _, ok := netNames[name] - assert.Assert(t, ok, info) - } - }, - } - }, + testCase.SubTests = []*test.Case{ + { + Description: "filter label", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, info) + netNames := map[string]struct{}{ + data.Get("netID1")[:12]: {}, + } + + for _, name := range lines { + _, ok := netNames[name] + assert.Assert(t, ok, info) + } + }, + } + }, + }, + { + Description: "filter name", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("net2")) }, - { - Description: "filter name", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("identifier")+"-2") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) - netNames := map[string]struct{}{ - data.Get("netID2")[:12]: {}, - } + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, info) + netNames := map[string]struct{}{ + data.Get("netID2")[:12]: {}, + } - for _, name := range lines { - _, ok := netNames[name] - assert.Assert(t, ok, info) - } - }, - } - }, + for _, name := range lines { + _, ok := netNames[name] + assert.Assert(t, ok, info) + } + }, + } }, }, } diff --git a/cmd/nerdctl/network/network_prune_linux_test.go b/cmd/nerdctl/network/network_prune_linux_test.go index 1692ac55053..c4d969976ab 100644 --- a/cmd/nerdctl/network/network_prune_linux_test.go +++ b/cmd/nerdctl/network/network_prune_linux_test.go @@ -25,12 +25,14 @@ import ( ) func TestNetworkPrune(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.Require = nerdtest.Private + + testCase.SubTests = []*test.Case{ { Description: "Prune does not collect started container network", - Require: nerdtest.Private, + NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) @@ -48,7 +50,7 @@ func TestNetworkPrune(t *testing.T) { }, { Description: "Prune does collect stopped container network", - Require: nerdtest.Private, + NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) @@ -67,5 +69,5 @@ func TestNetworkPrune(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go index 9e5bbaf200a..552c2ca966a 100644 --- a/cmd/nerdctl/network/network_remove_linux_test.go +++ b/cmd/nerdctl/network/network_remove_linux_test.go @@ -29,109 +29,102 @@ import ( ) func TestNetworkRemove(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "TestNetworkRemove", - Require: test.Not(nerdtest.Rootless), - SubTests: []*test.Case{ - { - Description: "Simple network remove", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + testCase.Require = nerdtest.RootFul + + testCase.SubTests = []*test.Case{ + { + Description: "Simple network remove", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } + }, + }, + { + Description: "Network remove when linked to container", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: test.Expects(1, []error{errors.New("is in use")}, nil), + }, + { + Description: "Network remove by id", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "rm", data.Get("netID")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } + }, + }, + { + Description: "Network remove by short id", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") }, - { - Description: "Network remove when linked to container", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - Errors: []error{errors.New("is in use")}, - } - }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("network", "rm", data.Get("netID")[:12]) }, - { - Description: "Network remove by id", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Get("netID")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) }, - { - Description: "Network remove by short id", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Get("netID")[:12]) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } }, }, } diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go index 3c8f5c252da..bc91e6ffbce 100644 --- a/cmd/nerdctl/system/system_info_test.go +++ b/cmd/nerdctl/system/system_info_test.go @@ -55,7 +55,7 @@ func TestInfo(t *testing.T) { Description: "info with namespace", Require: test.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("info") + return helpers.CustomCommand("nerdctl", "info") }, Expected: test.Expects(0, nil, test.Contains("Namespace: default")), }, @@ -66,7 +66,7 @@ func TestInfo(t *testing.T) { }, Require: test.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("info") + return helpers.CustomCommand("nerdctl", "info") }, Expected: test.Expects(0, nil, test.Contains("Namespace: test")), }, diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go index 40d08f1beea..7665ccad3be 100644 --- a/cmd/nerdctl/system/system_prune_linux_test.go +++ b/cmd/nerdctl/system/system_prune_linux_test.go @@ -23,16 +23,17 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestSystemPrune(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.NoParallel = true + + testCase.SubTests = []*test.Case{ { Description: "volume prune all success", // Private because of prune evidently @@ -83,24 +84,11 @@ func TestSystemPrune(t *testing.T) { helpers.Ensure("system", "prune", "-f", "--volumes", "--all") }, Command: func(data test.Data, helpers test.Helpers) test.Command { - buildctlBinary, err := buildkitutil.BuildctlBinary() - if err != nil { - t.Fatal(err) - } - - host, err := buildkitutil.GetBuildkitHost(testutil.Namespace) - if err != nil { - t.Fatal(err) - } - - buildctlArgs := buildkitutil.BuildctlBaseArgs(host) - buildctlArgs = append(buildctlArgs, "du") - - return helpers.CustomCommand(buildctlBinary, buildctlArgs...) + return nerdtest.BuildCtlCommand(data, helpers, "du") }, Expected: test.Expects(0, nil, test.Contains("Total:\t\t0B")), }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_create_test.go b/cmd/nerdctl/volume/volume_create_test.go index 767f7ac12be..90d592a14c2 100644 --- a/cmd/nerdctl/volume/volume_create_test.go +++ b/cmd/nerdctl/volume/volume_create_test.go @@ -29,7 +29,7 @@ import ( func TestVolumeCreate(t *testing.T) { nerdtest.Setup() - tg := &test.Group{ + testGroup := &test.Group{ { Description: "arg missing should create anonymous volume", Command: test.RunCommand("volume", "create"), @@ -82,13 +82,8 @@ func TestVolumeCreate(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - // NOTE: docker returns 125 on this - ExitCode: -1, - Errors: []error{errdefs.ErrInvalidArgument}, - } - }, + // NOTE: docker returns 125 on this + Expected: test.Expects(-1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "creating already existing volume should succeed", @@ -109,5 +104,5 @@ func TestVolumeCreate(t *testing.T) { }, } - tg.Run(t) + testGroup.Run(t) } diff --git a/cmd/nerdctl/volume/volume_inspect_test.go b/cmd/nerdctl/volume/volume_inspect_test.go index edee98be906..f7b2219a5b0 100644 --- a/cmd/nerdctl/volume/volume_inspect_test.go +++ b/cmd/nerdctl/volume/volume_inspect_test.go @@ -42,162 +42,170 @@ func createFileWithSize(mountPoint string, size int64) error { } func TestVolumeInspect(t *testing.T) { - nerdtest.Setup() - var size int64 = 1028 - tc := &test.Case{ - Description: "Volume inspect", - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("volprefix", data.Identifier()) - helpers.Ensure("volume", "create", data.Identifier()) - helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier()+"-second") - // Obviously note here that if inspect code gets totally hosed, this entire suite will - // probably fail right here on the Setup instead of actually testing something - vol := nerdtest.InspectVolume(helpers, data.Identifier()) - err := createFileWithSize(vol.Mountpoint, size) - assert.NilError(t, err, "File creation failed") + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ + "which is not always true. To be replaced by cp into the container.", + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (bool, string) { + isDocker, _ := nerdtest.Docker.Check(data, helpers, t) + return !isDocker || test.IsRoot(), "docker cli needs to be run as root" + }, + }) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("volume", "create", data.Identifier("first")) + helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier("second")) + // Obviously note here that if inspect code gets totally hosed, this entire suite will + // probably fail right here on the Setup instead of actually testing something + vol := nerdtest.InspectVolume(helpers, data.Identifier("first")) + err := createFileWithSize(vol.Mountpoint, size) + assert.NilError(t, err, "File creation failed") + data.Set("vol1", data.Identifier("first")) + data.Set("vol2", data.Identifier("second")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Get("vol1")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol2")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "arg missing should fail", + Command: test.RunCommand("volume", "inspect"), + Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-second") + { + Description: "invalid identifier should fail", + Command: test.RunCommand("volume", "inspect", "∞"), + Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, - - SubTests: []*test.Case{ - { - Description: "arg missing should fail", - Command: test.RunCommand("volume", "inspect"), - Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), + { + Description: "non existent volume should fail", + Command: test.RunCommand("volume", "inspect", "doesnotexist"), + Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), + }, + { + Description: "success", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", data.Get("vol1")) }, - { - Description: "invalid identifier should fail", - Command: test.RunCommand("volume", "inspect", "∞"), - Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)+info) + assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info) + }, + ), + } + }, + }, + { + Description: "inspect labels", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", data.Get("vol2")) }, - { - Description: "non existent volume should fail", - Command: test.RunCommand("volume", "inspect", "doesnotexist"), - Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol2")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + labels := *dc[0].Labels + assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) + assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) + assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) + }, + ), + } }, - { - Description: "success", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)+info) - assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info) - }, - ), - } - }, + }, + { + Description: "inspect size", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", "--size", data.Get("vol1")) }, - { - Description: "inspect labels", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix")+"-second") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - labels := *dc[0].Labels - assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) - assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) - assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) - }, - ), - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) + }, + ), + } }, - { - Description: "inspect size", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", "--size", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) - }, - ), - } - }, + }, + { + Description: "multi success", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", data.Get("vol1"), data.Get("vol2")) }, - { - Description: "multi success", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix"), data.Get("volprefix")+"-second") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - test.Contains(data.Get("volprefix")+"-second"), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)) - assert.Assert(t, dc[1].Name == data.Get("volprefix")+"-second", fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix")+"-second", dc[1].Name)) - }, - ), - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + test.Contains(data.Get("vol2")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)) + assert.Assert(t, dc[1].Name == data.Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol2"), dc[1].Name)) + }, + ), + } }, - { - Description: "part success multi", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)) - }, - ), - } - }, + }, + { + Description: "part success multi", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("vol1")) }, - { - Description: "multi failure", - Command: test.RunCommand("volume", "inspect", "invalid∞", "nonexistent"), - Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)) + }, + ), + } }, }, + { + Description: "multi failure", + Command: test.RunCommand("volume", "inspect", "invalid∞", "nonexistent"), + Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil), + }, } - tc.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index d0ad6d78463..2d09c45caae 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -35,17 +35,22 @@ func TestVolumeLsSize(t *testing.T) { Description: "Volume ls --size", Require: test.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-1") - helpers.Ensure("volume", "create", data.Identifier()+"-2") - helpers.Ensure("volume", "create", data.Identifier()+"-empty") - vol1 := nerdtest.InspectVolume(helpers, data.Identifier()+"-1") - vol2 := nerdtest.InspectVolume(helpers, data.Identifier()+"-2") + helpers.Ensure("volume", "create", data.Identifier("1")) + helpers.Ensure("volume", "create", data.Identifier("2")) + helpers.Ensure("volume", "create", data.Identifier("empty")) + vol1 := nerdtest.InspectVolume(helpers, data.Identifier("1")) + vol2 := nerdtest.InspectVolume(helpers, data.Identifier("2")) err := createFileWithSize(vol1.Mountpoint, 102400) assert.NilError(t, err, "File creation failed") err = createFileWithSize(vol2.Mountpoint, 204800) assert.NilError(t, err, "File creation failed") }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Identifier("1")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("2")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("empty")) + }, Command: test.RunCommand("volume", "ls", "--size"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -53,9 +58,9 @@ func TestVolumeLsSize(t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) volSizes := map[string]string{ - data.Identifier() + "-1": "100.0 KiB", - data.Identifier() + "-2": "200.0 KiB", - data.Identifier() + "-empty": "0.0 B", + data.Identifier("1"): "100.0 KiB", + data.Identifier("2"): "200.0 KiB", + data.Identifier("empty"): "0.0 B", } var numMatches = 0 @@ -77,316 +82,320 @@ func TestVolumeLsSize(t *testing.T) { }, } }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-1") - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-2") - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-empty") - }, } tc.Run(t) } func TestVolumeLsFilter(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tc := &test.Case{ - Description: "Volume ls", - Setup: func(data test.Data, helpers test.Helpers) { - var vol1, vol2, vol3, vol4 = data.Identifier() + "-1", data.Identifier() + "-2", data.Identifier() + "-3", data.Identifier() + "-4" - var label1, label2, label3, label4 = data.Identifier() + "=label-1", data.Identifier() + "=label-2", data.Identifier() + "=label-3", data.Identifier() + "-group=label-4" + testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ + "which is not always true. To be replaced by cp into the container.", + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (bool, string) { + isDocker, _ := nerdtest.Docker.Check(data, helpers, t) + return !isDocker || test.IsRoot(), "docker cli needs to be run as root" + }, + }) - helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1) - helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2) - helpers.Ensure("volume", "create", "--label="+label3, vol3) - helpers.Ensure("volume", "create", vol4) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var vol1, vol2, vol3, vol4 = data.Identifier("1"), data.Identifier("2"), data.Identifier("3"), data.Identifier("4") + var label1, label2, label3, label4 = "mylabel=label-1", "mylabel=label-2", "mylabel=label-3", "mylabel-group=label-4" - err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000) - assert.NilError(t, err, "File creation failed") + helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1) + helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2) + helpers.Ensure("volume", "create", "--label="+label3, vol3) + helpers.Ensure("volume", "create", vol4) + + // FIXME + // This will not work with Docker rootful and Docker cli run as a user + // We should replace it with cp inside the container + err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000) + assert.NilError(t, err, "File creation failed") - data.Set("vol1", vol1) - data.Set("vol2", vol2) - data.Set("vol3", vol3) - data.Set("vol4", vol4) - data.Set("mainlabel", data.Identifier()) - data.Set("label1", label1) - data.Set("label2", label2) - data.Set("label3", label3) - data.Set("label4", label4) + data.Set("vol1", vol1) + data.Set("vol2", vol2) + data.Set("vol3", vol3) + data.Set("vol4", vol4) + data.Set("mainlabel", "mylabel") + data.Set("label1", label1) + data.Set("label2", label2) + data.Set("label3", label3) + data.Set("label4", label4) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Get("vol1")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol2")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol3")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol4")) + } + testCase.SubTests = []*test.Case{ + { + Description: "No filter", + Command: test.RunCommand("volume", "ls", "--quiet"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + data.Get("vol3"): {}, + data.Get("vol4"): {}, + } + var numMatches = 0 + for _, name := range lines { + _, ok := volNames[name] + if !ok { + continue + } + numMatches++ + } + assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches)) + }, + } + }, }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Get("vol1")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol2")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol3")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol4")) + { + Description: "Retrieving label=mainlabel", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + data.Get("vol3"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, }, - SubTests: []*test.Case{ - { - Description: "No filter", - Command: test.RunCommand("volume", "ls", "--quiet"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - data.Get("vol3"): {}, - data.Get("vol4"): {}, - } - var numMatches = 0 - for _, name := range lines { - _, ok := volNames[name] - if !ok { - continue - } - numMatches++ - } - assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches)) - }, - } - }, + { + Description: "Retrieving label=mainlabel=label2", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label2")) }, - { - Description: "Retrieving label=mainlabel", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - data.Get("vol3"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving label=mainlabel=label2", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel=", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")+"=") }, - { - Description: "Retrieving label=mainlabel=", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")+"=") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + }, + } }, - { - Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label1"), "--filter", "label="+data.Get("label2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label1"), "--filter", "label="+data.Get("label2")) }, - { - Description: "Retrieving label=mainlabel and label=grouplabel=label4", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"), "--filter", "label="+data.Get("label4")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + }, + } }, - { - Description: "Retrieving name=volume1", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel and label=grouplabel=label4", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"), "--filter", "label="+data.Get("label4")) }, - { - Description: "Retrieving name=volume1 and name=volume2", - // FIXME: https://github.com/containerd/nerdctl/issues/3452 - // Nerdctl filter behavior is broken - Require: nerdtest.Docker, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"), "--filter", "name="+data.Get("vol2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size=1024000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - data.Get("vol4"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving name=volume1", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, + }, + { + Description: "Retrieving name=volume1 and name=volume2", + // Nerdctl filter behavior is broken + Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3452"), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"), "--filter", "name="+data.Get("vol2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, + }, + { + Description: "Retrieving size=1024000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + data.Get("vol4"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size>=1024000 size<=2048000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - data.Get("vol4"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving size>=1024000 size<=2048000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + data.Get("vol4"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size>204800 size<1024000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol3"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving size>204800 size<1024000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol3"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, }, } - tc.Run(t) + + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go index b20f64984dc..3ea36f387e6 100644 --- a/cmd/nerdctl/volume/volume_namespace_test.go +++ b/cmd/nerdctl/volume/volume_namespace_test.go @@ -26,71 +26,81 @@ import ( ) func TestVolumeNamespace(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tg := &test.Case{ - Description: "Namespaces", - Require: test.Not(nerdtest.Docker), - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("root_namespace", data.Identifier()) - data.Set("root_volume", data.Identifier()) - helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier()) - }, - SubTests: []*test.Case{ - { - Description: "inspect another namespace volume should fail", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("root_volume")) - }, - Expected: test.Expects(1, []error{ - errdefs.ErrNotFound, - }, nil), - }, - { - Description: "removing another namespace volume should fail", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "remove", data.Get("root_volume")) - }, - Expected: test.Expects(1, []error{ - errdefs.ErrNotFound, - }, nil), + // Docker does not support namespaces + testCase.Require = test.Not(nerdtest.Docker) + + // Create a volume in a different namespace + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Set("root_namespace", data.Identifier()) + data.Set("root_volume", data.Identifier()) + helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier()) + } + + // Cleanup once done + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Get("root_namespace") != "" { + helpers.Anyhow("--namespace", data.Identifier(), "volume", "remove", data.Identifier()) + helpers.Anyhow("namespace", "remove", data.Identifier()) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "inspect another namespace volume should fail", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "inspect", data.Get("root_volume")) }, - { - Description: "prune should leave another namespace volume untouched", - NoParallel: true, - Command: test.RunCommand("volume", "prune", "-a", "-f"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.DoesNotContain(data.Get("root_volume")), - func(stdout string, info string, t *testing.T) { - helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) - }, - ), - } - }, + Expected: test.Expects(1, []error{ + errdefs.ErrNotFound, + }, nil), + }, + { + Description: "removing another namespace volume should fail", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "remove", data.Get("root_volume")) }, - { - Description: "create with the same name should work, then delete it", - NoParallel: true, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "create", data.Get("root_volume")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", data.Get("root_volume")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - helpers.Ensure("volume", "inspect", data.Get("root_volume")) - helpers.Ensure("volume", "rm", data.Get("root_volume")) + Expected: test.Expects(1, []error{ + errdefs.ErrNotFound, + }, nil), + }, + { + Description: "prune should leave another namespace volume untouched", + // Make it private so that we do not interact with other tests in the main namespace + Require: nerdtest.Private, + Command: test.RunCommand("volume", "prune", "-a", "-f"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("root_volume")), + func(stdout string, info string, t *testing.T) { helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) }, - } - }, + ), + } + }, + }, + { + Description: "create with the same name should work, then delete it", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "create", data.Get("root_volume")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", data.Get("root_volume")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Ensure("volume", "inspect", data.Get("root_volume")) + helpers.Ensure("volume", "rm", data.Get("root_volume")) + helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) + }, + } }, }, } - tg.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go index 8898ad30ab4..5f59007167d 100644 --- a/cmd/nerdctl/volume/volume_prune_linux_test.go +++ b/cmd/nerdctl/volume/volume_prune_linux_test.go @@ -25,21 +25,19 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestVolumePrune(t *testing.T) { - nerdtest.Setup() - +func XXXTestVolumePrune(t *testing.T) { var setup = func(data test.Data, helpers test.Helpers) { anonIDBusy := strings.TrimSpace(helpers.Capture("volume", "create")) anonIDDangling := strings.TrimSpace(helpers.Capture("volume", "create")) - namedBusy := data.Identifier() + "-busy" - namedDangling := data.Identifier() + "-free" + namedBusy := data.Identifier("busy") + namedDangling := data.Identifier("free") helpers.Ensure("volume", "create", namedBusy) helpers.Ensure("volume", "create", namedDangling) helpers.Ensure("run", "--name", data.Identifier(), - "-v", namedBusy+":/whatever", - "-v", anonIDBusy+":/other", testutil.CommonImage) + "-v", namedBusy+":/namedbusyvolume", + "-v", anonIDBusy+":/anonbusyvolume", testutil.CommonImage) data.Set("anonIDBusy", anonIDBusy) data.Set("anonIDDangling", anonIDDangling) @@ -49,20 +47,23 @@ func TestVolumePrune(t *testing.T) { var cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("rm", "-f", data.Get("anonIDBusy")) - helpers.Anyhow("rm", "-f", data.Get("anonIDDangling")) - helpers.Anyhow("rm", "-f", data.Get("namedBusy")) - helpers.Anyhow("rm", "-f", data.Get("namedDangling")) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDDangling")) + helpers.Anyhow("volume", "rm", "-f", data.Get("namedBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Get("namedDangling")) } + testCase := nerdtest.Setup() // This set must be marked as private, since we cannot prune without interacting with other tests. - testGroup := &test.Group{ + testCase.Require = nerdtest.Private + // Furthermore, these two subtests cannot be run in parallel + testCase.SubTests = []*test.Case{ { Description: "prune anonymous only", - Require: nerdtest.Private, - Command: test.RunCommand("volume", "prune", "-f"), + NoParallel: true, Setup: setup, Cleanup: cleanup, + Command: test.RunCommand("volume", "prune", "-f"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -82,10 +83,10 @@ func TestVolumePrune(t *testing.T) { }, { Description: "prune all", - Require: nerdtest.Private, - Command: test.RunCommand("volume", "prune", "-f", "--all"), + NoParallel: true, Setup: setup, Cleanup: cleanup, + Command: test.RunCommand("volume", "prune", "-f", "--all"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -105,5 +106,5 @@ func TestVolumePrune(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_remove_linux_test.go b/cmd/nerdctl/volume/volume_remove_linux_test.go index ba775614766..08053887eaf 100644 --- a/cmd/nerdctl/volume/volume_remove_linux_test.go +++ b/cmd/nerdctl/volume/volume_remove_linux_test.go @@ -53,6 +53,7 @@ func TestVolumeRemove(t *testing.T) { Command: test.RunCommand("volume", "rm", "doesnotexist"), Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), }, + { Description: "busy volume should fail", @@ -62,22 +63,22 @@ func TestVolumeRemove(t *testing.T) { "--name", data.Identifier(), testutil.CommonImage) }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { return helpers.Command("volume", "rm", data.Identifier()) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), - - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - }, }, { Description: "busy anonymous volume should fail", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("run", "-v", "/volume", "--name", data.Identifier(), testutil.CommonImage) // Inspect the container and find the anonymous volume id inspect := nerdtest.InspectContainer(helpers, data.Identifier()) var anonName string @@ -87,20 +88,21 @@ func TestVolumeRemove(t *testing.T) { break } } - assert.Assert(t, anonName != "", "Failed to find anonymous volume id") + assert.Assert(t, anonName != "", "Failed to find anonymous volume id", inspect) data.Set("anonName", anonName) }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonName")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { // Try to remove that anon volume return helpers.Command("volume", "rm", data.Get("anonName")) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), - - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, }, { Description: "freed volume should succeed", @@ -111,6 +113,11 @@ func TestVolumeRemove(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { return helpers.Command("volume", "rm", data.Identifier()) }, @@ -120,19 +127,10 @@ func TestVolumeRemove(t *testing.T) { Output: test.Equals(data.Identifier() + "\n"), } }, - - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - }, }, { Description: "dangling volume should succeed", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", data.Identifier()) - }, - Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) }, @@ -141,6 +139,10 @@ func TestVolumeRemove(t *testing.T) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.Equals(data.Identifier() + "\n"), @@ -150,20 +152,20 @@ func TestVolumeRemove(t *testing.T) { { Description: "part success multi-remove", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier()+"-busy", data.Identifier()) - }, - Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) - helpers.Ensure("volume", "create", data.Identifier()+"-busy") - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()+"-busy"), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("volume", "create", data.Identifier("busy")) + helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-busy") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) + }, + + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy"), data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -181,22 +183,22 @@ func TestVolumeRemove(t *testing.T) { { Description: "success multi-remove", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", data.Identifier()+"-1", data.Identifier()+"-2") - }, - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-1") - helpers.Ensure("volume", "create", data.Identifier()+"-2") + helpers.Ensure("volume", "create", data.Identifier("1")) + helpers.Ensure("volume", "create", data.Identifier("2")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-1", data.Identifier()+"-2") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("1"), data.Identifier("2")) + }, + + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("volume", "rm", data.Identifier("1"), data.Identifier("2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: test.Equals(data.Identifier() + "-1\n" + data.Identifier() + "-2" + "\n"), + Output: test.Equals(data.Identifier("1") + "\n" + data.Identifier("2") + "\n"), } }, }, @@ -204,17 +206,17 @@ func TestVolumeRemove(t *testing.T) { Description: "failing multi-remove", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-busy") - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()+"-busy"), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("volume", "create", data.Identifier("busy")) + helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-busy") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) }, Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier()+"-busy") + return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy")) }, Expected: test.Expects(1, []error{ diff --git a/docs/testing/tools.md b/docs/testing/tools.md index 655306dc48b..d5857e64860 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -5,7 +5,7 @@ The integration test suite in nerdctl is meant to apply to both nerdctl and docker, and further support additional test properties to target specific contexts (ipv6, kube). -Basic _usage_ is covered in the [testing docs](testing.md). +Basic _usage_ is covered in the [testing docs](README.md). This here covers how to write tests, leveraging nerdctl `pkg/testutil/test` which has been specifically developed to take care of repetitive tasks, @@ -28,17 +28,12 @@ import ( ) func TestMyThing(t *testing.T) { - nerdtest.Setup() - // Declare your test - myTest := &test.Case{ - Description: "A first test", - // This is going to run `nerdctl info` (or `docker info`) - Command: test.RunCommand("info"), - // Verify the command exits with 0, and stdout contains the word `Kernel` - Expected: test.Expects(0, nil, test.Contains("Kernel")), - } - + myTest := nerdtest.Setup() + // This is going to run `nerdctl info` (or `docker info`) + myTest.Command = test.RunCommand("info") + // Verify the command exits with 0, and stdout contains the word `Kernel` + myTest.Expected = test.Expects(0, nil, test.Contains("Kernel")) // Run it myTest.Run(t) } @@ -53,7 +48,7 @@ You already saw two (`test.Expects` and `test.Contains`): First, `test.Expects(exitCode int, errors []error, outputCompare Comparator)`, which is convenient to quickly describe what you expect overall. -`exitCode` is obvious. +`exitCode` is obvious (note that passing -1 as an exit code will just verify the commands does fail without comparing the code). `errors` is a slice of go `error`, that allows you to compare what is seen on stderr with existing errors (for example: `errdefs.ErrNotFound`), or more generally @@ -69,6 +64,7 @@ Secondly, `test.Contains`, is a `Comparator`. Besides `test.Contains(string)`, there are a few more: - `test.DoesNotContain(string)` - `test.Equals(string)` +- `test.Match(*regexp.Regexp)` - `test.All(comparators ...Comparator)`, which allows you to bundle together a bunch of other comparators The following example shows how to implement your own custom `Comparator` @@ -166,9 +162,6 @@ Note that `Data` additionally exposes the following functions: Secondly, `Data` allows defining and manipulating "configuration" data. In the case of nerdctl here, the following configuration options are defined: -- `WithConfig(Docker, NotCompatible)` to flag a test as not compatible -- `WithConfig(Mode, Private)` will entirely isolate the test using a different -namespace, data root, nerdctl config, etc - `WithConfig(NerdctlToml, "foo")` which allows specifying a custom config - `WithConfig(DataRoot, "foo")` allowing to point to a custom data-root - `WithConfig(HostsDir, "foo")` to point to a specific hosts directory @@ -253,12 +246,20 @@ func TestMyThing(t *testing.T) { } ``` +Note that custom commands also unlock access to advanced properties for specific cases. +Specifically: +- `Background(timeout time.Duration)` which allows you to background a command execution +- `WithWrapper(binary string, args ...string)` which allows you to "wrap" your command with another binary +- `WithStdin(io.Reader)` which allows you to pass a reader to the command stdin +- `Clone()` which returns a copy of the command, with env, cwd, etc +- `Clear()` which returns a copy of the command, with env, cwd, etc, but without a binary or args + ### On `helpers` Inside a custom `Executor`, `Manager`, or `Butler`, you have access to a collection of `helpers` to simplify command execution: -- `helpers.Ensure(args ...string)` will run a command and ensure it exits succesfully +- `helpers.Ensure(args ...string)` will run a command and ensure it exits successfully - `helpers.Fail(args ...string)` will run a command and ensure it fails - `helpers.Anyhow(args ...string)` will run a command but does not care if it succeeds or fails - `helpers.Capture(args ...string)` will run a command, ensure it is successful, and return the output @@ -334,9 +335,9 @@ func TestMyThing(t *testing.T) { Subtests are just regular tests, attached to the `SubTests` slice of a test. -Note that a subtest will inherit its parent `Data` and `Env`, in the state they are at +Note that a subtest will inherit its parent `Data`, `Config` and `Env`, in the state they are at after the parent test has run its `Setup` and `Command` routines (but before `Cleanup`). -This does _not_ apply to `Identifier()` and `TempDir()`, which are unique to the sub-test. +This does _not_ apply to `Identifier()` and `TempDir()`, which are unique to the subtest. Also note that a test does not have to have a `Command`. This is a convenient pattern if you just need a common `Setup` for a bunch of subtests. @@ -348,6 +349,8 @@ A `test.Group` is just a convenient way to represent a slice of tests. Note that unlike a `test.Case`, a group cannot define properties inherited by subtests, nor `Setup` or `Cleanup` routines. +Also note that a group ALWAYS run in parallel with other tests at the same level. + - if you just have a bunch of subtests you want to run, put them in a `test.Group` - if you want to have a global setup, or otherwise set a common property first for your subtests, use a `test.Case` with `SubTests` @@ -358,7 +361,56 @@ All tests (and subtests) are assumed to be parallelizable. You can force a specific `test.Case` to not be run in parallel though, by setting its `NoParallel` property to `true`. -Note that if you want better isolation, it is usually better to use -`WithConfig(nerdtest.Mode, nerdtest.Private)` instead. -This will keep the test parallel (for nerdctl), but isolate it in a different context. -For Docker (which does not support namespaces), it is equivalent to passing `NoParallel: true`. +Note that if you want better isolation, it is usually better to use the requirement +`nerdtest.Private` instead of `NoParallel` (see below). + +## Requirements + +`test.Case` has a `Require` property that allow enforcing specific, per-test requirements. + +Here are a few: +```go +test.Windows // a test runs only on Windows (or Not(Windows)) +test.Linux // a test runs only on Linux +test.Darwin // a test runs only on Darwin +test.OS(name string) // a test runs only on the OS `name` +test.Binary(name string) // a test requires the bin `name` to be in the PATH +test.Not(req Requirement) // a test runs only if the opposite of the requirement `req` is fulfilled +test.Require(req ...Requirement) // a test runs only if all requirements are fulfilled + +nerdtest.Docker // a test only run on Docker - normally used with test.Not(nerdtest.Docker) +nerdtest.Soci // a test requires the soci snapshotter +nerdtest.RootLess // a test requires Rootless (or Not(Rootless), indicating it requires Rootful) +nerdtest.RootFul // a test requires Rootless (or Not(Rootless), indicating it requires Rootful) +nerdtest.Build // a test requires buildkit +nerdtest.CGroup // a test requires cgroup +nerdtest.OnlyIPv6 // a test is meant to run solely in the ipv6 environment +nerdtest.NerdctlNeedsFixing // indicates that a test cannot be run on nerdctl yet as a fix is required +nerdtest.BrokenTest // indicates that a test needs to be fixed and has been restricted to run only in certain cases +nerdtest.Private // see below +``` + +### About `nerdtest.Private` + +While all requirements above are self-descriptive or obvious, and are going to skip +tests for environments that do not match the requirements, `nerdtest.Private` is a +special case. + +What it does when required is: create a private namespace, data-root, hosts-dir, nerdctl.toml and +DOCKER_CONFIG that is private to the test. + +Note that subtests are going to inherit that environment as well. + +If the target is Docker - which does not support namespaces for eg - asking for `private` +will merely disable parallelization. + +The purpose of private is to provide a truly clean-room environment for tests +that are guaranteed to have side effects on others, or that do require an exclusive, pristine +environment. + +Using private is generally preferable to disabling parallelization, as doing the latter +would slow down the run and won't have the same isolation guarantees about the environment. + +## Utilities + +TBD \ No newline at end of file diff --git a/go.mod b/go.mod index fe7331f2ce1..4f62f06342c 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/fluent/fluent-logger-golang v1.9.0 github.com/fsnotify/fsnotify v1.7.0 github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/go-yaml/yaml v2.1.0+incompatible github.com/ipfs/go-cid v0.4.1 github.com/klauspost/compress v1.17.10 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index 69738d2f271..b4fe99837fd 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/pkg/testutil/nerdtest/ca/ca.go b/pkg/testutil/nerdtest/ca/ca.go new file mode 100644 index 00000000000..da367d464e1 --- /dev/null +++ b/pkg/testutil/nerdtest/ca/ca.go @@ -0,0 +1,162 @@ +/* + 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 ca + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +type CA struct { + KeyPath string + CertPath string + + t *testing.T + key *rsa.PrivateKey + cert *x509.Certificate + closeF func() error +} + +func (ca *CA) Close() error { + return ca.closeF() +} + +const keyLength = 4096 + +func New(data test.Data, t *testing.T) *CA { + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assert.NilError(t, err) + + cert := &x509.Certificate{ + SerialNumber: serialNumber(t), + Subject: pkix.Name{ + Organization: []string{"nerdctl test organization"}, + CommonName: fmt.Sprintf("nerdctl CA (%s)", t.Name()), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + dir, err := os.MkdirTemp(data.TempDir(), "ca") + assert.NilError(t, err) + keyPath := filepath.Join(dir, "ca.key") + certPath := filepath.Join(dir, "ca.cert") + writePair(t, keyPath, certPath, cert, cert, key, key) + + return &CA{ + KeyPath: keyPath, + CertPath: certPath, + t: t, + key: key, + cert: cert, + closeF: func() error { + return os.RemoveAll(dir) + }, + } +} + +type Cert struct { + KeyPath string + CertPath string + closeF func() error +} + +func (c *Cert) Close() error { + return c.closeF() +} + +func (ca *CA) NewCert(host string, additional ...string) *Cert { + t := ca.t + + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assert.NilError(t, err) + + additional = append([]string{host}, additional...) + + cert := &x509.Certificate{ + SerialNumber: serialNumber(t), + Subject: pkix.Name{ + Organization: []string{"nerdctl test organization"}, + CommonName: fmt.Sprintf("nerdctl %s (%s)", host, t.Name()), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: additional, + } + for _, h := range additional { + if ip := net.ParseIP(h); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } + } + + dir, err := os.MkdirTemp(t.TempDir(), "cert") + assert.NilError(t, err) + certPath := filepath.Join(dir, "a.cert") + keyPath := filepath.Join(dir, "a.key") + writePair(t, keyPath, certPath, cert, ca.cert, key, ca.key) + + return &Cert{ + CertPath: certPath, + KeyPath: keyPath, + closeF: func() error { + return os.RemoveAll(dir) + }, + } +} + +func writePair(t *testing.T, keyPath, certPath string, cert, caCert *x509.Certificate, key, caKey *rsa.PrivateKey) { + keyF, err := os.Create(keyPath) + assert.NilError(t, err) + defer keyF.Close() + assert.NilError(t, pem.Encode(keyF, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) + assert.NilError(t, keyF.Close()) + + certB, err := x509.CreateCertificate(rand.Reader, cert, caCert, &key.PublicKey, caKey) + assert.NilError(t, err) + certF, err := os.Create(certPath) + assert.NilError(t, err) + defer certF.Close() + assert.NilError(t, pem.Encode(certF, &pem.Block{Type: "CERTIFICATE", Bytes: certB})) + assert.NilError(t, certF.Close()) +} + +func serialNumber(t *testing.T) *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 60) + sn, err := rand.Int(rand.Reader, serialNumberLimit) + assert.NilError(t, err) + return sn +} diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go new file mode 100644 index 00000000000..95dfdf2be54 --- /dev/null +++ b/pkg/testutil/nerdtest/command.go @@ -0,0 +1,118 @@ +/* + 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 ( + "errors" + "os" + "path/filepath" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +type nerdCommand struct { + test.GenericCommand + + DockerConfig string + Namespace string + NerdctlToml string + HostsDir string + DataRoot string + Debug bool +} + +// Run does override the generic command run, as we are testing both docker and nerdctl +func (nc *nerdCommand) Run(expect *test.Expected) { + if nc.T() != nil { + nc.T().Helper() + } + + // If no DOCKER_CONFIG was explicitly provided, set ourselves inside the current working directory + // Note that subtests do then inherit parent test config by default, unless overridden + if nc.Env["DOCKER_CONFIG"] == "" { + nc.Env["DOCKER_CONFIG"] = nc.TempDir() + } + + if nc.DockerConfig != "" { + dest := filepath.Join(nc.Env["DOCKER_CONFIG"], "config.json") + if _, err := os.Stat(dest); errors.Is(err, os.ErrNotExist) { + err := os.WriteFile(dest, []byte(nc.DockerConfig), 0400) + if nc.GenericCommand.T() != nil { + assert.NilError(nc.T(), err, "failed to write custom docker config json file for test") + } + } + } + + // We are not in the business of testing docker *error* output, so, spay expectation here (for errors) + if GetTarget() != targetNerdctl { + if expect != nil { + expect.Errors = nil + } + + if nc.Debug { + nc.PrependArgs("--log-level=debug") + } + } else { + // Set the namespace + if nc.Namespace != "" { + nc.PrependArgs("--namespace=" + nc.Namespace) + } + + // If no NERDCTL_TOML was explicitly provided, set it to the private dir + if nc.Env["NERDCTL_TOML"] == "" { + nc.Env["NERDCTL_TOML"] = filepath.Join(nc.TempDir(), "nerdctl.toml") + } + + // If we have custom toml content, write it if it does not exist already + if nc.NerdctlToml != "" { + dest := nc.Env["NERDCTL_TOML"] + if _, err := os.Stat(dest); errors.Is(err, os.ErrNotExist) { + err := os.WriteFile(dest, []byte(nc.NerdctlToml), 0400) + if nc.GenericCommand.T() != nil { + assert.NilError(nc.GenericCommand.T(), err, "failed to write NerdctlToml") + } + } + } + + if nc.HostsDir != "" { + nc.PrependArgs("--hosts-dir=" + nc.HostsDir) + } + + if nc.DataRoot != "" { + nc.PrependArgs("--data-root=" + nc.DataRoot) + } + + if nc.Debug { + nc.PrependArgs("--debug-full") + } + } + + nc.GenericCommand.Run(expect) +} + +func (nc *nerdCommand) Clone() test.Command { + return &nerdCommand{ + GenericCommand: *((nc.GenericCommand.Clone()).(*test.GenericCommand)), + Namespace: nc.Namespace, + NerdctlToml: nc.NerdctlToml, + HostsDir: nc.HostsDir, + DataRoot: nc.DataRoot, + Debug: nc.Debug, + } +} diff --git a/pkg/testutil/nerdtest/helpers.go b/pkg/testutil/nerdtest/helpers.go new file mode 100644 index 00000000000..31dea761c9a --- /dev/null +++ b/pkg/testutil/nerdtest/helpers.go @@ -0,0 +1,125 @@ +/* + 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 ( + "encoding/json" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +const defaultNamespace = testutil.Namespace + +// FIXME: unexport most of the following? +type target = string + +const ( + targetNerdctl = target("nerdctl") + TargetDocker = target("docker") +) + +func GetTarget() string { + // Indirecting to testutil for now + return testutil.GetTarget() +} + +// InspectContainer is a helper that can be used inside custom commands or Setup +func InspectContainer(helpers test.Helpers, name string) dockercompat.Container { + var dc []dockercompat.Container + cmd := helpers.Command("container", "inspect", name) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + }, + }) + return dc[0] +} + +func InspectVolume(helpers test.Helpers, name string) native.Volume { + var dc []native.Volume + cmd := helpers.Command("volume", "inspect", name) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + }, + }) + return dc[0] +} + +func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { + var dc []dockercompat.Network + cmd := helpers.Command("network", "inspect", name) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + }, + }) + return dc[0] +} + +func InspectImage(helpers test.Helpers, name string) dockercompat.Image { + var dc []dockercompat.Image + cmd := helpers.Command("image", "inspect", name) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + }, + }) + return dc[0] +} + +const ( + maxRetry = 5 + sleep = time.Second +) + +func EnsureContainerStarted(helpers test.Helpers, con string) { + for i := 0; i < maxRetry; i++ { + count := i + cmd := helpers.Command("container", "inspect", con) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + if dc[0].State.Running { + return + } + if count == maxRetry-1 { + t.Fatalf("container %s still not running after %d retries", con, count) + } + time.Sleep(sleep) + }, + }) + } +} diff --git a/pkg/testutil/nerdtest/hoststoml/hoststoml.go b/pkg/testutil/nerdtest/hoststoml/hoststoml.go new file mode 100644 index 00000000000..f6ae48b1247 --- /dev/null +++ b/pkg/testutil/nerdtest/hoststoml/hoststoml.go @@ -0,0 +1,67 @@ +/* + 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 hoststoml + +import ( + "net" + "os" + "path/filepath" + "strconv" + + "github.com/pelletier/go-toml/v2" +) + +type hostsTomlHost struct { + CA string `toml:"ca,omitempty"` + SkipVerify bool `toml:"skip_verify,omitempty"` + Client [][]string `toml:"client,omitempty"` +} + +// See https://github.com/containerd/containerd/blob/main/docs/hosts.md +type HostsToml struct { + CA string `toml:"ca,omitempty"` + SkipVerify bool `toml:"skip_verify,omitempty"` + Client [][]string `toml:"client,omitempty"` + Headers map[string]string `toml:"header,omitempty"` + Server string `toml:"server,omitempty"` + Endpoints map[string]*hostsTomlHost `toml:"host,omitempty"` +} + +func (ht *HostsToml) Save(dir string, hostIP string, port int) error { + var err error + var r *os.File + + hostSubDir := hostIP + if port != 0 { + hostSubDir = net.JoinHostPort(hostIP, strconv.Itoa(port)) + } + + hostsSubDir := filepath.Join(dir, hostSubDir) + err = os.MkdirAll(hostsSubDir, 0700) + if err != nil { + return err + } + + if r, err = os.Create(filepath.Join(dir, hostSubDir, "hosts.toml")); err == nil { + defer r.Close() + enc := toml.NewEncoder(r) + enc.SetIndentTables(true) + err = enc.Encode(ht) + } + + return err +} diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go new file mode 100644 index 00000000000..d00508292c2 --- /dev/null +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -0,0 +1,246 @@ +/* + 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 registry + +import ( + "encoding/json" + "fmt" + "net" + "os" + "strconv" + "testing" + "time" + + "github.com/go-yaml/yaml" + "golang.org/x/crypto/bcrypt" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +type CesantaConfigServer struct { + Addr string `yaml:"addr,omitempty"` + Certificate string + Key string +} + +type CesantaConfigToken struct { + Issuer string `yaml:"issuer,omitempty"` + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + Expiration int `yaml:"expiration,omitempty"` +} + +type CesantaConfigUser struct { + Password string `yaml:"password,omitempty"` +} + +type CesantaMatchConditions struct { + Account string `yaml:"account,omitempty"` +} + +type CesantaConfigACLEntry struct { + Match CesantaMatchConditions `yaml:"match"` + Actions []string `yaml:"actions,flow"` +} + +type CesantaConfigACL []CesantaConfigACLEntry + +type CesantaConfig struct { + Server CesantaConfigServer `yaml:"server"` + Token CesantaConfigToken `yaml:"token"` + Users map[string]CesantaConfigUser `yaml:"users,omitempty"` + ACL CesantaConfigACL `yaml:"acl,omitempty"` +} + +func (cc *CesantaConfig) Save(path string) error { + var err error + var r *os.File + if r, err = os.Create(path); err == nil { + defer r.Close() + err = yaml.NewEncoder(r).Encode(cc) + } + return err +} + +func ensureContainerStarted(helpers test.Helpers, con string) { + const maxRetry = 5 + const sleep = time.Second + success := false + for i := 0; i < maxRetry && !success; i++ { + time.Sleep(sleep) + count := i + cmd := helpers.Command("container", "inspect", con) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + if dc[0].State.Running { + success = true + return + } + if count == maxRetry-1 { + // FIXME: there is currently no simple way to capture stderr + // Sometimes, it is convenient for debugging, like here + // Here we cheat with unbuffer which will bundle stderr and stdout together + // This is just bad + com := helpers.Command("logs", con) + com.WithWrapper("unbuffer") + com.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + t.Log(stdout) + }, + }) + t.Fatalf("container %s still not running after %d retries", con, count) + } + }, + }) + } +} + +func NewCesantaAuthServer(data test.Data, helpers test.Helpers, t *testing.T, ca *ca.CA, port int, user, pass string, tls bool) *TokenAuthServer { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(t, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + bpass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + assert.NilError(t, err, fmt.Errorf("failed bcrypt encrypting password: %w", err)) + // Prepare configuration file for authentication server + // Details: https://github.com/cesanta/docker_auth/blob/1.7.1/examples/simple.yml + configFile, err := os.CreateTemp(data.TempDir(), "authconfig") + assert.NilError(t, err, fmt.Errorf("failed creating temporary directory for config file: %w", err)) + configFileName := configFile.Name() + + cc := &CesantaConfig{ + Server: CesantaConfigServer{ + Addr: ":5100", + }, + Token: CesantaConfigToken{ + Issuer: "Cesanta auth server", + Expiration: 900, + }, + Users: map[string]CesantaConfigUser{ + user: { + Password: string(bpass), + }, + }, + ACL: CesantaConfigACL{ + { + Match: CesantaMatchConditions{ + Account: user, + }, + Actions: []string{"*"}, + }, + }, + } + + scheme := "http" + if tls { + scheme = "https" + cc.Server.Certificate = "/auth/domain.crt" + cc.Server.Key = "/auth/domain.key" + } else { + cc.Token.Certificate = "/auth/domain.crt" + cc.Token.Key = "/auth/domain.key" + } + + err = cc.Save(configFileName) + assert.NilError(t, err, fmt.Errorf("failed writing configuration: %w", err)) + + cert := ca.NewCert(hostIP.String()) + // FIXME: this will fail in many circumstances. Review strategy on how to acquire a free port. + // We probably have better code for that already somewhere. + port, err = portlock.Acquire(port) + assert.NilError(t, err, fmt.Errorf("failed acquiring port: %w", err)) + containerName := data.Identifier(fmt.Sprintf("cesanta-auth-server-%d-%t", port, tls)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + errCertClose := cert.Close() + errConfigClose := configFile.Close() + errConfigRemove := os.Remove(configFileName) + if errPortRelease != nil { + t.Error(errPortRelease.Error()) + } + if errCertClose != nil { + t.Error(errCertClose.Error()) + } + if errConfigClose != nil { + t.Error(errConfigClose.Error()) + } + if errConfigRemove != nil { + t.Error(errConfigRemove.Error()) + } + } + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure( + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:5100", listenIP, port), + "--name", containerName, + "-v", cert.CertPath+":/auth/domain.crt", + "-v", cert.KeyPath+":/auth/domain.key", + "-v", configFileName+":/config/auth_config.yml", + testutil.DockerAuthImage, + "/config/auth_config.yml", + ) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/auth", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 10, + true) + assert.NilError(t, err, fmt.Errorf("failed starting auth container in a timely manner: %w", err)) + + } + + return &TokenAuthServer{ + IP: hostIP, + Port: port, + Scheme: scheme, + CertPath: cert.CertPath, + Auth: &TokenAuth{ + Address: scheme + "://" + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + CertPath: cert.CertPath, + }, + Setup: setup, + Cleanup: cleanup, + Logs: func(data test.Data, helpers test.Helpers) { + // FIXME: get rid of unbuffer and allow manipulating stderr + cmd := helpers.Command("logs", containerName) + cmd.WithWrapper("unbuffer") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + t.Logf("%s: %q", containerName, stdout) + }, + }) + }, + } +} diff --git a/pkg/testutil/nerdtest/registry/common.go b/pkg/testutil/nerdtest/registry/common.go new file mode 100644 index 00000000000..0f7496b049a --- /dev/null +++ b/pkg/testutil/nerdtest/registry/common.go @@ -0,0 +1,108 @@ +/* + 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 registry + +import ( + "fmt" + "net" + "os" + "path/filepath" + + "golang.org/x/crypto/bcrypt" + + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +// Auth describes a struct able to serialize authenticator information into arguments to be fed to a registry container run +type Auth interface { + Params(data test.Data) []string +} + +type NoAuth struct { +} + +func (na *NoAuth) Params(data test.Data) []string { + return []string{} +} + +type TokenAuth struct { + Address string + CertPath string +} + +// FIXME: this is specific to Docker Registry +// Like need something else for Harbor and Gitlab +func (ta *TokenAuth) Params(data test.Data) []string { + return []string{ + "--env", "REGISTRY_AUTH=token", + "--env", "REGISTRY_AUTH_TOKEN_REALM=" + ta.Address + "/auth", + "--env", "REGISTRY_AUTH_TOKEN_SERVICE=Docker registry", + "--env", "REGISTRY_AUTH_TOKEN_ISSUER=Cesanta auth server", + "--env", "REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/auth/domain.crt", + "-v", ta.CertPath + ":/auth/domain.crt", + } +} + +type BasicAuth struct { + Realm string + HtFile string + Username string + Password string +} + +func (ba *BasicAuth) Params(data test.Data) []string { + if ba.Realm == "" { + ba.Realm = "Basic Realm" + } + if ba.HtFile == "" && ba.Username != "" && ba.Password != "" { + pass := ba.Password + encryptedPass, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + tmpDir, _ := os.MkdirTemp(data.TempDir(), "htpasswd") + ba.HtFile = filepath.Join(tmpDir, "htpasswd") + _ = os.WriteFile(ba.HtFile, []byte(fmt.Sprintf(`%s:%s`, ba.Username, string(encryptedPass[:]))), 0600) + } + ret := []string{ + "--env", "REGISTRY_AUTH=htpasswd", + "--env", "REGISTRY_AUTH_HTPASSWD_REALM=" + ba.Realm, + "--env", "REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd", + } + if ba.HtFile != "" { + ret = append(ret, "-v", ba.HtFile+":/htpasswd") + } + return ret +} + +type TokenAuthServer struct { + Scheme string + IP net.IP + Port int + CertPath string + Cleanup func(data test.Data, helpers test.Helpers) + Setup func(data test.Data, helpers test.Helpers) + Logs func(data test.Data, helpers test.Helpers) + Auth Auth +} + +type Server struct { + Scheme string + IP net.IP + Port int + Cleanup func(data test.Data, helpers test.Helpers) + Setup func(data test.Data, helpers test.Helpers) + Logs func(data test.Data, helpers test.Helpers) + HostsDir string // contains ":/hosts.toml" +} diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go new file mode 100644 index 00000000000..1cecefde899 --- /dev/null +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -0,0 +1,162 @@ +/* + 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 registry + +import ( + "fmt" + "net" + "os" + "strconv" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func NewDockerRegistry(data test.Data, helpers test.Helpers, t *testing.T, currentCA *ca.CA, port int, auth Auth) *Server { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(t, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + // XXX RELEASE PORT IN CLEANUP HERE + // FIXME: this will fail in many circumstances. Review strategy on how to acquire a free port. + // We probably have better code for that already somewhere. + port, err = portlock.Acquire(port) + assert.NilError(t, err, fmt.Errorf("failed acquiring port: %w", err)) + + containerName := data.Identifier(fmt.Sprintf("docker-registry-server-%d-%t", port, currentCA != nil)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + args := []string{ + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:5000", listenIP, port), + "--name", containerName, + } + scheme := "http" + var cert *ca.Cert + if currentCA != nil { + scheme = "https" + cert = currentCA.NewCert(hostIP.String(), "127.0.0.1", "localhost", "::1") + args = append(args, + "--env", "REGISTRY_HTTP_TLS_CERTIFICATE=/registry/domain.crt", + "--env", "REGISTRY_HTTP_TLS_KEY=/registry/domain.key", + "-v", cert.CertPath+":/registry/domain.crt", + "-v", cert.KeyPath+":/registry/domain.key", + ) + } + + // Attach authentication params returns by authenticator + args = append(args, auth.Params(data)...) + + // Get the right registry version + registryImage := testutil.RegistryImageStable + up := os.Getenv("DISTRIBUTION_VERSION") + if up != "" { + if up[0:1] != "v" { + up = "v" + up + } + registryImage = testutil.RegistryImageNext + up + } + args = append(args, registryImage) + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + + if cert != nil { + assert.NilError(t, cert.Close(), fmt.Errorf("failed cleaning certificates: %w", err)) + } + + assert.NilError(t, errPortRelease, fmt.Errorf("failed releasing port: %w", err)) + } + + // FIXME: in the future, we will want to further manipulate hosts toml file from the test + // This should then return the struct, instead of saving it on its own + hostsDir, err := func() (string, error) { + hDir, err := os.MkdirTemp(t.TempDir(), "certs.d") + assert.NilError(t, err, fmt.Errorf("failed creating directory certs.d: %w", err)) + + if currentCA != nil { + hostTomlContent := &hoststoml.HostsToml{ + CA: currentCA.CertPath, + } + + err = hostTomlContent.Save(hDir, hostIP.String(), port) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "127.0.0.1", port) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "localhost", port) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + if port == 443 { + err = hostTomlContent.Save(hDir, hostIP.String(), 0) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "127.0.0.1", 0) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "localhost", 0) + assert.NilError(t, err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + } + } + + return hDir, nil + }() + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure(args...) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/v2", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 10, + true) + assert.NilError(t, err, fmt.Errorf("failed starting docker registry in a timely manner: %w", err)) + } + + return &Server{ + Scheme: scheme, + IP: hostIP, + Port: port, + Cleanup: cleanup, + Setup: setup, + Logs: func(data test.Data, helpers test.Helpers) { + // FIXME: get rid of unbuffer and allow manipulating stderr + cmd := helpers.Command("logs", containerName) + cmd.WithWrapper("unbuffer") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + t.Logf("%s: %q", containerName, stdout) + }, + }) + }, + HostsDir: hostsDir, + } +} diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go new file mode 100644 index 00000000000..3f9c7fc49bf --- /dev/null +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -0,0 +1,96 @@ +/* + 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 registry + +import ( + "fmt" + "net" + "strconv" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, currentCA *ca.CA, port int, auth Auth) *Server { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(t, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + port, err = portlock.Acquire(port) + assert.NilError(t, err, fmt.Errorf("failed acquiring port: %w", err)) + + containerName := data.Identifier(fmt.Sprintf("kubo-registry-server-%d-%t", port, currentCA != nil)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + args := []string{ + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:%d", listenIP, port, port), + "--name", containerName, + "--entrypoint=/bin/sh", + testutil.KuboImage, + "-c", "--", + fmt.Sprintf("ipfs init && ipfs config Addresses.API /ip4/0.0.0.0/tcp/%d && ipfs daemon --offline", port), + } + + scheme := "http" + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + + assert.NilError(t, errPortRelease, fmt.Errorf("failed releasing port: %w", err)) + } + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure(args...) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/api/v0", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 10, + true) + assert.NilError(t, err, fmt.Errorf("failed starting kubo registry in a timely manner: %w", err)) + } + + return &Server{ + IP: hostIP, + Port: port, + Scheme: scheme, + Cleanup: cleanup, + Setup: setup, + Logs: func(data test.Data, helpers test.Helpers) { + // FIXME: get rid of unbuffer and allow manipulating stderr + cmd := helpers.Command("logs", containerName) + cmd.WithWrapper("unbuffer") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + t.Logf("%s: %q", containerName, stdout) + }, + }) + }, + } +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go new file mode 100644 index 00000000000..3f96ba86c83 --- /dev/null +++ b/pkg/testutil/nerdtest/requirements.go @@ -0,0 +1,286 @@ +/* + 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 ( + "encoding/json" + "fmt" + "os" + "os/exec" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +var ipv6 test.ConfigKey = "IPv6Test" +var kubernetes test.ConfigKey = "KubeTest" +var only test.ConfigValue = "Only" +var mode test.ConfigKey = "Mode" +var modePrivate test.ConfigValue = "Private" + +func environmentHasIPv6() bool { + return testutil.GetEnableIPv6() +} + +func environmentHasKubernetes() bool { + return testutil.GetEnableKubernetes() +} + +// OnlyIPv6 marks a test as suitable to be run exclusively inside an ipv6 environment +var OnlyIPv6 = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (ret bool, mess string) { + ret = environmentHasIPv6() + if !ret { + mess = "runner skips IPv6 compatible tests in the non-IPv6 environment" + } + data.WithConfig(ipv6, only) + return ret, mess + }, +} + +var OnlyKubernetes = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (ret bool, mess string) { + ret = environmentHasKubernetes() + if !ret { + mess = "runner skips Kubernetes compatible tests in the Kubernetes environment" + } + data.WithConfig(kubernetes, only) + return ret, mess + }, +} + +// Docker marks a test as suitable solely for Docker and not Nerdctl +// Generally used as test.Not(nerdtest.Docker), which of course it the opposite +var Docker = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (ret bool, mess string) { + ret = GetTarget() == TargetDocker + 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, t *testing.T) (ret bool, mess string) { + ret = GetTarget() == TargetDocker + if ret { + mess = "current target is docker" + } else { + mess = "current target is nerdctl, but we will skip as it is currently broken: " + 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, t *testing.T) (bool, string) { + ret, mess := req.Check(data, helpers, t) + 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, t *testing.T) (ret bool, mess string) { + // Make sure we DO not return "IsRootless true" for docker + ret = GetTarget() == targetNerdctl && 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 = test.Not(RootLess) + +// CGroup requires that cgroup is enabled +var CGroup = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (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(t, err, "failed to parse docker info") + switch dinf.CgroupDriver { + case "none", "": + ret = false + mess = "cgroup is none" + } + return ret, mess + }, +} + +// Soci requires that the soci snapshotter is enabled +var Soci = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (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(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, t *testing.T) (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(t, err, "failed to parse docker info") + for _, p := range dinf.Plugins.Storage { + if p == "stargz" { + ret = true + mess = "stargz is enabled" + } + } + return ret, mess + }, +} + +// Registry marks a test as requiring a registry to be deployed +var Registry = test.Require( + // Registry requires Linux currently + test.Linux, + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers, t *testing.T) (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 := testutil.RegistryImageStable + up := os.Getenv("DISTRIBUTION_VERSION") + if up != "" { + if up[0:1] != "v" { + up = "v" + up + } + registryImage = testutil.RegistryImageNext + up + } + helpers.Ensure("pull", "--quiet", registryImage) + helpers.Ensure("pull", "--quiet", testutil.DockerAuthImage) + helpers.Ensure("pull", "--quiet", testutil.KuboImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + // XXX FIXME: figure out what to do with reg setup/cleanup routines + }, + }, +) + +var BuildkitHost = test.SystemKey("bkHost") + +// 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, t *testing.T) (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 GetTarget() == targetNerdctl { + // NOTE: we might not have the final namespace... as Private may be processed later + ns, _ := data.Surface(test.SystemKey(Namespace)) + if ns == "" { + ns = defaultNamespace + } + _, err := buildkitutil.GetBuildkitHost(string(ns)) + 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 + } + } + return ret, mess + }, + Setup: func(data test.Data, helpers test.Helpers) { + ns, _ := data.Surface(test.SystemKey(Namespace)) + if ns == "" { + ns = defaultNamespace + } + bkHostAddr, _ := buildkitutil.GetBuildkitHost(string(ns)) + data.Sink(BuildkitHost, test.SystemValue(bkHostAddr)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + }, +} + +// 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, t *testing.T) (ret bool, mess string) { + // FIXME: is this necessary? + data.WithConfig(mode, modePrivate) + // That should be enough + namespace := data.Identifier("private") + data.WithConfig(Namespace, test.ConfigValue(namespace)) + // So... this happens too late + return true, "private mode creates a dedicated namespace for nerdctl, and disable parallelism for docker" + }, + Setup: func(data test.Data, helpers test.Helpers) { + // SHOULD NoParallel be subsumed into regular config? + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + containerList := helpers.Capture("ps", "-aq") + helpers.Ensure("rm", "-f", containerList) + helpers.Ensure("system", "prune", "-f", "--all", "--volumes") + // FIXME: there are conditions where we still have some stuff in there + helpers.Anyhow("namespace", "remove", string(data.ReadConfig(Namespace))) + }, +} diff --git a/pkg/testutil/nerdtest/test.go b/pkg/testutil/nerdtest/test.go index 812ff08591d..33730ccc51d 100644 --- a/pkg/testutil/nerdtest/test.go +++ b/pkg/testutil/nerdtest/test.go @@ -17,251 +17,112 @@ package nerdtest import ( - "encoding/json" - "fmt" - "os" - "path/filepath" + "os/exec" "testing" - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" - "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" - "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func Setup() { - test.CustomCommand(nerdctlSetup) +func Setup() *test.Case { + test.CustomCommand(&nerdctlSetup{}) + return &test.Case{ + Description: "root", + } } -// Nerdctl specific config key and values +var DockerConfig test.ConfigKey = "DockerConfig" +var Namespace test.ConfigKey = "Namespace" var NerdctlToml test.ConfigKey = "NerdctlToml" var HostsDir test.ConfigKey = "HostsDir" var DataRoot test.ConfigKey = "DataRoot" -var Namespace test.ConfigKey = "Namespace" - -var Mode test.ConfigKey = "Mode" -var ModePrivate test.ConfigValue = "Private" -var IPv6 test.ConfigKey = "IPv6Test" -var Only test.ConfigValue = "Only" - -var OnlyIPv6 = test.MakeRequirement(func(data test.Data) (ret bool, mess string) { - ret = testutil.GetEnableIPv6() - if !ret { - mess = "runner skips IPv6 compatible tests in the non-IPv6 environment" - } - data.WithConfig(IPv6, Only) - return ret, mess -}) - -var Private = test.MakeRequirement(func(data test.Data) (ret bool, mess string) { - data.WithConfig(Mode, ModePrivate) - return true, "" -}) - -var Docker = test.MakeRequirement(func(data test.Data) (ret bool, mess string) { - ret = testutil.GetTarget() == testutil.Docker - if ret { - mess = "current target is docker" - } else { - mess = "current target is not docker" - } - return ret, mess -}) +var Debug test.ConfigKey = "Debug" -var Rootless = test.MakeRequirement(func(data test.Data) (ret bool, mess string) { - ret = rootlessutil.IsRootless() - if ret { - mess = "environment is rootless" - } else { - mess = "environment is rootful" - } - return ret, mess -}) +type nerdctlSetup struct { +} -var Build = test.MakeRequirement(func(data test.Data) (ret bool, mess 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 = "" - if testutil.GetTarget() == testutil.Nerdctl { - _, err := buildkitutil.GetBuildkitHost(testutil.Namespace) +func (ns *nerdctlSetup) OnInitialize(testCase *test.Case, t *testing.T) test.Command { + var err error + var binary string + trgt := GetTarget() + switch trgt { + case targetNerdctl: + binary, err = exec.LookPath(trgt) if err != nil { - ret = false - mess = fmt.Sprintf("test requires buildkitd: %+v", err) + t.Fatalf("unable to find binary %q: %v", trgt, err) } + case TargetDocker: + binary, err = exec.LookPath(trgt) + if err != nil { + t.Fatalf("unable to find binary %q: %v", trgt, err) + } + if err = exec.Command("docker", "compose", "version").Run(); err != nil { + t.Fatalf("docker does not support compose: %v", err) + } + default: + t.Fatalf("unknown target %q", GetTarget()) } - return ret, mess -}) -type NerdCommand struct { - test.GenericCommand - // FIXME: annoying - forces custom Clone, etc - Target string -} + baseCommand := &nerdCommand{} + baseCommand.WithBinary(binary) + baseCommand.WithTempDir(testCase.Data.TempDir()) + baseCommand.WithEnv(testCase.Env) + baseCommand.WithT(t) -// Run does override the generic command run, as we are testing both docker and nerdctl -func (nc *NerdCommand) Run(expect *test.Expected) { - // We are not in the business of testing docker error output, so, spay expect for errors testing, if any - if expect != nil && nc.Target != testutil.Nerdctl { - expect.Errors = nil + baseCommand.EnvBlackList = []string{ + "LS_COLORS", + "DOCKER_CONFIG", + "CONTAINERD_SNAPSHOTTER", + "NERDCTL_TOML", + "CONTAINERD_ADDRESS", + "CNI_PATH", + "NETCONFPATH", + "NERDCTL_EXPERIMENTAL", + "NERDCTL_HOST_GATEWAY_IP", } - nc.GenericCommand.Run(expect) + return baseCommand } -// Clone is overridden as well, as we need to pass along the target -func (nc *NerdCommand) Clone() test.Command { - return &NerdCommand{ - GenericCommand: *((nc.GenericCommand.Clone()).(*test.GenericCommand)), - Target: nc.Target, +func (ns *nerdctlSetup) OnPostRequirements(testCase *test.Case, t *testing.T, com test.Command) { + // Ambient requirements, bail out now if these do not match + if environmentHasIPv6() && testCase.Data.ReadConfig(ipv6) != only { + t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") } -} - -// InspectContainer is a helper that can be used inside custom commands or Setup -func InspectContainer(helpers test.Helpers, name string) dockercompat.Container { - var dc []dockercompat.Container - cmd := helpers.Command("container", "inspect", name) - cmd.Run(&test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - }, - }) - return dc[0] -} -func InspectVolume(helpers test.Helpers, name string, args ...string) native.Volume { - var dc []native.Volume - cmdArgs := append([]string{"volume", "inspect"}, args...) - cmdArgs = append(cmdArgs, name) - - cmd := helpers.Command(cmdArgs...) - cmd.Run(&test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - }, - }) - return dc[0] -} - -func InspectNetwork(helpers test.Helpers, name string, args ...string) dockercompat.Network { - var dc []dockercompat.Network - cmdArgs := append([]string{"network", "inspect"}, args...) - cmdArgs = append(cmdArgs, name) - - cmd := helpers.Command(cmdArgs...) - cmd.Run(&test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - }, - }) - return dc[0] -} - -func InspectImage(helpers test.Helpers, name string) dockercompat.Image { - var dc []dockercompat.Image - cmd := helpers.Command("image", "inspect", name) - cmd.Run(&test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - err := json.Unmarshal([]byte(stdout), &dc) - assert.NilError(t, err, "Unable to unmarshal output\n"+info) - assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) - }, - }) - return dc[0] -} - -func nerdctlSetup(testCase *test.Case, t *testing.T) test.Command { - t.Helper() - - var testUtilBase *testutil.Base - dt := testCase.Data - var pvNamespace string - inherited := false - - if dt.ReadConfig(IPv6) != Only && testutil.GetEnableIPv6() { + if environmentHasKubernetes() && testCase.Data.ReadConfig(kubernetes) != only { t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") } - if dt.ReadConfig(Mode) == ModePrivate { - // If private was inherited, we already got a configured namespace - if dt.ReadConfig(Namespace) != "" { - pvNamespace = string(dt.ReadConfig(Namespace)) - inherited = true - } else { - // Otherwise, we need to set everything up - pvNamespace = testCase.Data.Identifier() - dt.WithConfig(Namespace, test.ConfigValue(pvNamespace)) - testCase.Env["DOCKER_CONFIG"] = testCase.Data.TempDir() - testCase.Env["NERDCTL_TOML"] = filepath.Join(testCase.Data.TempDir(), "nerdctl.toml") - dt.WithConfig(HostsDir, test.ConfigValue(testCase.Data.TempDir())) - dt.WithConfig(DataRoot, test.ConfigValue(testCase.Data.TempDir())) - } - testUtilBase = testutil.NewBaseWithNamespace(t, pvNamespace) - if testUtilBase.Target == testutil.Docker { - // For docker, just disable parallel + data := testCase.Data + if data.ReadConfig(mode) == modePrivate { + // For docker, we do disable parallel since there is no namespace where we can isolate + if GetTarget() == TargetDocker { testCase.NoParallel = true } - } else if dt.ReadConfig(Namespace) != "" { - pvNamespace = string(dt.ReadConfig(Namespace)) - testUtilBase = testutil.NewBaseWithNamespace(t, pvNamespace) - } else { - testUtilBase = testutil.NewBase(t) - } - - // If we were passed custom content for NerdctlToml, save it - // Not happening if this is not nerdctl of course - if testUtilBase.Target == testutil.Nerdctl && dt.ReadConfig(NerdctlToml) != "" { - dest := filepath.Join(testCase.Data.TempDir(), "nerdctl.toml") - testCase.Env["NERDCTL_TOML"] = dest - err := os.WriteFile(dest, []byte(dt.ReadConfig(NerdctlToml)), 0400) - assert.NilError(t, err, "failed to write custom nerdctl toml file for test") } - // Build the base - baseCommand := &NerdCommand{} - baseCommand.WithBinary(testUtilBase.Binary) - baseCommand.WithArgs(testUtilBase.Args...) - baseCommand.WithEnv(testCase.Env) - baseCommand.WithT(t) - baseCommand.WithTempDir(testCase.Data.TempDir()) - baseCommand.Target = testUtilBase.Target - - if testUtilBase.Target == testutil.Nerdctl { - if dt.ReadConfig(HostsDir) != "" { - baseCommand.GenericCommand.WithArgs("--hosts-dir=" + string(dt.ReadConfig(HostsDir))) - } - - if dt.ReadConfig(DataRoot) != "" { - baseCommand.GenericCommand.WithArgs("--data-root=" + string(dt.ReadConfig(DataRoot))) - } + // Map the config in + cc := com.(*nerdCommand) + cc.DockerConfig = string(data.ReadConfig(DockerConfig)) + cc.Namespace = string(data.ReadConfig(Namespace)) + if cc.Namespace == "" { + cc.Namespace = defaultNamespace } - - // If we were in a custom namespace, not inherited - make sure we clean up the namespace - // FIXME: this is broken, and custom namespaces are not cleaned properly - if testUtilBase.Target == testutil.Nerdctl && pvNamespace != "" && !inherited { - cleanup := func() { - cl := baseCommand.Clone() - cl.WithArgs("namespace", "remove", pvNamespace) - cl.Run(nil) - } - cleanup() - t.Cleanup(cleanup) + cc.NerdctlToml = string(data.ReadConfig(NerdctlToml)) + cc.HostsDir = string(data.ReadConfig(HostsDir)) + cc.DataRoot = string(data.ReadConfig(DataRoot)) + if data.ReadConfig(Debug) != "" { + cc.Debug = true } + // Save the namespace information into system - some tests do want it + data.Sink(test.SystemKey(Namespace), test.SystemValue(cc.Namespace)) +} - // Attach the base command - return baseCommand +func (ns *nerdctlSetup) OnPostSetup(testCase *test.Case, t *testing.T, com test.Command) { + // Some setup routines MAY alter config (specifically HostsDir, NerdctlToml, DataRoot) + data := testCase.Data + cc := com.(*nerdCommand) + cc.NerdctlToml = string(data.ReadConfig(NerdctlToml)) + cc.HostsDir = string(data.ReadConfig(HostsDir)) + cc.DataRoot = string(data.ReadConfig(DataRoot)) } diff --git a/pkg/testutil/nerdtest/third-party.go b/pkg/testutil/nerdtest/third-party.go new file mode 100644 index 00000000000..e22151d0937 --- /dev/null +++ b/pkg/testutil/nerdtest/third-party.go @@ -0,0 +1,74 @@ +/* + 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 ( + "os/exec" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func BuildCtlCommand(data test.Data, helpers test.Helpers, args ...string) test.Command { + // As tests with build Require Build, we already know we have buildctl in the path + buildctl, _ := exec.LookPath("buildctl") + bh, _ := data.Surface(BuildkitHost) + cmd := helpers.CustomCommand(buildctl, "--addr="+string(bh)) + cmd.WithArgs(args...) + return cmd +} + +func RegistryWithTokenAuth(data test.Data, helpers test.Helpers, t *testing.T, user, pass string, port int, tls bool) (*registry.Server, *registry.TokenAuthServer) { + rca := ca.New(data, t) + as := registry.NewCesantaAuthServer(data, helpers, t, rca, 0, user, pass, tls) + re := registry.NewDockerRegistry(data, helpers, t, rca, port, as.Auth) + return re, as +} + +func RegistryWithNoAuth(data test.Data, helpers test.Helpers, t *testing.T, port int, tls bool) *registry.Server { + var rca *ca.CA + if tls { + rca = ca.New(data, t) + } + return registry.NewDockerRegistry(data, helpers, t, rca, port, ®istry.NoAuth{}) +} + +func RegistryWithBasicAuth(data test.Data, helpers test.Helpers, t *testing.T, user, pass string, port int, tls bool) *registry.Server { + auth := ®istry.BasicAuth{ + Username: user, + Password: pass, + } + var rca *ca.CA + if tls { + rca = ca.New(data, t) + } + return registry.NewDockerRegistry(data, helpers, t, rca, port, auth) +} + +/* + if r, err := os.Open(tomlPath); err == nil { + log.L.Debugf("Loading config from %q", tomlPath) + defer r.Close() + dec := toml.NewDecoder(r).DisallowUnknownFields() // set Strict to detect typo + if err := dec.Decode(cfg); err != nil { + return nil, fmt.Errorf("failed to load nerdctl config (not daemon config) from %q (Hint: don't mix up daemon's `config.toml` with `nerdctl.toml`): %w", tomlPath, err) + } + log.L.Debugf("Loaded config %+v", cfg) + +*/ diff --git a/pkg/testutil/test/case.go b/pkg/testutil/test/case.go index eed35a929f6..1a890386dc3 100644 --- a/pkg/testutil/test/case.go +++ b/pkg/testutil/test/case.go @@ -22,13 +22,14 @@ import ( "gotest.tools/v3/assert" ) -// Group informally describes a slice of tests +// Group informally describes a slice of tests to be run in parallel type Group []*Case func (tg *Group) Run(t *testing.T) { t.Helper() // If the group contains only one test, no need to create a subtest sub := len(*tg) > 1 + // If we do have subtests, the root test is marked parallel if sub { t.Parallel() } @@ -60,7 +61,7 @@ type Case struct { // Cleanup Cleanup Butler // Requirement - Require Requirement + Require *Requirement // SubTests SubTests []*Case @@ -78,98 +79,128 @@ type Case struct { func (test *Case) Run(t *testing.T) { t.Helper() // Run the test - testRun := func(tt *testing.T) { - tt.Helper() - test.seal(tt) + testRun := func(subT *testing.T) { + subT.Helper() - if registeredInit == nil { + assert.Assert(subT, test.t == nil, "You cannot run a test multiple times") + + // Attach testing.T + test.t = subT + assert.Assert(test.t, test.Description != "", "A test description cannot be empty") + assert.Assert(test.t, test.Command == nil || test.Expected != nil, + "Expectations for a test command cannot be nil. You may want to use Setup instead.") + + // Ensure we have env + if test.Env == nil { + test.Env = map[string]string{} + } + + // If we have a parent, get parent env and data + var parentData Data + if test.parent != nil { + parentData = test.parent.Data + for k, v := range test.parent.Env { + if _, ok := test.Env[k]; !ok { + test.Env[k] = v + } + } + } + + // Inherit and attach Data + test.Data = configureData(test.t, test.Data, parentData) + + if registeredHooks == nil { bc := &GenericCommand{} bc.WithEnv(test.Env) - bc.WithT(tt) + bc.WithT(test.t) bc.WithTempDir(test.Data.TempDir()) test.baseCommand = bc } else { - test.baseCommand = registeredInit(test, test.t) + test.baseCommand = registeredHooks.OnInitialize(test, test.t) } - test.exec(tt) - } + // Set base command + test.helpers = &HelpersInternal{ + CmdInternal: test.baseCommand, + } - if test.subIt { - t.Run(test.Description, testRun) - } else { - testRun(t) - } -} + setups := []func(data Data, helpers Helpers){} + cleanups := []func(data Data, helpers Helpers){} -// seal is a private method to prepare the test -func (test *Case) seal(t *testing.T) { - t.Helper() - assert.Assert(t, test.t == nil, "You cannot run a test multiple times") - assert.Assert(t, test.Description != "", "A test description cannot be empty") - assert.Assert(t, test.Command == nil || test.Expected != nil, - "Expectations for a test command cannot be nil. You may want to use Setup instead.") - - // Ensure we have env - if test.Env == nil { - test.Env = map[string]string{} - } + // Register custom cleanup if any - MUST run before Requirements cleanups + if test.Cleanup != nil { + cleanups = append(cleanups, test.Cleanup) + } - // If we have a parent, get parent env and data - var parentData Data - if test.parent != nil { - parentData = test.parent.Data - for k, v := range test.parent.Env { - if _, ok := test.Env[k]; !ok { - test.Env[k] = v + // Check the requirements before going any further + if test.Require != nil { + shouldRun, message := test.Require.Check(test.Data, test.helpers, test.t) + if !shouldRun { + test.t.Skipf("test skipped as: %s", message) + } else { + if test.Require.Setup != nil { + setups = append(setups, test.Require.Setup) + } + if test.Require.Cleanup != nil { + cleanups = append(cleanups, test.Require.Cleanup) + } } } - } - // Attach testing.T - test.t = t - // Inherit and attach Data - test.Data = configureData(t, test.Data, parentData) - - // Check the requirements - if test.Require != nil { - test.Require(test.Data, t) - } -} + // Register setup if any + if test.Setup != nil { + setups = append(setups, test.Setup) + } -// exec is a private method that will take care of the test setup, command and cleanup execution -func (test *Case) exec(t *testing.T) { - t.Helper() - test.helpers = &helpers{ - test.baseCommand, - } + // Run optional post requirement hook + if registeredHooks != nil { + registeredHooks.OnPostRequirements(test, test.t, test.baseCommand) + } - // Set parallel unless asked not to - if !test.NoParallel { - t.Parallel() - } + // Set parallel unless asked not to + if !test.NoParallel { + test.t.Parallel() + } - // Register cleanup if there is any, and run it to collect any leftovers from previous runs - if test.Cleanup != nil { - test.Cleanup(test.Data, test.helpers) - t.Cleanup(func() { - test.Cleanup(test.Data, test.helpers) + // Execute cleanups now + for _, cleanup := range cleanups { + cleanup(test.Data, test.helpers) + } + test.t.Cleanup(func() { + for _, cleanup := range cleanups { + cleanup(test.Data, test.helpers) + } }) - } - // Run setup - if test.Setup != nil { - test.Setup(test.Data, test.helpers) - } + // Setup now + for _, setup := range setups { + setup(test.Data, test.helpers) + } + + // ENV may have been changed by setup routines + test.baseCommand.WithEnv(test.Env) + // And config as well, which may have effects + if registeredHooks != nil { + registeredHooks.OnPostSetup(test, test.t, test.baseCommand) + } + + // Run the command if any, with expectations + // Note: if we have a command, we already know we DO have Expected + if test.Command != nil { + test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers)) + } - // Run the command if any, with expectations - if test.Command != nil { - test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers)) + // Go for the subtests now + for _, subTest := range test.SubTests { + subTest.parent = test + subTest.subIt = true + subTest.Run(test.t) + } } - for _, subTest := range test.SubTests { - subTest.parent = test - subTest.subIt = true - subTest.Run(t) + if test.subIt { + t.Run(test.Description, testRun) + } else { + testRun(t) } } diff --git a/pkg/testutil/test/command.go b/pkg/testutil/test/command.go index 6fbb1779d52..184eefb4fbe 100644 --- a/pkg/testutil/test/command.go +++ b/pkg/testutil/test/command.go @@ -30,94 +30,102 @@ import ( // GenericCommand is a concrete Command implementation type GenericCommand struct { - WorkingDir string - Env map[string]string + WorkingDir string + Env map[string]string + EnvBlackList []string t *testing.T tempDir string helperBinary string helperArgs []string + prependArgs []string mainBinary string mainArgs []string result *icmd.Result - stdin io.Reader - async bool - timeout time.Duration + + stdin io.Reader + async bool + timeout time.Duration } -func (gc *GenericCommand) WithBinary(binary string) Command { +func (gc *GenericCommand) WithBinary(binary string) { gc.mainBinary = binary - return gc } -func (gc *GenericCommand) WithArgs(args ...string) Command { +func (gc *GenericCommand) WithArgs(args ...string) { gc.mainArgs = append(gc.mainArgs, args...) - return gc +} + +func (gc *GenericCommand) PrependArgs(args ...string) { + gc.prependArgs = append(gc.prependArgs, args...) } // WithEnv will overload the command env with values from the passed map -func (gc *GenericCommand) WithEnv(env map[string]string) Command { +func (gc *GenericCommand) WithEnv(env map[string]string) { if gc.Env == nil { gc.Env = map[string]string{} } for k, v := range env { gc.Env[k] = v } - return gc } -func (gc *GenericCommand) WithWrapper(binary string, args ...string) Command { +func (gc *GenericCommand) WithWrapper(binary string, args ...string) { gc.helperBinary = binary gc.helperArgs = args - return gc } // WithStdin sets the standard input of Cmd to the specified reader -func (gc *GenericCommand) WithStdin(r io.Reader) Command { +func (gc *GenericCommand) WithStdin(r io.Reader) { gc.stdin = r - return gc } -func (gc *GenericCommand) Background(timeout time.Duration) Command { +func (gc *GenericCommand) Background(timeout time.Duration) { // Run it gc.async = true i := gc.boot() - gc.result = icmd.StartCmd(i) gc.timeout = timeout - return gc + gc.result = icmd.StartCmd(i) } -// TODO: it should be possible to: -// - timeout execution +// TODO: it should be possible to timeout execution +// Primitives (gc.timeout) is here, it is just a matter of exposing a WithTimeout method +// - UX to be decided +// - validate use case: would we ever need this? + func (gc *GenericCommand) Run(expect *Expected) { + if gc.t != nil { + gc.t.Helper() + } + var result *icmd.Result var env []string if gc.async { result = icmd.WaitOnCmd(gc.timeout, gc.result) env = gc.result.Cmd.Env } else { - icmdCmd := gc.boot() - env = icmdCmd.Env + iCmdCmd := gc.boot() + env = iCmdCmd.Env // Run it - result = icmd.RunCmd(icmdCmd) + result = icmd.RunCmd(iCmdCmd) } // Check our expectations, if any if expect != nil { - // Build the debug string - additionally attach the env (which icmd does not do) + // Build the debug string - additionally attach the env (which iCmd does not do) debug := result.String() + "Env:\n" + strings.Join(env, "\n") // ExitCode goes first if expect.ExitCode == -1 { assert.Assert(gc.t, result.ExitCode != 0, - "Expected exit code to be different than 0"+debug) + "Expected exit code to be different than 0\n"+debug) } else { assert.Assert(gc.t, expect.ExitCode == result.ExitCode, - fmt.Sprintf("Expected exit code: %d", expect.ExitCode)+debug) + fmt.Sprintf("Expected exit code: %d\n", expect.ExitCode)+debug) } // Range through the expected errors and confirm they are seen on stderr for _, expectErr := range expect.Errors { assert.Assert(gc.t, strings.Contains(result.Stderr(), expectErr.Error()), - fmt.Sprintf("Expected error: %q to be found in stderr", expectErr.Error())+debug) + fmt.Sprintf("Expected error: %q to be found in stderr\n", expectErr.Error())+debug) } // Finally, check the output if we are asked to if expect.Output != nil { @@ -128,10 +136,12 @@ func (gc *GenericCommand) Run(expect *Expected) { func (gc *GenericCommand) boot() icmd.Cmd { // This is a helper function, not to appear in the debugging output - gc.t.Helper() + if gc.t != nil { + gc.t.Helper() + } binary := gc.mainBinary - args := gc.mainArgs + args := append(gc.prependArgs, gc.mainArgs...) if gc.helperBinary != "" { args = append([]string{binary}, args...) args = append(gc.helperArgs, args...) @@ -139,33 +149,48 @@ func (gc *GenericCommand) boot() icmd.Cmd { } // Create the command and set the env - // TODO: do we really need icmd? - icmdCmd := icmd.Command(binary, args...) - icmdCmd.Env = []string{} + // TODO: do we really need iCmd? + gc.t.Log(binary, strings.Join(args, " ")) + + iCmdCmd := icmd.Command(binary, args...) + iCmdCmd.Env = []string{} for _, v := range os.Environ() { - // Ignore LS_COLORS from the env, just too much noise - if !strings.HasPrefix(v, "LS_COLORS") { - icmdCmd.Env = append(icmdCmd.Env, v) + add := true + for _, b := range gc.EnvBlackList { + if strings.HasPrefix(v, b+"=") { + add = false + break + } + } + if add { + iCmdCmd.Env = append(iCmdCmd.Env, v) } } // Ensure the subprocess gets executed in a temporary directory unless explicitly instructed otherwise - icmdCmd.Dir = gc.WorkingDir - if icmdCmd.Dir == "" { - icmdCmd.Dir = gc.tempDir + iCmdCmd.Dir = gc.WorkingDir + if iCmdCmd.Dir == "" { + iCmdCmd.Dir = gc.tempDir + } + + if gc.stdin != nil { + iCmdCmd.Stdin = gc.stdin } // Attach any extra env we have for k, v := range gc.Env { - icmdCmd.Env = append(icmdCmd.Env, fmt.Sprintf("%s=%s", k, v)) + iCmdCmd.Env = append(iCmdCmd.Env, fmt.Sprintf("%s=%s", k, v)) } - return icmdCmd + return iCmdCmd } func (gc *GenericCommand) Clone() Command { - // Copy the command and return a new one - with WorkingDir, binary, args, etc + // Copy the command and return a new one - with almost everything from the parent command cc := *gc + cc.result = nil + cc.stdin = nil + cc.timeout = 0 // Clone Env cc.Env = make(map[string]string, len(gc.Env)) for k, v := range gc.Env { @@ -175,17 +200,33 @@ func (gc *GenericCommand) Clone() Command { } func (gc *GenericCommand) Clear() Command { - gc.mainBinary = "" - gc.helperBinary = "" - gc.mainArgs = []string{} - gc.helperArgs = []string{} - return gc + cc := *gc + cc.mainBinary = "" + cc.helperBinary = "" + cc.mainArgs = []string{} + cc.prependArgs = []string{} + cc.helperArgs = []string{} + // Clone Env + cc.Env = make(map[string]string, len(gc.Env)) + for k, v := range gc.Env { + cc.Env[k] = v + } + return &cc } -func (gc *GenericCommand) WithT(t *testing.T) { +func (gc *GenericCommand) WithT(t *testing.T) Command { gc.t = t + return gc } func (gc *GenericCommand) WithTempDir(tempDir string) { gc.tempDir = tempDir } + +func (gc *GenericCommand) T() *testing.T { + return gc.t +} + +func (gc *GenericCommand) TempDir() string { + return gc.tempDir +} diff --git a/pkg/testutil/test/data.go b/pkg/testutil/test/data.go index 99f2aa041ad..ed20198d790 100644 --- a/pkg/testutil/test/data.go +++ b/pkg/testutil/test/data.go @@ -17,21 +17,22 @@ package test import ( - "crypto/sha256" "fmt" + "regexp" "strings" "testing" + + "github.com/opencontainers/go-digest" ) // Contains the implementation of the Data interface - type data struct { config map[ConfigKey]ConfigValue system map[SystemKey]SystemValue labels map[string]string - testID string + testID func(suffix ...string) string tempDir string } @@ -68,8 +69,8 @@ func (dt *data) Set(key string, value string) Data { return dt } -func (dt *data) Identifier() string { - return dt.testID +func (dt *data) Identifier(suffix ...string) string { + return dt.testID(suffix...) } func (dt *data) TempDir() string { @@ -92,20 +93,24 @@ func (dt *data) adopt(parent Data) { } func (dt *data) Sink(key SystemKey, value SystemValue) { + if dt.system == nil { + dt.system = map[SystemKey]SystemValue{} + } if _, ok := dt.system[key]; !ok { dt.system[key] = value } else { - // XXX should we really panic? panic(fmt.Sprintf("Unable to set system key %s multiple times", key)) } } -func (dt *data) Surface(key SystemKey) SystemValue { +func (dt *data) Surface(key SystemKey) (SystemValue, error) { + if dt.system == nil { + dt.system = map[SystemKey]SystemValue{} + } if v, ok := dt.system[key]; ok { - return v + return v, nil } - // XXX should we really panic? - panic(fmt.Sprintf("Unable to retrieve system key %s", key)) + return "", fmt.Errorf("unable to retrieve system key %s", key) } func (dt *data) getLabels() map[string]string { @@ -116,20 +121,25 @@ func (dt *data) getConfig() map[ConfigKey]ConfigValue { return dt.config } -func defaultIdentifierHashing(name string) string { - s := strings.ReplaceAll(name, " ", "_") - s = strings.ReplaceAll(s, "/", "_") - s = strings.ReplaceAll(s, "-", "_") - s = strings.ReplaceAll(s, ",", "_") - s = strings.ToLower(s) - if len(s) > 76 { - s = fmt.Sprintf("%x", sha256.Sum256([]byte(s))) +func defaultIdentifierHashing(names ...string) string { + // Notes: this identifier MAY be used for namespaces, image names, etc. + // So, the rules are stringent on what it can contain. + replaceWith := []byte("-") + name := strings.ToLower(strings.Join(names, string(replaceWith))) + // Ensure we have a unique identifier despite characters replacements (well, as unique as name) + signature := digest.SHA256.FromString(name).Encoded()[0:8] + // Make sure we do not use any unsafe characters + safeName := regexp.MustCompile(`[^a-zA-Z0-9-]+`) + noRepeat := regexp.MustCompile(fmt.Sprintf(`[%s]{2,}`, replaceWith)) + sn := safeName.ReplaceAll([]byte(name), replaceWith) + sn = noRepeat.ReplaceAll(sn, replaceWith) + // Ensure we will never go above 76 characters in length (with signature) + if len(sn) > 67 { + sn = sn[0:67] } - - return s + return string(sn) + "-" + signature } -// TODO: allow to pass custom hashing methods? func configureData(t *testing.T, seedData Data, parent Data) Data { if seedData == nil { seedData = &data{} @@ -138,7 +148,10 @@ func configureData(t *testing.T, seedData Data, parent Data) Data { config: seedData.getConfig(), labels: seedData.getLabels(), tempDir: t.TempDir(), - testID: defaultIdentifierHashing(t.Name()), + testID: func(suffix ...string) string { + suffix = append([]string{t.Name()}, suffix...) + return defaultIdentifierHashing(suffix...) + }, } if parent != nil { dat.adopt(parent) diff --git a/pkg/testutil/test/expected.go b/pkg/testutil/test/expected.go index 81d617acfdf..0a5dbcd833b 100644 --- a/pkg/testutil/test/expected.go +++ b/pkg/testutil/test/expected.go @@ -18,18 +18,31 @@ package test import ( "fmt" + "regexp" "strings" "testing" "gotest.tools/v3/assert" ) +// RunCommand is the simplest way to express a test.Command for very basic cases when access to test data is not necessary func RunCommand(args ...string) Executor { return func(data Data, helpers Helpers) Command { return helpers.Command(args...) } } +// Expects is provided as a simple helper covering "expectations" for simple use-cases where access to the test data is not necessary +func Expects(exitCode int, errors []error, output Comparator) Manager { + return func(_ Data, _ Helpers) *Expected { + return &Expected{ + ExitCode: exitCode, + Errors: errors, + Output: output, + } + } +} + // WithData returns a data object with a certain key value set func WithData(key string, value string) Data { dat := &data{} @@ -44,18 +57,7 @@ func WithConfig(key ConfigKey, value ConfigValue) Data { return dat } -// Expects is provided as a simple helper covering "expectations" for simple use-cases where access to the test data is not necessary -func Expects(exitCode int, errors []error, output Comparator) Manager { - return func(_ Data, _ Helpers) *Expected { - return &Expected{ - ExitCode: exitCode, - Errors: errors, - Output: output, - } - } -} - -// All can be used as a parameter for expected.Output and allow passing a collection of conditions to match +// All can be used as a parameter for expected.Output to group a set of comparators func All(comparators ...Comparator) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() @@ -69,7 +71,7 @@ func All(comparators ...Comparator) Comparator { func Contains(compare string) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() - assert.Assert(t, strings.Contains(stdout, compare), fmt.Sprintf("Expected output to contain: %q", compare)+info) + assert.Check(t, strings.Contains(stdout, compare), fmt.Sprintf("Output does not contain: %q", compare)+info) } } @@ -77,7 +79,7 @@ func Contains(compare string) Comparator { func DoesNotContain(compare string) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() - assert.Assert(t, !strings.Contains(stdout, compare), fmt.Sprintf("Expected output to not contain: %q", compare)+info) + assert.Check(t, !strings.Contains(stdout, compare), fmt.Sprintf("Output does contain: %q", compare)+info) } } @@ -88,3 +90,12 @@ func Equals(compare string) Comparator { assert.Equal(t, compare, stdout, info) } } + +// Provisional - expected use, but have not seen it so far +// Match is to be used for expected.Output to ensure we match a regexp +func Match(reg *regexp.Regexp) Comparator { + return func(stdout string, info string, t *testing.T) { + t.Helper() + assert.Check(t, reg.MatchString(stdout), fmt.Sprintf("Output does not match: %s", reg), info) + } +} diff --git a/pkg/testutil/test/helpers.go b/pkg/testutil/test/helpers.go index 64a734dd86c..8150c63eff6 100644 --- a/pkg/testutil/test/helpers.go +++ b/pkg/testutil/test/helpers.go @@ -18,6 +18,7 @@ package test import "testing" +// Helpers provides a set of helpers to run commands with simple expectations, available in most stages of a test (Setup, Cleanup, etc...) type Helpers interface { Ensure(args ...string) Anyhow(args ...string) @@ -28,25 +29,33 @@ type Helpers interface { CustomCommand(binary string, args ...string) Command } -type helpers struct { - cmd Command +// FIXME: see `nerdtest/test.go` for why this is exported - it should not be + +type HelpersInternal struct { + CmdInternal Command } -func (hel *helpers) Ensure(args ...string) { - hel.Command(args...).Run(&Expected{}) +// Ensure will run a command and make sure it is successful +func (hel *HelpersInternal) Ensure(args ...string) { + hel.Command(args...).Run(&Expected{ + ExitCode: 0, + }) } -func (hel *helpers) Anyhow(args ...string) { +// Anyhow will run a command regardless of outcome (may or may not fail) +func (hel *HelpersInternal) Anyhow(args ...string) { hel.Command(args...).Run(nil) } -func (hel *helpers) Fail(args ...string) { +// Fail will run a command and make sure it does fail +func (hel *HelpersInternal) Fail(args ...string) { hel.Command(args...).Run(&Expected{ ExitCode: 1, }) } -func (hel *helpers) Capture(args ...string) string { +// Capture will run a command, ensure it is successful and return stdout +func (hel *HelpersInternal) Capture(args ...string) string { var ret string hel.Command(args...).Run(&Expected{ Output: func(stdout string, info string, t *testing.T) { @@ -56,15 +65,17 @@ func (hel *helpers) Capture(args ...string) string { return ret } -func (hel *helpers) Command(args ...string) Command { - cc := hel.cmd.Clone() +// Command will return a clone of your base command without running it +func (hel *HelpersInternal) Command(args ...string) Command { + cc := hel.CmdInternal.Clone() cc.WithArgs(args...) return cc } -func (hel *helpers) CustomCommand(binary string, args ...string) Command { - cc := hel.cmd.Clone() - cc.Clear() +// CustomCommand will return a command for the requested binary and args, with all the environment of your test +// (eg: Env, Cwd, etc.) +func (hel *HelpersInternal) CustomCommand(binary string, args ...string) Command { + cc := hel.CmdInternal.Clear() cc.WithBinary(binary) cc.WithArgs(args...) return cc diff --git a/pkg/testutil/test/requirement.go b/pkg/testutil/test/requirement.go index 1acad13af28..a19e7e75b4b 100644 --- a/pkg/testutil/test/requirement.go +++ b/pkg/testutil/test/requirement.go @@ -23,93 +23,92 @@ import ( "testing" ) -func MakeRequirement(fn func(data Data) (bool, string)) Requirement { - return func(data Data, t *testing.T) (bool, string) { - ret, mess := fn(data) - - if t != nil && !ret { - t.Helper() - t.Skipf("Test skipped as %s", mess) - } +func Binary(name string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers, t *testing.T) (bool, string) { + mess := fmt.Sprintf("executable %q has been found in PATH", name) + ret := true + if _, err := exec.LookPath(name); err != nil { + ret = false + mess = fmt.Sprintf("executable %q doesn't exist in PATH", name) + } - return ret, mess + return ret, mess + }, } } -func Binary(name string) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { - mess = fmt.Sprintf("executable %q has been found in PATH", name) - ret = true - if _, err := exec.LookPath(name); err != nil { - ret = false - mess = fmt.Sprintf("executable %q doesn't exist in PATH", name) - } - - return ret, mess - }) -} - -func OS(os string) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { - mess = fmt.Sprintf("current operating is %q", runtime.GOOS) - ret = true - if runtime.GOOS != os { - ret = false - } +func OS(os string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers, t *testing.T) (bool, string) { + mess := fmt.Sprintf("current operating system is %q", runtime.GOOS) + ret := true + if runtime.GOOS != os { + ret = false + } - return ret, mess - }) + return ret, mess + }, + } } -var Windows = MakeRequirement(func(data Data) (ret bool, mess string) { - ret = runtime.GOOS == "windows" - if ret { - mess = "operating system is Windows" - } else { - mess = "operating system is not Windows" - } - return ret, mess -}) +func Arch(arch string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers, t *testing.T) (bool, string) { + mess := fmt.Sprintf("current architecture is %q", runtime.GOARCH) + ret := true + if runtime.GOARCH != arch { + ret = false + } -var Linux = MakeRequirement(func(data Data) (ret bool, mess string) { - ret = runtime.GOOS == "linux" - if ret { - mess = "operating system is Linux" - } else { - mess = "operating system is not Linux" + return ret, mess + }, } - return ret, mess -}) +} -var Darwin = MakeRequirement(func(data Data) (ret bool, mess string) { - ret = runtime.GOOS == "darwin" - if ret { - mess = "operating system is Darwin" - } else { - mess = "operating system is not Darwin" +var Amd64 = Arch("amd64") +var Arm64 = Arch("arm64") +var Windows = OS("windows") +var Linux = OS("linux") +var Darwin = OS("darwin") + +// NOTE: nested Not will loose setups and cleanups... +func Not(requirement *Requirement) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers, t *testing.T) (bool, string) { + ret, mess := requirement.Check(data, helpers, t) + return !ret, mess + }, } - return ret, mess -}) - -func Not(requirement Requirement) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { - b, mess := requirement(data, nil) - return !b, mess - }) } -func Require(thing ...Requirement) Requirement { - return func(data Data, t *testing.T) (ret bool, mess string) { - for _, th := range thing { - b, m := th(data, nil) - if !b { - if t != nil { - t.Helper() - t.Skipf("Test skipped as %s", m) +func Require(requirements ...*Requirement) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers, t *testing.T) (bool, string) { + ret := true + var mess, subMess string + for _, requirement := range requirements { + ret, subMess = requirement.Check(data, helpers, t) + mess += subMess + if !ret { + return ret, mess + } + } + return ret, mess + }, + Setup: func(data Data, helpers Helpers) { + for _, requirement := range requirements { + if requirement.Setup != nil { + requirement.Setup(data, helpers) + } + } + }, + Cleanup: func(data Data, helpers Helpers) { + for _, requirement := range requirements { + if requirement.Cleanup != nil { + requirement.Cleanup(data, helpers) } - return false, "" } - } - return true, "" + }, } } diff --git a/pkg/testutil/test/test.go b/pkg/testutil/test/test.go index 2e6743be8e6..5708296e5e3 100644 --- a/pkg/testutil/test/test.go +++ b/pkg/testutil/test/test.go @@ -22,9 +22,16 @@ import ( "time" ) -// A Requirement is a function that can evaluate random requirement and possibly skip a test -// See test.MakeRequirement to make your own -type Requirement func(data Data, t *testing.T) (bool, string) +// An Evaluator is a function that decides whether a test should run on not, to be fed to MakeRequirement +type Evaluator func(data Data, helpers Helpers, t *testing.T) (bool, string) + +// A Requirement offers a way to verify random conditions to decide if a test should be skipped +// It can furthermore (optionally) provide custom setup and cleanup routines to perform +type Requirement struct { + Check Evaluator + Setup Butler + Cleanup Butler +} // A Butler is the function signature meant to be attached to a Setup or Cleanup routine for a test.Case type Butler func(data Data, helpers Helpers) @@ -42,15 +49,15 @@ type Manager func(data Data, helpers Helpers) *Expected // Typically, a Case has a base-command, from which all commands involved in the test are derived. type Command interface { // WithBinary specifies what binary to execute - WithBinary(binary string) Command + WithBinary(binary string) // WithArgs specifies the args to pass to the binary. Note that WithArgs is additive. - WithArgs(args ...string) Command + WithArgs(args ...string) // WithEnv adds the passed map to the environment of the command to be executed - WithEnv(env map[string]string) Command + WithEnv(env map[string]string) // WithWrapper allows wrapping a command with another command (for example: `time`, `unbuffer`) - WithWrapper(binary string, args ...string) Command + WithWrapper(binary string, args ...string) // WithStdin allows passing a reader to be used for stdin for the command - WithStdin(r io.Reader) Command + WithStdin(r io.Reader) // Run does execute the command, and compare the output with the provided expectation. // Passing nil for `Expected` will just run the command regardless of outcome. // An empty `&Expected{}` is (of course) equivalent to &Expected{Exit: 0}, meaning the command is verified to be @@ -58,12 +65,15 @@ type Command interface { Run(expect *Expected) // Clone returns a copy of the command Clone() Command - // Clear will clear binary and arguments, but retain the env, or any other custom properties + // Clear does a clone, but will clear binary and arguments, but retain the env, or any other custom properties + // Gotcha: if GenericCommand is embedded with a custom Run and an overridden Clear to return the embedding type + // the result will be the embedding command, no longer the GenericCommand Clear() Command - // Allow starting a command in the background - Background(timeout time.Duration) Command + // Background allows starting a command in the background + Background(timeout time.Duration) } +// A Comparator is the function signature to implement when implementing testing against stdout of a command type Comparator func(stdout string, info string, t *testing.T) // Expected expresses the expected output of a command @@ -85,7 +95,7 @@ type SystemValue string // Data is meant to hold information about a test: // - first, any random key value data that the test implementer wants to carry / modify - this is test data // - second, configuration specific to the binary being tested - typically defined by the specialized command being tested -// - third, immutable "system" info (unique identifier, tempdir, or other SystemKey/Value pairs) +// - third, immutable "system" info (unique identifier, tempDir, or other SystemKey/Value pairs) type Data interface { // Get returns the value of a certain key for custom data Get(key string) string @@ -93,13 +103,13 @@ type Data interface { Set(key string, value string) Data // Identifier returns the test identifier that can be used to name resources - Identifier() string + Identifier(suffix ...string) string // TempDir returns the test temporary directory TempDir() string // Sink allows to define ONCE a certain system property Sink(key SystemKey, value SystemValue) // Surface allows retrieving a certain system property - Surface(key SystemKey) SystemValue + Surface(key SystemKey) (SystemValue, error) // WithConfig allows setting a declared ConfigKey to a ConfigValue WithConfig(key ConfigKey, value ConfigValue) Data @@ -110,10 +120,16 @@ type Data interface { getConfig() map[ConfigKey]ConfigValue } +type Hooks interface { + OnInitialize(testCase *Case, t *testing.T) Command + OnPostRequirements(testCase *Case, t *testing.T, com Command) + OnPostSetup(testCase *Case, t *testing.T, com Command) +} + var ( - registeredInit func(test *Case, t *testing.T) Command + registeredHooks Hooks ) -func CustomCommand(custom func(test *Case, t *testing.T) Command) { - registeredInit = custom +func CustomCommand(hooks Hooks) { + registeredHooks = hooks } diff --git a/pkg/testutil/test/utilities.go b/pkg/testutil/test/utilities.go new file mode 100644 index 00000000000..b12715d7b82 --- /dev/null +++ b/pkg/testutil/test/utilities.go @@ -0,0 +1,42 @@ +/* + 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 test + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "os" +) + +// IsRoot returns true if we are root... simple +func IsRoot() bool { + return os.Geteuid() == 0 +} + +// RandomStringBase64 generates a base64 encoded random string +func RandomStringBase64(n int) string { + b := make([]byte, n) + l, err := rand.Read(b) + if err != nil { + panic(err) + } + if l != n { + panic(fmt.Errorf("expected %d bytes, got %d bytes", n, l)) + } + return base64.URLEncoding.EncodeToString(b) +} diff --git a/pkg/testutil/testregistry/certsd_linux.go b/pkg/testutil/testregistry/certsd_linux.go index 955bc4ba12f..2a9587e08c4 100644 --- a/pkg/testutil/testregistry/certsd_linux.go +++ b/pkg/testutil/testregistry/certsd_linux.go @@ -17,31 +17,11 @@ package testregistry import ( - "fmt" - "net" - "os" - "path/filepath" - "strconv" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" ) func generateCertsd(dir string, certPath string, hostIP string, port int) error { - joined := hostIP - if port != 0 { - joined = net.JoinHostPort(hostIP, strconv.Itoa(port)) - } - - hostsSubDir := filepath.Join(dir, joined) - err := os.MkdirAll(hostsSubDir, 0700) - if err != nil { - return err - } - - hostsTOMLPath := filepath.Join(hostsSubDir, "hosts.toml") - // See https://github.com/containerd/containerd/blob/main/docs/hosts.md - hostsTOML := fmt.Sprintf(` -server = "https://%s" -[host."https://%s"] - ca = %q - `, joined, joined, certPath) - return os.WriteFile(hostsTOMLPath, []byte(hostsTOML), 0700) + return (&hoststoml.HostsToml{ + CA: certPath, + }).Save(dir, hostIP, port) } diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go index 61342b1a28f..538076f0586 100644 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ b/pkg/testutil/testregistry/testregistry_linux.go @@ -64,9 +64,9 @@ func EnsureImages(base *testutil.Base) { } registryImage = testutil.RegistryImageNext + up } - base.Cmd("pull", registryImage).AssertOK() - base.Cmd("pull", testutil.DockerAuthImage).AssertOK() - base.Cmd("pull", testutil.KuboImage).AssertOK() + base.Cmd("pull", "--quiet", registryImage).AssertOK() + base.Cmd("pull", "--quiet", testutil.DockerAuthImage).AssertOK() + base.Cmd("pull", "--quiet", testutil.KuboImage).AssertOK() } func NewAuthServer(base *testutil.Base, ca *testca.CA, port int, user, pass string, tls bool) *TokenAuthServer { diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index a7237ad6c82..c4c4359ad8b 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -549,6 +549,7 @@ var ( flagTestKillDaemon bool flagTestIPv6 bool flagTestKube bool + flagVerbose bool ) var ( @@ -560,6 +561,9 @@ func M(m *testing.M) { flag.BoolVar(&flagTestKillDaemon, "test.allow-kill-daemon", false, "enable tests that kill the daemon") flag.BoolVar(&flagTestIPv6, "test.only-ipv6", false, "enable tests on IPv6") flag.BoolVar(&flagTestKube, "test.only-kubernetes", false, "enable tests on Kubernetes") + if flag.Lookup("test.v") != nil { + flagVerbose = true + } flag.Parse() os.Exit(func() int { @@ -633,6 +637,8 @@ func IsDocker() bool { return GetTarget() == Docker } +func GetVerbose() bool { return flagVerbose } + func DockerIncompatible(t testing.TB) { if IsDocker() { t.Skip("test is incompatible with Docker") diff --git a/pkg/testutil/testutil_linux.go b/pkg/testutil/testutil_linux.go index 0629abbdac2..dce9431e021 100644 --- a/pkg/testutil/testutil_linux.go +++ b/pkg/testutil/testutil_linux.go @@ -126,7 +126,7 @@ func NewDelayOnceReader(wrapped io.Reader) io.Reader { func (r *delayOnceReader) Read(p []byte) (int, error) { // FIXME: this is obviously not exact science. At 1 second, it will fail regularly on the CI under load. - r.once.Do(func() { time.Sleep(2 * time.Second) }) + r.once.Do(func() { time.Sleep(5 * time.Second) }) n, err := r.wrapped.Read(p) if errors.Is(err, io.EOF) { time.Sleep(time.Second)