From 7813191d74e0ba91ef5e92c2745439697e8d9d65 Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 22 Sep 2024 21:26:38 -0700 Subject: [PATCH] [WIP] Signed-off-by: apostasie --- .../builder/builder_build_linux_test.go | 88 ++-- cmd/nerdctl/builder/builder_build_test.go | 198 ++++++--- cmd/nerdctl/builder/builder_linux_test.go | 219 +++++----- .../container/container_attach_linux_test.go | 156 +++---- .../container/container_commit_test.go | 79 ++-- .../container/container_run_linux_test.go | 134 +++--- .../container/container_start_linux_test.go | 70 ++-- cmd/nerdctl/image/image_push_linux_test.go | 381 ++++++++++-------- cmd/nerdctl/image/image_remove_linux_test.go | 107 ----- cmd/nerdctl/image/image_remove_test.go | 308 ++++++++++++++ cmd/nerdctl/image/image_save_linux_test.go | 50 --- cmd/nerdctl/image/image_save_test.go | 193 +++++++-- cmd/nerdctl/login/login_linux_test.go | 42 ++ pkg/testutil/nerdtest/helpers.go | 119 ++++++ pkg/testutil/nerdtest/requirements.go | 133 ++++++ pkg/testutil/nerdtest/test.go | 113 +----- pkg/testutil/test/case.go | 2 +- pkg/testutil/test/command.go | 11 +- pkg/testutil/test/data.go | 17 +- pkg/testutil/test/requirement.go | 32 +- pkg/testutil/test/test.go | 2 +- 21 files changed, 1568 insertions(+), 886 deletions(-) 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 create mode 100644 pkg/testutil/nerdtest/helpers.go create mode 100644 pkg/testutil/nerdtest/requirements.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..7a2db6f507f 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,139 @@ 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) +func TestCanBuildOnOtherPlatform(t *testing.T) { + nerdtest.Setup() - buildCtx := helpers.CreateBuildContext(t, dockerfile) + testutil.RequiresBuild(t) - base.Cmd("build", buildCtx, "-t", imageName).AssertErrNotContains("tarball") + // Check that we can emulate a different platform + 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, stdout) + }, + }) + + 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 + } + } - 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") + if found == nil { + t.Skip("buildkit worker does not support emulation") + } - 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") + testCase := &test.Case{ + Description: "Successfully build on emulated platforms", + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("build", data.Get("buildCtx"), "--platform", fmt.Sprintf("%s/%s", found.OS, found.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 +180,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 +190,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 +223,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 +289,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 +328,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 +371,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 +394,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 +417,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 +470,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 +485,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 +500,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 +525,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 +546,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 +567,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 +623,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..dbb25bfdbe9 100644 --- a/cmd/nerdctl/builder/builder_linux_test.go +++ b/cmd/nerdctl/builder/builder_linux_test.go @@ -18,136 +18,139 @@ 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: test.Not(nerdtest.Rootless), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + 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/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index 71a74eae59e..e9068e9d536 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -24,81 +24,97 @@ 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})) + return helpers. + Command("attach", "--detach-keys=ctrl-a,ctrl-b", 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, 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_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index aca549d9446..49deaced94f 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,95 @@ 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: test.Not(nerdtest.Rootless), + 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})) + return helpers. + Command("run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage). + 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, 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..e8caf239fbb 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -24,41 +24,51 @@ 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"))), + helpers. + Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage). + WithWrapper("unbuffer", "-p"). + WithStdin(si). + 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})) + return helpers. + Command("start", "-a", "--detach-keys=ctrl-a,ctrl-b", 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, 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/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 17f757703cf..8ce6f01f467 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,227 @@ 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", + + 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) + 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..8eeea2bc9e8 --- /dev/null +++ b/cmd/nerdctl/image/image_remove_test.go @@ -0,0 +1,308 @@ +/* + 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 TestRemoveImage(t *testing.T) { + nerdtest.Setup() + + repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) + nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) + // NOTES: + // - there MAY be circumstances where docker fails with this (as in: succeed in untagging the image) + // If that ever happens, we could add a preventative `image prune --force --all` inside Setup + // - since all of these are rmi-ing the common image, we need private mode + testGroup := test.Group{ + { + Description: "Remove image with stopped container - without -f", + Require: test.Require( + nerdtest.Private, + 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", 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", + Require: test.Require( + nerdtest.Private, + 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", + Require: test.Require( + nerdtest.Private, + 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") + }, + 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", + // 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( + nerdtest.Private, + 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", + Require: test.Require( + nerdtest.Private, + 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", + Require: test.Require( + nerdtest.Private, + 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", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + 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", + Require: test.Require( + nerdtest.Private, + 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", + Require: test.Require( + nerdtest.Private, + 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", 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", + Require: test.Require( + nerdtest.Private, + 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), + }) + }, + } + }, + }, + } + + testGroup.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..809f23fbcc5 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -17,54 +17,167 @@ 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] - } - 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() -} +func TestSave(t *testing.T) { + nerdtest.Setup() -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] + testGroup := &test.Group{ + { + Description: "Test content (linux only)", + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", 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")) + }, + } + }, + }, + { + Description: "Single image, by id", + // 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. + Require: nerdtest.Private, + 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", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if testutil.GetTarget() == testutil.Docker { + 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", + Require: nerdtest.Private, + 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", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if testutil.GetTarget() == testutil.Docker { + 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")), + }, + { + Description: "Reproduce issue 3425 (no tag)", + // 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. + Require: test.Require( + nerdtest.Private, + // FIXME: nerdctl is broken + nerdtest.Docker, + ), + 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", testutil.CommonImage) + helpers.Ensure("run", "--name", data.Identifier(), "-d", testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("pull", testutil.CommonImage) + // helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + tarPath := filepath.Join(data.TempDir(), "out.tar") + return helpers.Command("save", "-o", tarPath, testutil.CommonImage) // data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Reproduce issue 3425, with tag", + // 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. + Require: test.Require( + nerdtest.Private, + // FIXME: nerdctl is broken + nerdtest.Docker, + ), + 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", testutil.CommonImage) + helpers.Ensure("run", "--name", data.Identifier(), "-d", testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + tarPath := filepath.Join(data.TempDir(), "out.tar") + return helpers.Command("save", "-o", tarPath, data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, } - base.Cmd("tag", testutil.CommonImage, "foobar").AssertOK() - - 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() + testGroup.Run(t) } diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 13a68c4900a..565ed94bb7c 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -31,6 +31,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "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/testca" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) @@ -88,6 +90,46 @@ func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { } func TestLoginPersistence(t *testing.T) { + nerdtest.Setup() + + var authbasic testregistry.Auth + var authno testregistry.Auth + var authtoken testregistry.Auth + var dependentCleanup func(error) + + testCase := &test.Case{ + Setup: func(data test.Data, helpers test.Helpers) { + username := testregistry.SafeRandomString(30) + "∞" + password := testregistry.SafeRandomString(30) + ":∞" + + authno = &testregistry.NoAuth{} + authbasic = &testregistry.BasicAuth{ + Username: username, + Password: password, + } + 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 + + // Start the registry with the requested options + reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) + + // Register registry cleanup + t.Cleanup(func() { + reg.Cleanup(nil) + }) + + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + + }, + SubTests: []*test.Case{}, + } + base := testutil.NewBase(t) t.Parallel() diff --git a/pkg/testutil/nerdtest/helpers.go b/pkg/testutil/nerdtest/helpers.go new file mode 100644 index 00000000000..346a6c9122a --- /dev/null +++ b/pkg/testutil/nerdtest/helpers.go @@ -0,0 +1,119 @@ +/* + 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/test" +) + +// 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 EnsureContainerStarted(helpers test.Helpers, con string) { + const ( + maxRetry = 5 + sleep = time.Second + ) + for i := 0; i < maxRetry; i++ { + count := i + cmd := helpers.Command("container", "inspect", con) + cmd.Run(&test.Expected{ + ExitCode: 0, + 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("conainer %s not running", con) + } + time.Sleep(sleep) + }, + }) + } +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go new file mode 100644 index 00000000000..e92f1486864 --- /dev/null +++ b/pkg/testutil/nerdtest/requirements.go @@ -0,0 +1,133 @@ +/* + 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" + "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 only test.ConfigValue = "Only" +var mode test.ConfigKey = "Mode" +var modePrivate test.ConfigValue = "Private" + +var OnlyIPv6 = test.MakeRequirement(func(data test.Data, t *testing.T) (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, t *testing.T) (ret bool, mess string) { + data.WithConfig(mode, modePrivate) + return true, "private mode" +}) + +var Soci = test.MakeRequirement(func(data test.Data, t *testing.T) (ret bool, mess string) { + ret = false + mess = "soci is not enabled" + (&test.GenericCommand{}). + WithT(t). + WithBinary(testutil.GetTarget()). + WithArgs("info", "--format", "{{ json . }}"). + Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + 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 Docker = test.MakeRequirement(func(data test.Data, t *testing.T) (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 Rootless = test.MakeRequirement(func(data test.Data, t *testing.T) (ret bool, mess string) { + // Make sure we DO not return "IsRootless true" for docker + ret = testutil.GetTarget() != testutil.Docker && rootlessutil.IsRootless() + if ret { + mess = "environment is rootless" + } else { + mess = "environment is rootful" + } + return ret, mess +}) + +var Build = test.MakeRequirement(func(data test.Data, t *testing.T) (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 = "buildkitd is enabled" + if testutil.GetTarget() == testutil.Nerdctl { + _, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + if err != nil { + ret = false + mess = fmt.Sprintf("buildkitd is not enabled: %+v", err) + } + } + return ret, mess +}) + +var CGroup = test.MakeRequirement(func(data test.Data, t *testing.T) (ret bool, mess string) { + ret = true + mess = "cgroup is enabled" + (&test.GenericCommand{}). + WithT(t). + WithBinary(testutil.GetTarget()). + WithArgs("info", "--format", "{{ json . }}"). + Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + 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 +}) diff --git a/pkg/testutil/nerdtest/test.go b/pkg/testutil/nerdtest/test.go index a2f7a5bd3c2..c688ba07c0c 100644 --- a/pkg/testutil/nerdtest/test.go +++ b/pkg/testutil/nerdtest/test.go @@ -17,18 +17,12 @@ package nerdtest import ( - "encoding/json" - "fmt" "os" "path/filepath" "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" ) @@ -43,60 +37,6 @@ 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 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 -}) - -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) - if err != nil { - ret = false - mess = fmt.Sprintf("test requires buildkitd: %+v", err) - } - } - return ret, mess -}) - type NerdCommand struct { test.GenericCommand // FIXME: annoying - forces custom Clone, etc @@ -121,55 +61,6 @@ func (nc *NerdCommand) Clone() test.Command { } } -// 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 nerdctlSetup(testCase *test.Case, t *testing.T) test.Command { t.Helper() @@ -178,11 +69,11 @@ func nerdctlSetup(testCase *test.Case, t *testing.T) test.Command { var pvNamespace string inherited := false - if dt.ReadConfig(IPv6) != Only && testutil.GetEnableIPv6() { + if dt.ReadConfig(ipv6) != only && testutil.GetEnableIPv6() { t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") } - if dt.ReadConfig(Mode) == ModePrivate { + if dt.ReadConfig(mode) == modePrivate { // If private was inherited, we already got a configured namespace if dt.ReadConfig(Namespace) != "" { pvNamespace = string(dt.ReadConfig(Namespace)) diff --git a/pkg/testutil/test/case.go b/pkg/testutil/test/case.go index eed35a929f6..36384c6962b 100644 --- a/pkg/testutil/test/case.go +++ b/pkg/testutil/test/case.go @@ -133,7 +133,7 @@ func (test *Case) seal(t *testing.T) { // Check the requirements if test.Require != nil { - test.Require(test.Data, t) + test.Require(test.Data, true, t) } } diff --git a/pkg/testutil/test/command.go b/pkg/testutil/test/command.go index 6fbb1779d52..3e1b840db6e 100644 --- a/pkg/testutil/test/command.go +++ b/pkg/testutil/test/command.go @@ -128,7 +128,9 @@ 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 @@ -155,6 +157,10 @@ func (gc *GenericCommand) boot() icmd.Cmd { 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)) @@ -182,8 +188,9 @@ func (gc *GenericCommand) Clear() Command { return gc } -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) { diff --git a/pkg/testutil/test/data.go b/pkg/testutil/test/data.go index 99f2aa041ad..bf152a9a586 100644 --- a/pkg/testutil/test/data.go +++ b/pkg/testutil/test/data.go @@ -17,9 +17,8 @@ package test import ( - "crypto/sha256" + "crypto/sha1" "fmt" - "strings" "testing" ) @@ -117,16 +116,10 @@ func (dt *data) getConfig() map[ConfigKey]ConfigValue { } 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))) - } - - return s + // So... looks like the docker registry implementation is not happy with valid image names... + // "cannot specify 64-byte hexadecimal strings" + // Using sha1 then... + return fmt.Sprintf("%x", sha1.Sum([]byte(name))) } // TODO: allow to pass custom hashing methods? diff --git a/pkg/testutil/test/requirement.go b/pkg/testutil/test/requirement.go index 1acad13af28..df4ffaec051 100644 --- a/pkg/testutil/test/requirement.go +++ b/pkg/testutil/test/requirement.go @@ -23,11 +23,11 @@ import ( "testing" ) -func MakeRequirement(fn func(data Data) (bool, string)) Requirement { - return func(data Data, t *testing.T) (bool, string) { - ret, mess := fn(data) +func MakeRequirement(fn func(data Data, t *testing.T) (bool, string)) Requirement { + return func(data Data, skip bool, t *testing.T) (bool, string) { + ret, mess := fn(data, t) - if t != nil && !ret { + if skip && !ret { t.Helper() t.Skipf("Test skipped as %s", mess) } @@ -37,7 +37,7 @@ func MakeRequirement(fn func(data Data) (bool, string)) Requirement { } func Binary(name string) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { + return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { mess = fmt.Sprintf("executable %q has been found in PATH", name) ret = true if _, err := exec.LookPath(name); err != nil { @@ -50,7 +50,7 @@ func Binary(name string) Requirement { } func OS(os string) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { + return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { mess = fmt.Sprintf("current operating is %q", runtime.GOOS) ret = true if runtime.GOOS != os { @@ -61,49 +61,53 @@ func OS(os string) Requirement { }) } -var Windows = MakeRequirement(func(data Data) (ret bool, mess string) { +var Windows = MakeRequirement(func(data Data, t *testing.T) (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 }) -var Linux = MakeRequirement(func(data Data) (ret bool, mess string) { +var Linux = MakeRequirement(func(data Data, t *testing.T) (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 }) -var Darwin = MakeRequirement(func(data Data) (ret bool, mess string) { +var Darwin = MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { ret = runtime.GOOS == "darwin" if ret { mess = "operating system is Darwin" } else { mess = "operating system is not Darwin" } + return ret, mess }) func Not(requirement Requirement) Requirement { - return MakeRequirement(func(data Data) (ret bool, mess string) { - b, mess := requirement(data, nil) + return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { + b, mess := requirement(data, false, t) + return !b, mess }) } func Require(thing ...Requirement) Requirement { - return func(data Data, t *testing.T) (ret bool, mess string) { + return func(data Data, skip bool, t *testing.T) (ret bool, mess string) { for _, th := range thing { - b, m := th(data, nil) + b, m := th(data, false, t) if !b { - if t != nil { + if skip { t.Helper() t.Skipf("Test skipped as %s", m) } diff --git a/pkg/testutil/test/test.go b/pkg/testutil/test/test.go index 2e6743be8e6..858563be789 100644 --- a/pkg/testutil/test/test.go +++ b/pkg/testutil/test/test.go @@ -24,7 +24,7 @@ import ( // 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) +type Requirement func(data Data, skip bool, t *testing.T) (bool, string) // 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)