diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bd3c674777..d8d98c0c9b7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,7 +82,9 @@ jobs: with: fetch-depth: 1 - name: "Prepare integration test environment" - run: docker build -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . + run: | + docker build -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . + docker build -t test-integration-flaky --target test-integration-flaky --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" run: | sudo systemctl disable --now snapd.service snapd.socket @@ -99,12 +101,15 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - name: "Run integration tests" + run: | + docker run -t --rm --privileged test-integration + - name: "Run integration tests: flaky" uses: nick-fields/retry@v3 with: timeout_minutes: 30 max_attempts: 2 retry_on: error - command: docker run -t --rm --privileged test-integration + command: docker run -t --rm --privileged test-integration-flaky test-integration-ipv6: runs-on: "ubuntu-${{ matrix.ubuntu }}" @@ -133,7 +138,9 @@ jobs: echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json sudo systemctl restart docker - name: "Prepare integration test environment" - run: docker build -t test-integration-ipv6 --target test-integration-ipv6 --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . + run: | + docker build -t test-integration-ipv6 --target test-integration-ipv6 --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . + docker build -t test-integration-ipv6-flaky --target test-integration-ipv6-flaky --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" run: | sudo systemctl disable --now snapd.service snapd.socket @@ -149,18 +156,20 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install linux/amd64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - - name: "Run integration tests" # The nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config. # Therefore it's hard to debug why the IPv6 tests fail in such an isolation layer. # On the other side, using the host network is easier at configuration. # Besides, each job is running on a different instance, which means using host network here # is safe and has no side effects on others. + - name: "Run integration tests" + run: docker run --network host -t --rm --privileged test-integration-ipv6 + - name: "Run integration tests: flaky" uses: nick-fields/retry@v3 with: timeout_minutes: 30 max_attempts: 2 retry_on: error - command: docker run --network host -t --rm --privileged test-integration-ipv6 + command: docker run --network host -t --rm --privileged test-integration-ipv6-flaky test-integration-rootless: runs-on: "ubuntu-${{ matrix.ubuntu }}" @@ -220,7 +229,9 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install linux/arm64 docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7 - name: "Prepare (network driver=slirp4netns, port driver=builtin)" - run: docker build -t ${TEST_TARGET} --target ${TEST_TARGET} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} --build-arg ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION} . + run: | + docker build -t ${TEST_TARGET} --target ${TEST_TARGET} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} --build-arg ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION} . + docker build -t ${TEST_TARGET}-flaky --target ${TEST_TARGET}-flaky --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} --build-arg ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION} . - name: "Disable BuildKit for RootlessKit v1 (workaround for issue #622)" run: | # https://github.com/containerd/nerdctl/issues/622 @@ -230,12 +241,14 @@ jobs: fi echo "WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622}" >> "$GITHUB_ENV" - name: "Test (network driver=slirp4netns, port driver=builtin)" + run: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} + - name: "Test (network driver=slirp4netns, port driver=builtin): flaky" uses: nick-fields/retry@v3 with: timeout_minutes: 30 max_attempts: 2 retry_on: error - command: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} + command: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET}-flaky cross: runs-on: ubuntu-24.04 @@ -285,21 +298,25 @@ jobs: run: | sudo apt-get install -y expect - name: "Ensure that the integration test suite is compatible with Docker" + run: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon + - name: "Ensure that the integration test suite is compatible with Docker: flaky" uses: nick-fields/retry@v3 with: timeout_minutes: 30 max_attempts: 2 retry_on: error # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization - command: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon + command: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon -test.only-flaky - name: "Ensure that the IPv6 integration test suite is compatible with Docker" + run: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon -test.only-ipv6 + - name: "Ensure that the IPv6 integration test suite is compatible with Docker: flaky" uses: nick-fields/retry@v3 with: timeout_minutes: 30 max_attempts: 2 retry_on: error # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization - command: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon -test.only-ipv6 + command: go test -p 1 -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.allow-kill-daemon -test.only-ipv6 -test.only-flaky test-integration-windows: runs-on: windows-2022 diff --git a/Dockerfile b/Dockerfile index f825b4bc837..9732a0f22a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -318,9 +318,13 @@ RUN curl -o nydus-static.tgz -fsSL --proto '=https' --tlsv1.2 "https://github.co tar xzf nydus-static.tgz && \ mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \ rm nydus-static.tgz -CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ +CMD ["gotestsum", "--format=testname", "--packages=./cmd/nerdctl/...", \ "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon"] +FROM test-integration AS test-integration-flaky +CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ + "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon", "-test.only-flaky"] + FROM test-integration AS test-integration-rootless # Install SSH for creating systemd user session. # (`sudo` does not work for this purpose, @@ -343,16 +347,29 @@ VOLUME /home/rootless/.local/share COPY ./Dockerfile.d/test-integration-rootless.sh / RUN chmod a+rx /test-integration-rootless.sh CMD ["/test-integration-rootless.sh", \ - "gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ + "gotestsum", "--format=testname", "--packages=./cmd/nerdctl/...", \ "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon"] +FROM test-integration-rootless AS test-integration-rootless-flaky +CMD ["/test-integration-rootless.sh", \ + "gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ + "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon", "-test.only-flaky"] + # test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf RUN chown -R rootless:rootless /home/rootless/.config +FROM test-integration-rootless-flaky AS test-integration-rootless-port-slirp4netns-flaky +COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf +RUN chown -R rootless:rootless /home/rootless/.config + FROM test-integration AS test-integration-ipv6 -CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ +CMD ["gotestsum", "--format=testname", "--packages=./cmd/nerdctl/...", \ "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon", "-test.only-ipv6"] +FROM test-integration AS test-integration-ipv6-flaky +CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=./cmd/nerdctl/...", \ + "--", "-timeout=60m", "-p", "1", "-args", "-test.allow-kill-daemon", "-test.only-ipv6", "-test.only-flaky"] + FROM base AS demo diff --git a/cmd/nerdctl/builder/builder_build_linux_test.go b/cmd/nerdctl/builder/builder_build_linux_test.go index 0f80066b0a2..b65c9097bd6 100644 --- a/cmd/nerdctl/builder/builder_build_linux_test.go +++ b/cmd/nerdctl/builder/builder_build_linux_test.go @@ -18,17 +18,19 @@ 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) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) + nerdtest.Setup() var dockerBuilderArgs []string if testutil.IsDocker() { @@ -38,48 +40,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{ + Require: nerdtest.Build, + 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.Custom("tar", "Cxf", data.TempDir(), tarPath).Run(&test.Expected{}) + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + dockerfile := `FROM parent +CMD ["echo", "test-nerdctl-build-context-oci-layout"]` + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + var cmd test.TestableCommand + 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 6b9052f9d12..59d3fa65767 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -17,41 +17,154 @@ package builder import ( + "encoding/json" "fmt" "os" "path/filepath" + "runtime" "strings" "testing" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/platforms" + + 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() +func TestBuildBAB(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + 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") + }, + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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), + }, + }, + } - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) + testCase.Run(t) +} - buildCtx := helpers.CreateBuildContext(t, dockerfile) +func TestCanBuildOnOtherPlatform(t *testing.T) { + nerdtest.Setup() - 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") + requireEmulation := &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + host, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(t, err) + var plt []struct { + Platforms []platforms.Platform + } + cmd := helpers.Custom("buildctl", "--addr", host, "debug", "workers", "--format", "json") + cmd.Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + err = json.Unmarshal([]byte(stdout), &plt) + assert.NilError(t, err, info) + }, + }) + + 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 + } + } - ignoredImageNamed := imageName + "-" + "ignored" - outputOpt := fmt.Sprintf("--output=type=docker,name=%s", ignoredImageNamed) - base.Cmd("build", buildCtx, "-t", imageName, outputOpt).AssertOK() + mess := "buildkit worker does not support emulation" + ret := found != nil + if ret { + mess = "buildkit worker does support emulation" + data.Set("OS", found.OS) + data.Set("Arch", found.Architecture) + } - base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n") - base.Cmd("run", "--rm", ignoredImageNamed).AssertFail() + return ret, mess + }, + } + + testCase := &test.Case{ + Require: test.Require( + nerdtest.Build, + requireEmulation, + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("build", data.Get("buildCtx"), "--platform", fmt.Sprintf("%s/%s", data.Get("OS"), data.Get("Architecture")), "-t", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + } + + testCase.Run(t) } // TestBuildBaseImage tests if an image can be built on the previously built image. @@ -70,7 +183,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() @@ -80,7 +193,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() @@ -113,7 +226,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() @@ -179,7 +292,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) @@ -218,7 +331,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") @@ -261,7 +374,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() @@ -284,7 +397,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() @@ -307,7 +420,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() @@ -360,7 +473,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") @@ -375,7 +488,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("") @@ -390,7 +503,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("") @@ -415,7 +528,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("") @@ -436,7 +549,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) @@ -457,7 +570,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 @@ -513,7 +626,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..f47baa9f4cb 100644 --- a/cmd/nerdctl/builder/builder_linux_test.go +++ b/cmd/nerdctl/builder/builder_linux_test.go @@ -18,136 +18,133 @@ 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.Command("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.Command("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.TestableCommand { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + cmd := helpers.Command("builder", "debug", buildCtx) + cmd.WithStdin(bytes.NewReader([]byte("c\n"))) + return cmd + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "WithPull", + Require: nerdtest.RootFul, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + if bkGC != nil { + bkGC() + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + buildkitConfig := fmt.Sprintf(`[worker.oci] enabled = false [worker.containerd] enabled = true namespace = "%s"`, testutil.Namespace) - cleanup := useBuildkitConfig(t, buildkitConfig) - defer cleanup() - - testCases := []struct { - name string - pull string - }{ - { - name: "build with local image", - pull: "false", - }, - { - name: "build with newest image", - pull: "true", - }, - { - name: "build with buildkit default", - // buildkit default pulls from remote - pull: "default", + bkGC = useBuildkitConfig(t, buildkitConfig) + oldImage := testutil.BusyboxImage + oldImageSha := "141c253bc4c3fd0a201d32dc1f493bcf3fff003b6df416dea4f41046e0f37d47" + newImage := testutil.AlpineImage + + helpers.Ensure("pull", oldImage) + helpers.Ensure("tag", oldImage, newImage) + + dockerfile := fmt.Sprintf(`FROM %s`, newImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + + data.Set("buildCtx", buildCtx) + data.Set("oldImageSha", oldImageSha) + }, + SubTests: []*test.Case{ + { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + 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.TestableCommand { + return helpers.Command("build", data.Get("buildCtx"), "--pull=true") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("build", data.Get("buildCtx")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + }, }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Cmd("image", "prune", "--force", "--all").AssertOK() - - base.Cmd("pull", oldImage).Run() - base.Cmd("tag", oldImage, newImage).Run() - - dockerfile := fmt.Sprintf(`FROM %s`, newImage) - tmpDir := t.TempDir() - err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644) - assert.NilError(t, err) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - buildCmd := []string{"build", buildCtx} - switch tc.pull { - case "false": - buildCmd = append(buildCmd, "--pull=false") - base.Cmd(buildCmd...).AssertErrContains(oldImageSha) - case "true": - buildCmd = append(buildCmd, "--pull=true") - base.Cmd(buildCmd...).AssertErrNotContains(oldImageSha) - case "default": - base.Cmd(buildCmd...).AssertErrNotContains(oldImageSha) - } - }) - } + testCase.Run(t) } func useBuildkitConfig(t *testing.T, config string) (cleanup func()) { diff --git a/cmd/nerdctl/completion/completion_linux_test.go b/cmd/nerdctl/completion/completion_linux_test.go index 234c9a21ebc..81156e15b16 100644 --- a/cmd/nerdctl/completion/completion_linux_test.go +++ b/cmd/nerdctl/completion/completion_linux_test.go @@ -28,8 +28,7 @@ func TestCompletion(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ - Description: "Base completion", - Require: test.Not(nerdtest.Docker), + Require: test.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("pull", testutil.AlpineImage) helpers.Ensure("network", "create", data.Identifier()) @@ -43,37 +42,37 @@ func TestCompletion(t *testing.T) { SubTests: []*test.Case{ { Description: "--cgroup-manager", - Command: test.RunCommand("__complete", "--cgroup-manager", ""), + Command: test.Command("__complete", "--cgroup-manager", ""), Expected: test.Expects(0, nil, test.Contains("cgroupfs\n")), }, { Description: "--snapshotter", - Command: test.RunCommand("__complete", "--snapshotter", ""), + Command: test.Command("__complete", "--snapshotter", ""), Expected: test.Expects(0, nil, test.Contains("native\n")), }, { Description: "empty", - Command: test.RunCommand("__complete", ""), + Command: test.Command("__complete", ""), Expected: test.Expects(0, nil, test.Contains("run\t")), }, { Description: "run -", - Command: test.RunCommand("__complete", "run", "-"), + Command: test.Command("__complete", "run", "-"), Expected: test.Expects(0, nil, test.Contains("--network\t")), }, { Description: "run --n", - Command: test.RunCommand("__complete", "run", "--n"), + Command: test.Command("__complete", "run", "--n"), Expected: test.Expects(0, nil, test.Contains("--network\t")), }, { Description: "run --ne", - Command: test.RunCommand("__complete", "run", "--ne"), + Command: test.Command("__complete", "run", "--ne"), Expected: test.Expects(0, nil, test.Contains("--network\t")), }, { Description: "run --net", - Command: test.RunCommand("__complete", "run", "--net", ""), + Command: test.Command("__complete", "run", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -85,7 +84,7 @@ func TestCompletion(t *testing.T) { }, { Description: "run -it --net", - Command: test.RunCommand("__complete", "run", "-it", "--net", ""), + Command: test.Command("__complete", "run", "-it", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -97,7 +96,7 @@ func TestCompletion(t *testing.T) { }, { Description: "run -ti --rm --net", - Command: test.RunCommand("__complete", "run", "-it", "--rm", "--net", ""), + Command: test.Command("__complete", "run", "-it", "--rm", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -109,12 +108,12 @@ func TestCompletion(t *testing.T) { }, { Description: "run --restart", - Command: test.RunCommand("__complete", "run", "--restart", ""), + Command: test.Command("__complete", "run", "--restart", ""), Expected: test.Expects(0, nil, test.Contains("always\n")), }, { Description: "network --rm", - Command: test.RunCommand("__complete", "network", "rm", ""), + Command: test.Command("__complete", "network", "rm", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.All( @@ -126,7 +125,7 @@ func TestCompletion(t *testing.T) { }, { Description: "run --cap-add", - Command: test.RunCommand("__complete", "run", "--cap-add", ""), + Command: test.Command("__complete", "run", "--cap-add", ""), Expected: test.Expects(0, nil, test.All( test.Contains("sys_admin\n"), test.DoesNotContain("CAP_SYS_ADMIN\n"), @@ -134,7 +133,7 @@ func TestCompletion(t *testing.T) { }, { Description: "volume inspect", - Command: test.RunCommand("__complete", "volume", "inspect", ""), + Command: test.Command("__complete", "volume", "inspect", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.Contains(data.Get("identifier") + "\n"), @@ -143,7 +142,7 @@ func TestCompletion(t *testing.T) { }, { Description: "volume rm", - Command: test.RunCommand("__complete", "volume", "rm", ""), + Command: test.Command("__complete", "volume", "rm", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.Contains(data.Get("identifier") + "\n"), @@ -152,52 +151,46 @@ func TestCompletion(t *testing.T) { }, { Description: "no namespace --cgroup-manager", - Command: func(data test.Data, helpers test.Helpers) test.Command { - cmd := helpers.Command() - cmd.Clear() - cmd.WithBinary("nerdctl") - cmd.WithArgs("__complete", "--cgroup-manager", "") - return cmd + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("nerdctl", "__complete", "--cgroup-manager", "") }, Expected: test.Expects(0, nil, test.Contains("cgroupfs\n")), }, { Description: "no namespace empty", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("__complete", "") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("nerdctl", "__complete", "") }, Expected: test.Expects(0, nil, test.Contains("run\t")), }, { Description: "namespace space empty", - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} - return helpers.Command().Clear().WithBinary("nerdctl"). - WithArgs("__complete", "--namespace", testutil.Namespace, "") + return helpers.Custom("nerdctl", "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "") }, Expected: test.Expects(0, nil, test.Contains("run\t")), }, { Description: "run -i", - Command: test.RunCommand("__complete", "run", "-i", ""), + Command: test.Command("__complete", "run", "-i", ""), Expected: test.Expects(0, nil, test.Contains(testutil.AlpineImage)), }, { Description: "run -it", - Command: test.RunCommand("__complete", "run", "-it", ""), + Command: test.Command("__complete", "run", "-it", ""), Expected: test.Expects(0, nil, test.Contains(testutil.AlpineImage)), }, { Description: "run -it --rm", - Command: test.RunCommand("__complete", "run", "-it", "--rm", ""), + Command: test.Command("__complete", "run", "-it", "--rm", ""), Expected: test.Expects(0, nil, test.Contains(testutil.AlpineImage)), }, { Description: "namespace run -i", - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"} - return helpers.Command().Clear().WithBinary("nerdctl"). - WithArgs("__complete", "--namespace", testutil.Namespace, "run", "-i", "") + return helpers.Custom("nerdctl", "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "run", "-i", "") }, Expected: test.Expects(0, nil, test.Contains(testutil.AlpineImage+"\n")), }, diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index 71a74eae59e..69c8cf3c6f6 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -24,81 +24,99 @@ 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) { + testCase := 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 - ))), + )) + + cmd := helpers. + Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithWrapper("unbuffer", "-p") + cmd.WithStdin(si) + cmd.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"))), + testCase.SubTests = []*test.Case{ + { + 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.TestableCommand { + 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. + cmd := helpers.Command("attach", data.Identifier()) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, 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.TestableCommand { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("attach", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, + }, } - // `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code, - // so the exit code cannot be easily tested here. - base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", containerName).CmdOption(opts...).AssertOutContains("2") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, false) -} - -func TestAttachDetachKeys(t *testing.T) { - t.Parallel() - skipAttachForDocker(t) - - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - prepareContainerToAttach(base, containerName) - - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader( - []byte{1, 2}, // https://www.physics.udel.edu/~watson/scen103/ascii.html - ))), - } - base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", "--detach-keys=ctrl-a,ctrl-b", containerName). - CmdOption(opts...).AssertOutContains("read detach keys") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_commit_test.go b/cmd/nerdctl/container/container_commit_test.go index f9f553d9ca1..98b11b5eb3a 100644 --- a/cmd/nerdctl/container/container_commit_test.go +++ b/cmd/nerdctl/container/container_commit_test.go @@ -17,39 +17,63 @@ package container import ( - "fmt" "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/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() + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + 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.TestableCommand { + helpers.Ensure( + "commit", + "-c", `CMD ["/foo"]`, + "-c", `ENTRYPOINT ["cat"]`, + "--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.TestableCommand { + helpers.Ensure( + "commit", + "-c", `CMD ["/foo"]`, + "-c", `ENTRYPOINT ["cat"]`, + "--pause=false", + data.Identifier(), data.Identifier()) + return helpers.Command("run", "--rm", data.Identifier()) + }, + Expected: test.Expects(0, nil, test.Equals("hello-test-commit\n")), + }, } + + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index ff5e076d241..56b71ce854a 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -185,9 +185,9 @@ func TestCreateWithTty(t *testing.T) { // TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993 func TestIssue2993(t *testing.T) { - testutil.DockerIncompatible(t) + testCase := nerdtest.Setup() - nerdtest.Setup() + testCase.Require = test.Not(nerdtest.Docker) const ( containersPathKey = "containersPath" @@ -203,7 +203,7 @@ func TestIssue2993(t *testing.T) { return h } - testCase := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "Issue #2993 - nerdctl no longer leaks containers and etchosts directories and files when container creation fails.", Setup: func(data test.Data, helpers test.Helpers) { @@ -233,7 +233,7 @@ func TestIssue2993(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "--data-root", data.TempDir(), "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--data-root", data.TempDir(), "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", "infinity") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -281,7 +281,7 @@ func TestIssue2993(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("--data-root", data.TempDir(), "rm", "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("--data-root", data.TempDir(), "rm", "-f", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index ec9fbd2ce70..5f9e3d30aa6 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -95,6 +95,8 @@ func TestLogsWithInheritedFlags(t *testing.T) { base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar").AssertOK() + // NOTE: seen with Docker: there are circumstances where this happens too fast and we get foo + time.Sleep(1 * time.Second) // test rootCmd alias `-n` already used in logs subcommand base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error { if !(stdout == "bar\n" || stdout == "") { diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index aca549d9446..a177ec0864e 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -36,9 +36,10 @@ import ( "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/strutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestRunCustomRootfs(t *testing.T) { @@ -458,64 +459,96 @@ func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) { } func TestRunWithOOMScoreAdj(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("test skipped for rootless containers.") + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestStartDetachKeys", + Require: nerdtest.RootFul, + Command: test.Command("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.TestableCommand { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - opts := []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))), // https://www.physics.udel.edu/~watson/scen103/ascii.html - } - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - // - // "-p" is needed because we need unbuffer to read from stdin, and from [1]: - // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. - // To use unbuffer in a pipeline, use the -p flag." - // - // [1] https://linux.die.net/man/1/unbuffer - base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", containerName, testutil.CommonImage). - CmdOption(opts...).AssertOutContains("read detach keys") - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testCase.Run(t) } func TestRunWithTtyAndDetached(t *testing.T) { - base := testutil.NewBase(t) - imageName := testutil.CommonImage - withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) - withTtyContainerName := "with-terminal-" + testutil.Identifier(t) - - // without -t, fail - base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() - base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") - withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) - assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) - - // with -t, success - base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() - defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() - base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") - withTtyContainer := base.InspectContainer(withTtyContainerName) - assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + 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.TestableCommand { + 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.TestableCommand { + 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) + }), + } + }, + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index 4fe6f2d249c..ac2da9d90ae 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -24,41 +24,52 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestStartDetachKeys(t *testing.T) { - t.Parallel() + nerdtest.Setup() - skipAttachForDocker(t) + testCase := &test.Case{ + Description: "TestStartDetachKeys", + Require: test.Binary("unbuffer"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + si := testutil.NewDelayOnceReader(strings.NewReader("exit\n")) - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - opts := []func(*testutil.Cmd){ - // If NewDelayOnceReader is not used, - // the container state will be Created instead of Exited. - // Maybe `unbuffer` exits too early in that case? - testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("exit\n"))), + cmd := helpers. + Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithWrapper("unbuffer", "-p") + cmd.WithStdin(si) + cmd.Run(&test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, false, info) + }), + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + si := testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2})) + cmd := helpers. + Command("start", "-a", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) + cmd.WithStdin(si) + cmd.WithWrapper("unbuffer", "-p") + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Equal(t, container.State.Running, true, info) + }, + } + }, } - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - // - // "-p" is needed because we need unbuffer to read from stdin, and from [1]: - // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations. - // To use unbuffer in a pipeline, use the -p flag." - // - // [1] https://linux.die.net/man/1/unbuffer - base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage). - CmdOption(opts...).AssertOK() - container := base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, false) - opts = []func(*testutil.Cmd){ - testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))), // https://www.physics.udel.edu/~watson/scen103/ascii.html - } - base.CmdWithHelper([]string{"unbuffer", "-p"}, "start", "-a", "--detach-keys=ctrl-a,ctrl-b", containerName). - CmdOption(opts...).AssertOutContains("read detach keys") - container = base.InspectContainer(containerName) - assert.Equal(base.T, container.State.Running, true) + testCase.Run(t) + } diff --git a/cmd/nerdctl/helpers/testing.go b/cmd/nerdctl/helpers/testing.go index a9c633faa13..c7f460b19a0 100644 --- a/cmd/nerdctl/helpers/testing.go +++ b/cmd/nerdctl/helpers/testing.go @@ -17,11 +17,22 @@ package helpers import ( + "context" + "encoding/json" + "errors" + "fmt" "os" + "os/exec" "path/filepath" "testing" "gotest.tools/v3/assert" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" ) func CreateBuildContext(t *testing.T, dockerfile string) string { @@ -30,3 +41,97 @@ func CreateBuildContext(t *testing.T, dockerfile string) string { assert.NilError(t, err) return tmpDir } + +func RmiAll(base *testutil.Base) { + base.T.Logf("Pruning images") + imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + // remove empty output line at the end + imageIDs = imageIDs[:len(imageIDs)-1] + // use `Run` on purpose (same below) because `rmi all` may fail on individual + // image id that has an expected running container (e.g. a registry) + base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() + + base.T.Logf("Pruning build caches") + if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { + base.Cmd("builder", "prune", "--force").AssertOK() + } + + // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs + // https://github.com/containerd/nerdctl/pull/1833 + if base.Target == testutil.Nerdctl { + base.T.Logf("Pruning all content blobs") + addr := base.ContainerdAddress() + client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) + assert.NilError(base.T, err) + cs := client.ContentStore() + ctx := context.TODO() + wf := func(info content.Info) error { + base.T.Logf("Pruning blob %+v", info) + if err := cs.Delete(ctx, info.Digest); err != nil { + base.T.Log(err) + } + return nil + } + if err := cs.Walk(ctx, wf); err != nil { + base.T.Log(err) + } + + base.T.Logf("Pruning all images (again?)") + imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() + base.T.Logf("pruning following images: %+v", imageIDs) + base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() + } +} + +func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { + if err := os.MkdirAll(rootfsPath, 0755); err != nil { + return err + } + workDir, err := os.MkdirTemp("", "extract-docker-archive") + if err != nil { + return err + } + defer os.RemoveAll(workDir) + if err := ExtractTarFile(workDir, archiveTarPath); err != nil { + return err + } + manifestJSONPath := filepath.Join(workDir, "manifest.json") + manifestJSONBytes, err := os.ReadFile(manifestJSONPath) + if err != nil { + return err + } + var mani DockerArchiveManifestJSON + if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { + return err + } + if len(mani) > 1 { + return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani)) + } + if len(mani) < 1 { + return errors.New("invalid archive") + } + ent := mani[0] + for _, l := range ent.Layers { + layerTarPath := filepath.Join(workDir, l) + if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil { + return err + } + } + return nil +} + +type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry + +type DockerArchiveManifestJSONEntry struct { + Config string + RepoTags []string + Layers []string +} + +func ExtractTarFile(dirPath, tarFilePath string) error { + cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + } + return nil +} diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go index ec638021b11..c50e16ae06c 100644 --- a/cmd/nerdctl/helpers/testing_linux.go +++ b/cmd/nerdctl/helpers/testing_linux.go @@ -17,9 +17,6 @@ package helpers import ( - "context" - "encoding/json" - "errors" "fmt" "io" "net" @@ -32,10 +29,6 @@ import ( "gotest.tools/v3/assert" - containerd "github.com/containerd/containerd/v2/client" - "github.com/containerd/containerd/v2/core/content" - - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) @@ -137,47 +130,6 @@ func NewCosignKeyPair(t testing.TB, path string, password string) *CosignKeyPair } } -func RmiAll(base *testutil.Base) { - base.T.Logf("Pruning images") - imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() - // remove empty output line at the end - imageIDs = imageIDs[:len(imageIDs)-1] - // use `Run` on purpose (same below) because `rmi all` may fail on individual - // image id that has an expected running container (e.g. a registry) - base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() - - base.T.Logf("Pruning build caches") - if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil { - base.Cmd("builder", "prune", "--force").AssertOK() - } - - // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs - // https://github.com/containerd/nerdctl/pull/1833 - if base.Target == testutil.Nerdctl { - base.T.Logf("Pruning all content blobs") - addr := base.ContainerdAddress() - client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace)) - assert.NilError(base.T, err) - cs := client.ContentStore() - ctx := context.TODO() - wf := func(info content.Info) error { - base.T.Logf("Pruning blob %+v", info) - if err := cs.Delete(ctx, info.Digest); err != nil { - base.T.Log(err) - } - return nil - } - if err := cs.Walk(ctx, wf); err != nil { - base.T.Log(err) - } - - base.T.Logf("Pruning all images (again?)") - imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines() - base.T.Logf("pruning following images: %+v", imageIDs) - base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run() - } -} - func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) { comp := testutil.NewComposeDir(t, dockerComposeYAML) defer comp.CleanUp() @@ -228,56 +180,3 @@ func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail() base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() } - -func ExtractDockerArchive(archiveTarPath, rootfsPath string) error { - if err := os.MkdirAll(rootfsPath, 0755); err != nil { - return err - } - workDir, err := os.MkdirTemp("", "extract-docker-archive") - if err != nil { - return err - } - defer os.RemoveAll(workDir) - if err := ExtractTarFile(workDir, archiveTarPath); err != nil { - return err - } - manifestJSONPath := filepath.Join(workDir, "manifest.json") - manifestJSONBytes, err := os.ReadFile(manifestJSONPath) - if err != nil { - return err - } - var mani DockerArchiveManifestJSON - if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil { - return err - } - if len(mani) > 1 { - return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani)) - } - if len(mani) < 1 { - return errors.New("invalid archive") - } - ent := mani[0] - for _, l := range ent.Layers { - layerTarPath := filepath.Join(workDir, l) - if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil { - return err - } - } - return nil -} - -type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry - -type DockerArchiveManifestJSONEntry struct { - Config string - RepoTags []string - Layers []string -} - -func ExtractTarFile(dirPath, tarFilePath string) error { - cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) - } - return nil -} diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index ae90cca5af9..7e25b411c5a 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -20,63 +20,125 @@ import ( "fmt" "testing" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestImageConvertNydus(t *testing.T) { - testutil.RequireExecutable(t, "nydus-image") - testutil.DockerIncompatible(t) +func TestImageConvert(t *testing.T) { + nerdtest.Setup() - base := testutil.NewBase(t) - t.Parallel() + testCase := &test.Case{ + Description: "Test image conversion", + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "esgz", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--estargz", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "nydus", + Require: test.Require( + test.Binary("nydus-image"), + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--nydus", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstd", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstdchunked", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3", + testutil.CommonImage, data.Identifier("converted-image")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + } - convertedImage := testutil.Identifier(t) + ":nydus" - base.Cmd("rmi", convertedImage).Run() - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("image", "convert", "--nydus", "--oci", - testutil.CommonImage, convertedImage).AssertOK() - defer base.Cmd("rmi", convertedImage).Run() + testCase.Run(t) - // use `nydusify` check whether the convertd nydus image is valid +} - // skip if rootless - if rootlessutil.IsRootless() { - t.Skip("Nydusify check is not supported rootless mode.") - } +func TestImageConvertNydusVerify(t *testing.T) { + nerdtest.Setup() - // skip if nydusify and nydusd are not installed - testutil.RequireExecutable(t, "nydusify") - testutil.RequireExecutable(t, "nydusd") + const remoteImageKey = "remoteImageKey" - // setup local docker registry - registry := testregistry.NewWithNoAuth(base, 0, false) - remoteImage := fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port) - t.Cleanup(func() { - base.Cmd("rmi", remoteImage).Run() - registry.Cleanup(nil) - }) + var registry *testregistry.RegistryServer - base.Cmd("tag", convertedImage, remoteImage).AssertOK() - base.Cmd("push", remoteImage).AssertOK() - nydusifyCmd := testutil.Cmd{ - Cmd: icmd.Command( - "nydusify", - "check", - "--source", - testutil.CommonImage, - "--target", - remoteImage, - "--source-insecure", - "--target-insecure", + testCase := &test.Case{ + Description: "TestImageConvertNydusVerify", + Require: test.Require( + test.Linux, + test.Binary("nydus-image"), + test.Binary("nydusify"), + test.Binary("nydusd"), + test.Not(nerdtest.Docker), + nerdtest.RootFul, ), - Base: base, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + data.Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port)) + helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image")) + helpers.Ensure("tag", data.Identifier("converted-image"), data.Get(remoteImageKey)) + helpers.Ensure("push", data.Get(remoteImageKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("converted-image")) + if registry != nil { + registry.Cleanup(nil) + helpers.Anyhow("rmi", data.Get(remoteImageKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("nydusify", + "check", + "--source", + testutil.CommonImage, + "--target", + data.Get(remoteImageKey), + "--source-insecure", + "--target-insecure", + ) + }, + Expected: test.Expects(0, nil, nil), } - // nydus is creating temporary files - make sure we are in a proper location for that - nydusifyCmd.Cmd.Dir = base.T.TempDir() - nydusifyCmd.AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_convert_test.go b/cmd/nerdctl/image/image_convert_test.go deleted file mode 100644 index ca5780597d3..00000000000 --- a/cmd/nerdctl/image/image_convert_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "runtime" - "testing" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestImageConvert(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("no windows support yet") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - t.Parallel() - - base.Cmd("pull", testutil.CommonImage).AssertOK() - - testCases := []struct { - identifier string - args []string - }{ - { - "esgz", - []string{"--estargz"}, - }, - { - "zstd", - []string{"--zstd", "--zstd-compression-level", "3"}, - }, - { - "zstdchunked", - []string{"--zstdchunked", "--zstdchunked-compression-level", "3"}, - }, - } - - for _, tc := range testCases { - convertedImage := testutil.Identifier(t) + ":" + tc.identifier - args := append([]string{"image", "convert", "--oci"}, tc.args...) - args = append(args, testutil.CommonImage, convertedImage) - - t.Run(tc.identifier, func(t *testing.T) { - t.Parallel() - - base.Cmd("rmi", convertedImage).Run() - t.Cleanup(func() { - base.Cmd("rmi", convertedImage).Run() - }) - - base.Cmd(args...).AssertOK() - }) - } -} diff --git a/cmd/nerdctl/image/image_encrypt_linux_test.go b/cmd/nerdctl/image/image_encrypt_linux_test.go index 80ff117c007..136e03cdd95 100644 --- a/cmd/nerdctl/image/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image/image_encrypt_linux_test.go @@ -18,39 +18,65 @@ package image import ( "fmt" + "strings" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) func TestImageEncryptJWE(t *testing.T) { - testutil.RequiresBuild(t) - testutil.DockerIncompatible(t) - keyPair := helpers.NewJWEKeyPair(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - - defer keyPair.Cleanup() - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.Port, tID) - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef).AssertOK() - base.Cmd("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef).AssertOutExactly("1\n") - base.Cmd("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") - base.Cmd("push", encryptImageRef).AssertOK() - - defer base.Cmd("rmi", encryptImageRef).Run() - - // remove all local images (in the nerdctl-test namespace), to ensure that we do not have blobs of the original image. - helpers.RmiAll(base) - base.Cmd("pull", encryptImageRef).AssertFail() // defaults to --unpack=true, and fails due to missing prv key - base.Cmd("pull", "--unpack=false", encryptImageRef).AssertOK() - decryptImageRef := tID + ":decrypted" - defer base.Cmd("rmi", decryptImageRef).Run() - base.Cmd("image", "decrypt", "--key="+keyPair.Pub, encryptImageRef, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.Prv, encryptImageRef, decryptImageRef).AssertOK() + nerdtest.Setup() + + var registry *testregistry.RegistryServer + var keyPair *testhelpers.JweKeyPair + + const remoteImageKey = "remoteImageKey" + + testCase := &test.Case{ + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + // This test needs to rmi the common image + nerdtest.Private, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + keyPair.Cleanup() + helpers.Anyhow("rmi", "-f", data.Get(remoteImageKey)) + } + helpers.Anyhow("rmi", "-f", data.Identifier("decrypted")) + }, + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 0, false) + keyPair = testhelpers.NewJWEKeyPair(t) + helpers.Ensure("pull", testutil.CommonImage) + encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", registry.Port, data.Identifier()) + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef) + inspector := helpers.Capture("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef) + assert.Equal(t, inspector, "1\n") + inspector = helpers.Capture("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef) + assert.Assert(t, strings.Contains(inspector, "org.opencontainers.image.enc.keys.jwe")) + helpers.Ensure("push", encryptImageRef) + helpers.Anyhow("rmi", "-f", encryptImageRef) + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + data.Set(remoteImageKey, encryptImageRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Fail("pull", data.Get(remoteImageKey)) + helpers.Ensure("pull", "--unpack=false", data.Get(remoteImageKey)) + helpers.Fail("image", "decrypt", "--key="+keyPair.Pub, data.Get(remoteImageKey), data.Identifier("decrypted")) // decryption needs prv key, not pub key + return helpers.Command("image", "decrypt", "--key="+keyPair.Prv, data.Get(remoteImageKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(0, nil, nil), + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index 21bef4f5692..1d2e07e97fc 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -18,9 +18,8 @@ package image import ( "encoding/json" - "fmt" + "errors" "io" - "runtime" "strings" "testing" "time" @@ -28,6 +27,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) type historyObj struct { @@ -39,53 +40,20 @@ type historyObj struct { Comment string } -func imageHistoryJSONHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) []historyObj { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, "--format", "json") - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - fmt.Println(cmdResult.Stderr()) - - dec := json.NewDecoder(strings.NewReader(cmdResult.Stdout())) +func decode(stdout string) ([]historyObj, error) { + dec := json.NewDecoder(strings.NewReader(stdout)) object := []historyObj{} for { var v historyObj if err := dec.Decode(&v); err == io.EOF { break } else if err != nil { - base.T.Fatal(err) + return nil, errors.New("failed to decode history object") } object = append(object, v) } - return object -} - -func imageHistoryRawHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) string { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - return cmdResult.Stdout() + return object, nil } func TestImageHistory(t *testing.T) { @@ -97,69 +65,88 @@ func TestImageHistory(t *testing.T) { // possibly one is unpacked on the filessystem while the other is the tar file size? // - we do not truncate ids when --quiet has been provided // this is a conscious decision here - truncating with --quiet does not make much sense - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - - // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? - // Disabling for now - if runtime.GOOS == "windows" { - t.Skip("Windows is not supported for this test right now") - } - // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms - if runtime.GOARCH != "arm64" { - t.Skip("Windows is not supported for this test right now") + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImageHistory", + Require: test.Require( + test.Not(nerdtest.Docker), + // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? + test.Not(test.Windows), + // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms + test.Arm64, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--platform", "linux/arm64", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "trunc, no quiet, human", + Command: test.Command("image", "history", "--human=true", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, len(history), 2, info) + assert.Equal(t, history[0].Size, "0B", info) + // FIXME: how is this going to age? + assert.Equal(t, history[0].CreatedSince, "3 years ago", info) + assert.Equal(t, history[0].Snapshot, "", info) + assert.Equal(t, history[0].Comment, "", info) + + localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") + localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") + compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) + compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) + assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String(), info) + assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", info) + assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String(), info) + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", info) + + assert.Equal(t, history[1].Size, "5.947MB", info) + assert.Equal(t, history[1].CreatedSince, "3 years ago", info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info) + assert.Equal(t, history[1].Comment, "", info) + }), + }, + { + Description: "no human - dates and sizes and not prettyfied", + Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[0].Size, "0", info) + assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt, info) + assert.Equal(t, history[1].Size, "5947392", info) + assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt, info) + }), + }, + { + Description: "no trunc - do not truncate sha or cmd", + Command: test.Command("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") + }), + }, + { + Description: "Quiet has no effect with format, so, go no-json, no-trunc", + Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + { + Description: "With quiet, trunc has no effect", + Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + }, } - base.Cmd("pull", "--platform", "linux/arm64", testutil.CommonImage).AssertOK() - - localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") - localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") - - // Human, no quiet, truncate - history := imageHistoryJSONHelper(base, testutil.CommonImage, false, false, true) - compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) - compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) - - // Two layers - assert.Equal(base.T, len(history), 2) - // First layer is a comment - zero size, no snap, - assert.Equal(base.T, history[0].Size, "0B") - assert.Equal(base.T, history[0].CreatedSince, "3 years ago") - assert.Equal(base.T, history[0].Snapshot, "") - assert.Equal(base.T, history[0].Comment, "") - - assert.Equal(base.T, compTime1.UTC().String(), localTimeL1.UTC().String()) - assert.Equal(base.T, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]") - - assert.Equal(base.T, compTime2.UTC().String(), localTimeL2.UTC().String()) - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…") - - assert.Equal(base.T, history[1].Size, "5.947MB") - assert.Equal(base.T, history[1].CreatedSince, "3 years ago") - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…") - assert.Equal(base.T, history[1].Comment, "") - - // No human - dates and sizes and not prettyfied - history = imageHistoryJSONHelper(base, testutil.CommonImage, false, false, false) - - assert.Equal(base.T, history[0].Size, "0") - assert.Equal(base.T, history[0].CreatedSince, history[0].CreatedAt) - - assert.Equal(base.T, history[1].Size, "5947392") - assert.Equal(base.T, history[1].CreatedSince, history[1].CreatedAt) - - // No trunc - do not truncate sha or cmd - history = imageHistoryJSONHelper(base, testutil.CommonImage, true, false, true) - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") - - // Quiet has no effect with format, so, go no-json, no-trunc - rawHistory := imageHistoryRawHelper(base, testutil.CommonImage, true, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") - - // With quiet, trunc has no effect - rawHistory = imageHistoryRawHelper(base, testutil.CommonImage, false, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 5a0f7111a28..93278de9294 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -40,7 +40,7 @@ func TestImageInspectSimpleCases(t *testing.T) { SubTests: []*test.Case{ { Description: "Contains some stuff", - Command: test.RunCommand("image", "inspect", testutil.CommonImage), + Command: test.Command("image", "inspect", testutil.CommonImage), Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { var dc []dockercompat.Image err := json.Unmarshal([]byte(stdout), &dc) @@ -53,12 +53,12 @@ func TestImageInspectSimpleCases(t *testing.T) { }, { Description: "RawFormat support (.Id)", - Command: test.RunCommand("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}"), + Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}"), Expected: test.Expects(0, nil, nil), }, { Description: "typedFormat support (.ID)", - Command: test.RunCommand("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}"), + Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}"), Expected: test.Expects(0, nil, nil), }, }, @@ -81,7 +81,6 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { } testCase := &test.Case{ - Description: "TestImageInspectDifferentValidReferencesForTheSameImage", Require: test.Require( test.Not(nerdtest.Docker), test.Not(test.Windows), @@ -96,7 +95,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { SubTests: []*test.Case{ { Description: "name and tags +/- sha combinations", - Command: test.RunCommand("image", "inspect", "busybox"), + Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { @@ -121,7 +120,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { }, { Description: "by digest, short or long, with or without prefix", - Command: test.RunCommand("image", "inspect", "busybox"), + Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { @@ -154,7 +153,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { }, { Description: "prove that wrong references with correct digest do not get resolved", - Command: test.RunCommand("image", "inspect", "busybox"), + Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { @@ -176,7 +175,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { }, { Description: "prove that invalid reference return no result without crashing", - Command: test.RunCommand("image", "inspect", "busybox"), + Command: test.Command("image", "inspect", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { @@ -197,7 +196,7 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { }, { Description: "retrieving multiple entries at once", - Command: test.RunCommand("image", "inspect", "busybox", "busybox"), + Command: test.Command("image", "inspect", "busybox", "busybox"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index fceb83a0426..332dd40ddf4 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -24,143 +24,302 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/tabutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestImagesWithNames(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("images", "--names", testutil.CommonImage).AssertOutContains(testutil.CommonImage) - base.Cmd("images", "--names", testutil.CommonImage).AssertOutWithFunc(func(out string) error { - lines := strings.Split(strings.TrimSpace(out), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - name, _ := tab.ReadRow(lines[1], "NAME") - assert.Equal(t, name, testutil.CommonImage) - return nil - }) -} - func TestImages(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" - if base.Target == testutil.Docker { - header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImages", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("pull", testutil.NginxAlpineImage) + }, + SubTests: []*test.Case{ + { + Description: "No params", + Command: test.Command("images"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE" + if nerdtest.IsDocker() { + header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE" + } + tab := tabutil.NewReader(header) + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, info) + found := false + for _, line := range lines[1:] { + repo, _ := tab.ReadRow(line, "REPOSITORY") + tag, _ := tab.ReadRow(line, "TAG") + if repo+":"+tag == testutil.CommonImage { + found = true + break + } + } + assert.Assert(t, found, info) + }, + } + }, + }, + { + Description: "With names", + Command: test.Command("images", "--names", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(testutil.CommonImage), + func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE") + err := tab.ParseHeader(lines[0]) + assert.NilError(t, err, info) + found := false + for _, line := range lines[1:] { + name, _ := tab.ReadRow(line, "NAME") + if name == testutil.CommonImage { + found = true + break + } + } + + assert.Assert(t, found, info) + }, + ), + } + }, + }, + { + Description: "CheckCreatedTime", + Command: test.Command("images", "--format", "'{{json .CreatedAt}}'"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, info) + createdTimes := lines + slices.Reverse(createdTimes) + assert.Assert(t, slices.IsSorted(createdTimes), info) + }, + } + }, + }, + }, } - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("images", testutil.CommonImage).AssertOutWithFunc(func(out string) error { - lines := strings.Split(strings.TrimSpace(out), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 2 lines, got %d", len(lines)) - } - tab := tabutil.NewReader(header) - err := tab.ParseHeader(lines[0]) - if err != nil { - return fmt.Errorf("failed to parse header: %v", err) - } - repo, _ := tab.ReadRow(lines[1], "REPOSITORY") - tag, _ := tab.ReadRow(lines[1], "TAG") - assert.Equal(t, repo+":"+tag, testutil.CommonImage) - return nil - }) + testCase.Run(t) } func TestImagesFilter(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - t.Parallel() - base := testutil.NewBase(t) - tempName := testutil.Identifier(base.T) - base.Cmd("pull", testutil.CommonImage).AssertOK() - - dockerfile := fmt.Sprintf(`FROM %s + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagesFilter", + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + // FIXME: we might want to get rid of these and just use tag + helpers.Ensure("pull", "busybox:glibc") + helpers.Ensure("pull", "busybox:uclibc") + + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] \n LABEL foo=bar -LABEL version=0.1`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - base.Cmd("build", "-t", tempName, "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() - defer base.Cmd("rmi", tempName).AssertOK() - - busyboxGlibc, busyboxUclibc := "busybox:glibc", "busybox:uclibc" - base.Cmd("pull", busyboxGlibc).AssertOK() - defer base.Cmd("rmi", busyboxGlibc).AssertOK() - - base.Cmd("pull", busyboxUclibc).AssertOK() - defer base.Cmd("rmi", busyboxUclibc).AssertOK() - - // before/since filters are not compatible with DOCKER_BUILDKIT=1? (but still compatible with DOCKER_BUILDKIT=0) - if base.Target == testutil.Nerdctl { - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutNotContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("since=%s:%s", "non-exists-image", "non-exists-image")).AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", "non-exists-image", "non-exists-image")).AssertOutContains(tempName) +LABEL version=0.1 +RUN echo "actually creating a layer so that docker sets the createdAt time" +`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + data.Set("buildCtx", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "busybox:glibc") + helpers.Anyhow("rmi", "busybox:uclibc") + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + data.Set("builtImageID", data.Identifier()) + return helpers.Command("build", "-t", data.Identifier(), data.Get("buildCtx")) + }, + Expected: test.Expects(0, nil, nil), + SubTests: []*test.Case{ + { + Description: "label=foo=bar", + Command: test.Command("images", "--filter", "label=foo=bar"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar1", + Command: test.Command("images", "--filter", "label=foo=bar1"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.DoesNotContain(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar label=version=0.1", + Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=foo=bar label=version=0.1", + Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.DoesNotContain(data.Get("builtImageID")), + } + }, + }, + { + Description: "label=version", + Command: test.Command("images", "--filter", "label=version"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "reference=ID*", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("images", "--filter", fmt.Sprintf("reference=%s*", data.Get("builtImageID"))) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Get("builtImageID")), + } + }, + }, + { + Description: "reference=busy*:*libc*", + Command: test.Command("images", "--filter", "reference=busy*:*libc*"), + Expected: test.Expects(0, nil, test.All( + test.Contains("glibc"), + test.Contains("uclibc"), + )), + }, + { + Description: "before=ID:latest", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("images", "--filter", fmt.Sprintf("before=%s:latest", data.Get("builtImageID"))) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(testutil.ImageRepo(testutil.CommonImage)), + test.DoesNotContain(data.Get("builtImageID")), + ), + } + }, + }, + { + Description: "since=" + testutil.CommonImage, + Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "since=" + testutil.CommonImage + " " + testutil.CommonImage, + Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "since=non-exists-image", + Command: test.Command("images", "--filter", "since=non-exists-image"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + { + Description: "before=non-exists-image", + Command: test.Command("images", "--filter", "before=non-exists-image"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("builtImageID")), + test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + ), + } + }, + }, + }, } - base.Cmd("images", "--filter", "label=foo=bar").AssertOutContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar1").AssertOutNotContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1").AssertOutContains(tempName) - base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2").AssertOutNotContains(tempName) - base.Cmd("images", "--filter", "label=version").AssertOutContains(tempName) - base.Cmd("images", "--filter", fmt.Sprintf("reference=%s*", tempName)).AssertOutContains(tempName) - base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("glibc") - base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("uclibc") + + testCase.Run(t) } func TestImagesFilterDangling(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Cmd("container", "prune", "-f").AssertOK() - base.Cmd("image", "prune", "--all", "-f").AssertOK() + nerdtest.Setup() - dockerfile := fmt.Sprintf(`FROM %s + testCase := &test.Case{ + Description: "TestImagesFilterDangling", + // This test relies on a clean slate and the ability to GC everything + NoParallel: true, + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK() - - // dangling image test - base.Cmd("images", "--filter", "dangling=true").AssertOutContains("") - base.Cmd("images", "--filter", "dangling=false").AssertOutNotContains("") -} - -func TestImageListCheckCreatedTime(t *testing.T) { - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("pull", testutil.NginxAlpineImage).AssertOK() - - var createdTimes []string - - base.Cmd("images", "--format", "'{{json .CreatedAt}}'").AssertOutWithFunc(func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) < 2 { - return fmt.Errorf("expected at least 4 lines, got %d", len(lines)) - } - createdTimes = append(createdTimes, lines...) - return nil - }) - - slices.Reverse(createdTimes) - if !slices.IsSorted(createdTimes) { - t.Errorf("expected images in decending order") + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + data.Set("buildCtx", buildCtx) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "prune", "-f") + helpers.Anyhow("image", "prune", "--all", "-f") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("build", data.Get("buildCtx")) + }, + Expected: test.Expects(0, nil, nil), + SubTests: []*test.Case{ + { + Description: "dangling", + Command: test.Command("images", "--filter", "dangling=true"), + Expected: test.Expects(0, nil, test.Contains("")), + }, + { + Description: "not dangling", + Command: test.Command("images", "--filter", "dangling=false"), + Expected: test.Expects(0, nil, test.DoesNotContain("")), + }, + }, } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_load_linux_test.go b/cmd/nerdctl/image/image_load_linux_test.go deleted file mode 100644 index 4d7b0f83dce..00000000000 --- a/cmd/nerdctl/image/image_load_linux_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestLoadStdinFromPipe(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - img := testutil.Identifier(t) - tmp := t.TempDir() - output := filepath.Join(tmp, "output") - - setup := func() { - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("tag", testutil.CommonImage, img).AssertOK() - base.Cmd("save", img, "-o", filepath.Join(tmp, "common.tar")).AssertOK() - base.Cmd("rmi", "-f", img).AssertOK() - } - - tearDown := func() { - base.Cmd("rmi", "-f", img).AssertOK() - } - - t.Cleanup(tearDown) - tearDown() - - setup() - - loadCmd := strings.Join(base.Cmd("load").Command, " ") - combined, err := exec.Command("sh", "-euxc", fmt.Sprintf("`cat %s/common.tar | %s > %s`", tmp, loadCmd, output)).CombinedOutput() - assert.NilError(t, err, "failed with error %s and combined output is %s", err, string(combined)) - - fb, err := os.ReadFile(output) - assert.NilError(t, err) - - assert.Assert(t, strings.Contains(string(fb), fmt.Sprintf("Loaded image: %s:latest", img))) - base.Cmd("images").AssertOutContains(img) -} - -func TestLoadStdinEmpty(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("load").AssertFail() -} diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go new file mode 100644 index 00000000000..97f31c04b6e --- /dev/null +++ b/cmd/nerdctl/image/image_load_test.go @@ -0,0 +1,81 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestLoadStdinFromPipe(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinFromPipe", + Require: test.Linux, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + helpers.Ensure("save", data.Identifier(), "-o", filepath.Join(data.TempDir(), "common.tar")) + helpers.Ensure("rmi", "-f", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("load") + reader, err := os.Open(filepath.Join(data.TempDir(), "common.tar")) + assert.NilError(t, err, "failed to open common.tar") + cmd.WithStdin(reader) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(fmt.Sprintf("Loaded image: %s:latest", data.Identifier())), + func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("images"), data.Identifier())) + }, + ), + } + }, + } + + testCase.Run(t) +} + +func TestLoadStdinEmpty(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinEmpty", + Require: test.Linux, + Command: test.Command("load"), + Expected: test.Expects(1, nil, nil), + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index 94ef0c625f2..11804871b52 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -18,117 +18,210 @@ package image import ( "fmt" + "strings" "testing" "time" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestImagePrune(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).AssertOK() - - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", buildCtx).AssertOK() - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images").AssertOutContainsAll(imageName, "") - - base.Cmd("image", "prune", "--force").AssertOutNotContains(imageName) - base.Cmd("images").AssertOutNotContains("") - base.Cmd("images").AssertOutContains(imageName) -} - -func TestImagePruneAll(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - // The following commands will clean up all images, so it should fail at this point. - defer base.Cmd("rmi", imageName).AssertFail() - base.Cmd("images").AssertOutContains(imageName) - - tID := testutil.Identifier(t) - base.Cmd("run", "--name", tID, imageName).AssertOK() - base.Cmd("image", "prune", "--force", "--all").AssertOutNotContains(imageName) - base.Cmd("images").AssertOutContains(imageName) - - base.Cmd("rm", "-f", tID).AssertOK() - base.Cmd("image", "prune", "--force", "--all").AssertOutContains(imageName) - base.Cmd("images").AssertOutNotContains(imageName) -} - -func TestImagePruneFilterLabel(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - t.Cleanup(func() { base.Cmd("rmi", "--force", imageName) }) - - dockerfile := fmt.Sprintf(`FROM %s + testCase := nerdtest.Setup() + + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + testCase.NoParallel = true + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // We need to delete everything here for prune to make any sense + base := testutil.NewBase(t) + testhelpers.RmiAll(base) + } + testCase.SubTests = []*test.Case{ + { + Description: "without all", + NoParallel: true, + Require: test.Require( + // This never worked with Docker - the only reason we ever got was side effects from other tests + // See inline comments. + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s + CMD ["echo", "nerdctl-test-image-prune"] + `, testutil.CommonImage) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + // After we rebuild with tag, docker will no longer show the version from above + // Swapping order does not change anything. + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.Command("image", "prune", "--force"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with all", + Require: test.Require( + // Same as above + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s + CMD ["echo", "nerdctl-test-image-prune"] + `, testutil.CommonImage) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + helpers.Ensure("run", "--name", data.Identifier(), data.Identifier()) + }, + Command: test.Command("image", "prune", "--force", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + helpers.Ensure("rm", "-f", data.Identifier()) + removed := helpers.Capture("image", "prune", "--force", "--all") + assert.Assert(t, strings.Contains(removed, data.Identifier()), info) + imgList = helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with filter label", + Require: nerdtest.Build, + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=baz").AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=bar").AssertOK() - base.Cmd("images", "--all").AssertOutNotContains(imageName) -} - -func TestImagePruneFilterUntil(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - // For deterministically testing the filter, set the image's created timestamp to 2 hours in the past. - base.Env = append(base.Env, fmt.Sprintf("SOURCE_DATE_EPOCH=%d", time.Now().Add(-2*time.Hour).Unix())) - - imageName := testutil.Identifier(t) - teardown := func() { - // Image should have been pruned; but cleanup on failure. - base.Cmd("rmi", "--force", imageName).Run() + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.Command("image", "prune", "--force", "--all", "--filter", "label=foo=baz"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") + assert.Assert(t, strings.Contains(prune, data.Identifier()), info) + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with until", + Require: nerdtest.Build, + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +RUN echo "Anything, so that we create actual content for docker to set the current time for CreatedAt" +CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + data.Set("imageID", data.Identifier()) + }, + Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=12h"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("imageID")), + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Get("imageID")), info) + }, + ), + } + }, + SubTests: []*test.Case{ + { + Description: "Wait and remove until=10ms", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + time.Sleep(1 * time.Second) + }, + Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=10ms"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("imageID")), + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Get("imageID")), imgList, info) + }, + ), + } + }, + }, + }, + }, } - t.Cleanup(teardown) - teardown() - - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-test-image-prune-filter-until"]`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "until=12h").AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - // Pause to ensure enough time has passed for the image to be cleaned on next prune. - time.Sleep(3 * time.Second) - base.Cmd("image", "prune", "--force", "--all", "--filter", "until=10ms").AssertOK() - base.Cmd("images", "--all").AssertOutNotContains(imageName) + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index d3e956238cf..9de6dce1060 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -18,152 +18,217 @@ package image import ( "fmt" - "os/exec" + "strconv" "strings" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestImageVerifyWithCosign(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - testImageRef := fmt.Sprintf("%s:%d/%s", - localhostIP, reg.Port, tID) - t.Logf("testImageRef=%q", testImageRef) - - dockerfile := fmt.Sprintf(`FROM %s +func TestImagePullWithCosign(t *testing.T) { + nerdtest.Setup() + + var registry *testregistry.RegistryServer + var keyPair *testhelpers.CosignKeyPair + + testCase := &test.Case{ + Description: "TestImagePullWithCosign", + Require: test.Require( + test.Linux, + nerdtest.Build, + test.Binary("cosign"), + test.Not(nerdtest.Docker), + ), + Env: map[string]string{ + "COSIGN_PASSWORD": "1", + }, + Setup: func(data test.Data, helpers test.Helpers) { + keyPair = testhelpers.NewCosignKeyPair(t, "cosign-key-pair", "1") + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", testImageRef, buildCtx) + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":one") + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":two") + helpers.Ensure("rmi", "-f", testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if keyPair != nil { + keyPair.Cleanup() + } + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + helpers.Anyhow("rmi", "-f", testImageRef) + } + }, + SubTests: []*test.Case{ + { + Description: "Pull with the correct key", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, testImageRef+":one") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Pull with unrelated key", + Env: map[string]string{ + "COSIGN_PASSWORD": "2", + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + newKeyPair := testhelpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey, testImageRef+":two") + }, + Expected: test.Expects(1, nil, nil), + }, + }, + } - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() + testCase.Run(t) } func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 80, false) - defer reg.Cleanup(nil) - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - t.Logf("testImageRef=%q", testImageRef) - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() - base.Cmd("--insecure-registry", "pull", testImageRef).AssertOK() -} - -func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - testImageRef := fmt.Sprintf("%s:%d/%s", - localhostIP, reg.Port, tID) - t.Logf("testImageRef=%q", testImageRef) - - dockerfile := fmt.Sprintf(`FROM %s + nerdtest.Setup() + + var registry *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "TestImagePullPlainHttpWithDefaultPort", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() - - base.Env = append(base.Env, "COSIGN_PASSWORD=2") - newKeyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey).AssertFail() -} - -func TestPullSoci(t *testing.T) { - testutil.DockerIncompatible(t) - tests := []struct { - name string - sociIndexDigest string - image string - remoteSnapshotsExpectedCount int - }{ - { - name: "Run without specifying SOCI index", - sociIndexDigest: "", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", testImageRef, buildCtx) + helpers.Ensure("--insecure-registry", "push", testImageRef) + helpers.Ensure("rmi", "-f", testImageRef) }, - { - name: "Run with bad SOCI index", - sociIndexDigest: "sha256:thisisabadindex0000000000000000000000000000000000000000000000000", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + return helpers.Command("--insecure-registry", "pull", testImageRef) }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - helpers.RequiresSoci(base) - - //counting initial snapshot mounts - initialMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) + Expected: test.Expects(0, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + helpers.Anyhow("rmi", "-f", testImageRef) } + }, + } - remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge") - - pullOutput := base.Cmd("--snapshotter=soci", "pull", tt.image).Out() - base.T.Logf("pull output: %s", pullOutput) - - actualMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) - } - remoteSnapshotsActualCount := strings.Count(string(actualMounts), "fuse.rawBridge") - base.T.Logf("number of actual mounts: %v", remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - - rmiOutput := base.Cmd("rmi", testutil.FfmpegSociImage).Out() - base.T.Logf("rmi output: %s", rmiOutput) - - base.T.Logf("number of expected mounts: %v", tt.remoteSnapshotsExpectedCount) + testCase.Run(t) +} - if tt.remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) { - t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d", - tt.remoteSnapshotsExpectedCount, remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - } - }) +func TestImagePullSoci(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagePullSoci", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Soci, + ), + + // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount` + // They also feel prone to raciness... + SubTests: []*test.Case{ + { + Description: "Run without specifying SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", ""), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Custom("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), + info) + }, + } + }, + }, + { + Description: "Run with bad SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", "sha256:thisisabadindex0000000000000000000000000000000000000000000000000"), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Custom("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), + info) + }, + } + }, + }, + }, } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 17f757703cf..fa5cf0fe230 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -17,6 +17,7 @@ package image import ( + "errors" "fmt" "net/http" "strings" @@ -24,171 +25,230 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestPushPlainHTTPFails(t *testing.T) { - testutil.RequiresBuild(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - res := base.Cmd("push", testImageRef).Run() - resCombined := res.Combined() - t.Logf("result: exitCode=%d, out=%q", res.ExitCode, res) - assert.Assert(t, res.ExitCode != 0) - assert.Assert(t, strings.Contains(resCombined, "server gave HTTP response to HTTPS client")) -} - -func TestPushPlainHTTPLocalhost(t *testing.T) { - testutil.RequiresBuild(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - localhostIP, reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("push", testImageRef).AssertOK() -} - -func TestPushPlainHTTPInsecure(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushPlainHttpInsecureWithDefaultPort(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 80, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushInsecureWithLogin(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) - defer reg.Cleanup(nil) - - base.Cmd("--insecure-registry", "login", "-u", "admin", "-p", "badmin", - fmt.Sprintf("%s:%d", reg.IP.String(), reg.Port)).AssertOK() - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("push", testImageRef).AssertFail() - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() -} - -func TestPushWithHostsDir(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because Docker doesn't have `--hosts-dir` option, and we don't want to contaminate the global /etc/docker/certs.d during this test - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) - defer reg.Cleanup(nil) - - base.Cmd("--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", reg.IP.String(), reg.Port)).AssertOK() - - base.Cmd("pull", testutil.CommonImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK() - - base.Cmd("--debug", "--hosts-dir", reg.HostsDir, "push", testImageRef).AssertOK() -} - -func TestPushNonDistributableArtifacts(t *testing.T) { - testutil.RequiresBuild(t) - // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon - // Skip docker, because "--allow-nondistributable-artifacts" is a daemon-only option and requires restarting the daemon - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.NonDistBlobImage).AssertOK() - - testImgRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.NonDistBlobImage, ":")[1]) - base.Cmd("tag", testutil.NonDistBlobImage, testImgRef).AssertOK() - - base.Cmd("--debug", "--insecure-registry", "push", testImgRef).AssertOK() - - blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", reg.IP.String(), reg.Port, testutil.Identifier(t), testutil.NonDistBlobDigest) - resp, err := http.Get(blobURL) - assert.Assert(t, err, "error making http request") - if resp.Body != nil { - resp.Body.Close() - } - assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") - - base.Cmd("--debug", "--insecure-registry", "push", "--allow-nondistributable-artifacts", testImgRef).AssertOK() - resp, err = http.Get(blobURL) - assert.Assert(t, err, "error making http request") - if resp.Body != nil { - resp.Body.Close() +func TestPush(t *testing.T) { + nerdtest.Setup() + + var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "Test push", + + Require: test.Linux, + + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registryNoAuthHTTPRandom = testregistry.NewWithNoAuth(base, 0, false) + registryNoAuthHTTPDefault = testregistry.NewWithNoAuth(base, 80, false) + registryTokenAuthHTTPSRandom = testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true) + }, + + Cleanup: func(data test.Data, helpers test.Helpers) { + if registryNoAuthHTTPRandom != nil { + registryNoAuthHTTPRandom.Cleanup(nil) + // XXX might crash + registryNoAuthHTTPDefault.Cleanup(nil) + registryTokenAuthHTTPSRandom.Cleanup(nil) + } + }, + + SubTests: []*test.Case{ + { + Description: "plain http", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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.TestableCommand { + 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..1d897dccbb9 --- /dev/null +++ b/cmd/nerdctl/image/image_remove_test.go @@ -0,0 +1,304 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestRemove(t *testing.T) { + testCase := nerdtest.Setup() + + repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) + nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) + // NOTES: + // - since all of these are rmi-ing the common image, we need private mode + testCase.Require = nerdtest.Private + + testCase.SubTests = []*test.Case{ + { + Description: "Remove image with stopped container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with stopped container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - with -f", + NoParallel: true, + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - without -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NginxAlpineImage) + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("rmi", testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.All( + test.DoesNotContain(repoName), + // a created container with removed image doesn't impact other `rmi` command + test.DoesNotContain(nginxRepoName), + ), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + nerdtest.CGroup, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - with -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + nerdtest.CGroup, + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - without -f", + NoParallel: true, + Require: test.Require( + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - with -f", + NoParallel: true, + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/image/image_save_linux_test.go b/cmd/nerdctl/image/image_save_linux_test.go deleted file mode 100644 index 0c7c722e97e..00000000000 --- a/cmd/nerdctl/image/image_save_linux_test.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestSave(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.AlpineImage).AssertOK() - archiveTarPath := filepath.Join(t.TempDir(), "a.tar") - base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK() - rootfsPath := filepath.Join(t.TempDir(), "rootfs") - err := helpers.ExtractDockerArchive(archiveTarPath, rootfsPath) - assert.NilError(t, err) - etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") - etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) - assert.NilError(t, err) - etcOSRelease := string(etcOSReleaseBytes) - t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage) - t.Log(etcOSRelease) - assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) -} diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index c8078967477..23860cf443e 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -17,54 +17,119 @@ package image import ( + "os" "path/filepath" "strings" "testing" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestSaveById(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] +func TestSaveContent(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "Test content (linux only)", + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("save", "-o", filepath.Join(data.TempDir(), "out.tar"), testutil.CommonImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + rootfsPath := filepath.Join(data.TempDir(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.TempDir(), "out.tar"), rootfsPath) + assert.NilError(t, err) + etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") + etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) + assert.NilError(t, err) + etcOSRelease := string(etcOSReleaseBytes) + assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) + }, + } + }, } - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() + + testCase.Run(t) } -func TestSaveByIdWithDifferentNames(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] - } +func TestSave(t *testing.T) { + testCase := nerdtest.Setup() + + // This test relies on the fact that we can remove the common image, which definitely conflicts with others, + // hence the private mode. + // Further note though, that this will hide the fact this the save command could fail if some layers are missing. + // See https://github.com/containerd/nerdctl/issues/3425 and others for details. + testCase.Require = nerdtest.Private - base.Cmd("tag", testutil.CommonImage, "foobar").AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "Single image, by id", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + // Docker and Nerdctl do not agree on what is the definition of an image ID + if nerdtest.IsDocker() { + 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.TestableCommand { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + { + Description: "Image with different names, by id", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if nerdtest.IsDocker() { + 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.TestableCommand { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + } - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go b/cmd/nerdctl/ipfs/ipfs_build_linux_test.go deleted file mode 100644 index 7ca00803bdb..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package ipfs - -import ( - "fmt" - "strings" - "testing" - "time" - - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestIPFSBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") - - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s -CMD ["echo", "nerdctl-build-test-string"] - `, ipfsCIDBase) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - done := ipfsRegistryUp(t, base) - defer done() - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName).AssertOK() - - base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-build-test-string") -} - -func ipfsRegistryUp(t *testing.T, base *testutil.Base, args ...string) (done func() error) { - res := icmd.StartCmd(base.Cmd(append([]string{"ipfs", "registry", "serve"}, args...)...).Cmd) - time.Sleep(time.Second) - assert.Assert(t, res.Cmd.Process != nil) - assert.NilError(t, res.Error) - return func() error { - res.Cmd.Process.Kill() - icmd.WaitOnCmd(3*time.Second, res) - return nil - } -} diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index 1ac2b682482..7619e0b0591 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -19,84 +19,144 @@ package ipfs import ( "fmt" "io" + "strconv" "strings" "testing" + "time" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSComposeUp(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - tests := []struct { - name string - snapshotter string - pushOptions []string - composeOptions []string - requiresStargz bool - }{ - { - name: "overlayfs", - snapshotter: "overlayfs", - }, - { - name: "stargz", - snapshotter: "stargz", - pushOptions: []string{"--estargz"}, - requiresStargz: true, - }, - { - name: "ipfs-address", - snapshotter: "overlayfs", - pushOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - composeOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - if tt.requiresStargz { - helpers.RequiresStargz(base) - } - ipfsImgs := make([]string, 2) - for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} { - ipfsImgs[i] = pushImageToIPFS(t, base, img, tt.pushOptions...) +func TestIPFSCompNoBuild(t *testing.T) { + testCase := nerdtest.Setup() + + const ipfsAddrKey = "ipfsAddrKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Registry, + nerdtest.IPFS, + // See note below + // nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Start Kubo + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + data.Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)) + + // Ensure we have the images + helpers.Ensure("pull", "--quiet", testutil.WordpressImage) + helpers.Ensure("pull", "--quiet", testutil.MariaDBImage) + } + + testCase.SubTests = []*test.Case{ + subtestTestIPFSCompNoB(t, false, false), + subtestTestIPFSCompNoB(t, true, false), + subtestTestIPFSCompNoB(t, false, true), + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + } + // Speeding up repeat tests... + helpers.Anyhow("rmi", testutil.WordpressImage) + helpers.Anyhow("rmi", testutil.MariaDBImage) + } + + testCase.Run(t) +} + +func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { + t.Helper() + + const ipfsAddrKey = "ipfsAddrKey" + const mariaImageCIDKey = "mariaImageCIDKey" + const wordpressImageCIDKey = "wordpressImageCIDKey" + const composeExtraKey = "composeExtraKey" + + testCase := &test.Case{} + + testCase.Description += "with" + + if stargz { + testCase.Description += "-stargz" + } + + if byAddr { + testCase.Description += "-byAddr" + } + + if stargz { + testCase.Require = nerdtest.Stargz + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var ipfsCIDWP, ipfsCIDMD string + if stargz { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--estargz") + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--estargz") + } else if byAddr { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + data.Set(composeExtraKey, "--ipfs-address="+data.Get(ipfsAddrKey)) + } else { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage) + } + data.Set(wordpressImageCIDKey, ipfsCIDWP) + data.Set(mariaImageCIDKey, ipfsCIDMD) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // NOTE: + // Removing these images locally forces tests to be sequentials (as IPFS being content addressable, + // they have the same cid - except for the estargz version obviously) + // Deliberately electing to not remove them here so that we can parallelize and cut down the running time + /* + if data.Get(mariaImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mariaImageCIDKey)) + helpers.Anyhow("rmi", data.Get(wordpressImageCIDKey)) } - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER="+tt.snapshotter) - helpers.ComposeUp(t, base, fmt.Sprintf(` + */ + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + safePort, err := portlock.Acquire(0) + assert.NilError(helpers.T(), err) + data.Set("wordpressPort", strconv.Itoa(safePort)) + composeUP(data, helpers, fmt.Sprintf(` version: '3.1' services: wordpress: - image: %s + image: ipfs://%s restart: always ports: - - 8080:80 + - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - # workaround for https://github.com/containerd/stargz-snapshotter/issues/444 - - "/run" - wordpress:/var/www/html db: - image: %s + image: ipfs://%s restart: always environment: MYSQL_DATABASE: exampledb @@ -104,53 +164,160 @@ services: MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - # workaround for https://github.com/containerd/stargz-snapshotter/issues/444 - - "/run" - db:/var/lib/mysql volumes: wordpress: db: -`, ipfsImgs[0], ipfsImgs[1]), tt.composeOptions...) - }) +`, data.Get(wordpressImageCIDKey), safePort, data.Get(mariaImageCIDKey)), data.Get(composeExtraKey)) + // FIXME: need to break down composeUP into testable commands instead + // Right now, this is just a dummy placeholder + return helpers.Command("info") } + + testCase.Expected = test.Expects(0, nil, nil) + + return testCase } -func TestIPFSComposeUpBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.NginxAlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") +func TestIPFSCompBuild(t *testing.T) { + testCase := nerdtest.Setup() + + var ipfsServer test.TestableCommand + var comp *testutil.ComposeDir + + const mainImageCIDKey = "mainImageCIDKey" + safePort, err := portlock.Acquire(0) + assert.NilError(t, err) + var listenAddr = "localhost:" + strconv.Itoa(safePort) + + testCase.Require = test.Require( + // Linux only + test.Linux, + // Obviously not docker supported + test.Not(nerdtest.Docker), + nerdtest.Build, + nerdtest.IPFS, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Get alpine + helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) + // Start a local ipfs backed registry + // FIXME: this is bad and likely to collide with other tests + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) - const dockerComposeYAML = ` + // Save nginx to ipfs + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) + + const dockerComposeYAML = ` services: web: build: . ports: - - 8080:80 + - 8081:80 ` - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s + dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s COPY index.html /usr/share/nginx/html/index.html -`, ipfsCIDBase) - indexHTML := t.Name() +`, listenAddr, data.Get(mainImageCIDKey)) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + comp = testutil.NewComposeDir(t, dockerComposeYAML) + comp.WriteFile("Dockerfile", dockerfile) + comp.WriteFile("index.html", data.Identifier("indexhtml")) + } - comp.WriteFile("Dockerfile", dockerfile) - comp.WriteFile("index.html", indexHTML) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + ipfsServer.Run(nil) + } + if comp != nil { + helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + comp.CleanUp() + } + } - done := ipfsRegistryUp(t, base) - defer done() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", comp.YAMLFullPath(), "up", "-d", "--build") + } - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - t.Logf("respBody=%q", respBody) - assert.Assert(t, strings.Contains(string(respBody), indexHTML)) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + t.Logf("respBody=%q", respBody) + assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml"))) + }, + } + } + + testCase.Run(t) +} + +func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, opts string) { + comp := testutil.NewComposeDir(helpers.T(), dockerComposeYAML) + // defer comp.CleanUp() + + // Because it might or might not happen, and + helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + defer helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + + projectName := comp.ProjectName() + + args := []string{"compose", "-f", comp.YAMLFullPath()} + if opts != "" { + args = append(args, opts) + } + + helpers.Ensure(append(args, "up", "--quiet-pull", "-d")...) + + helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName)) + + checkWordpress := func() error { + // FIXME: see other notes on using the same port repeatedly + resp, err := nettestutil.HTTPGet("http://127.0.0.1:"+data.Get("wordpressPort"), 5, false) + if err != nil { + return err + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { + return fmt.Errorf("respBody does not contain %q (%s)", testutil.WordpressIndexHTMLSnippet, string(respBody)) + } + return nil + } + + var wordpressWorking bool + var err error + // 15 seconds is long enough + for i := 0; i < 5; i++ { + err = checkWordpress() + if err == nil { + wordpressWorking = true + break + } + time.Sleep(3 * time.Second) + } + + if !wordpressWorking { + ccc := helpers.Capture("ps", "-a") + helpers.T().Log(ccc) + helpers.T().Error(helpers.Err("logs", projectName+"-wordpress-1")) + helpers.T().Fatalf("wordpress is not working %v", err) + } + + helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v") + helpers.Fail("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Fail("network", "inspect", fmt.Sprintf("%s_default", projectName)) } diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go new file mode 100644 index 00000000000..aa7754c7a2e --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -0,0 +1,104 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSAddrWithKubo(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImagemainImageCIDKey" + const ipfsAddrKey = "ipfsAddrKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Registry, + nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + ipfsAddr := fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port) + data.Set(ipfsAddrKey, ipfsAddr) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey))) + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.Private, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey)), "--estargz") + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/ipfs/ipfs_linux_test.go b/cmd/nerdctl/ipfs/ipfs_linux_test.go deleted file mode 100644 index a50e426ae0f..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_linux_test.go +++ /dev/null @@ -1,148 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package ipfs - -import ( - "fmt" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" -) - -func TestIPFS(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - - // encryption - keyPair := helpers.NewJWEKeyPair(t) - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - encryptImageRef := tID + ":enc" - layersNum := 1 - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, ipfsCID, encryptImageRef).AssertOK() - base.Cmd("image", "inspect", "--mode=native", "--format={{len .Manifest.Layers}}", encryptImageRef).AssertOutExactly(fmt.Sprintf("%d\n", layersNum)) - for i := 0; i < layersNum; i++ { - base.Cmd("image", "inspect", "--mode=native", fmt.Sprintf("--format={{json (index .Manifest.Layers %d) }}", i), encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") - } - ipfsCIDEnc := cidOf(t, base.Cmd("push", "ipfs://"+encryptImageRef).OutLines()) - helpers.RmiAll(base) - - decryptImageRef := tID + ":dec" - base.Cmd("pull", "--unpack=false", ipfsCIDEnc).AssertOK() - base.Cmd("image", "decrypt", "--key="+keyPair.Pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.Prv, ipfsCIDEnc, decryptImageRef).AssertOK() - base.Cmd("run", "--rm", decryptImageRef, "/bin/sh", "-c", "echo hello").AssertOK() -} - -func TestIPFSAddress(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", ipfsaddr)) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", "--ipfs-address", ipfsaddr, ipfsCID).AssertOK() - base.Cmd("run", "--ipfs-address", ipfsaddr, "--rm", ipfsCID, "echo", "hello").AssertOK() -} - -func TestIPFSCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK() -} - -func TestIPFSWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() -} - -func TestIPFSWithLazyPullingCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "ls /.stargz-snapshotter && cat /hello").AssertOK() - base.Cmd("image", "rm", ipfsCID2).AssertOK() -} - -func pushImageToIPFS(t *testing.T, base *testutil.Base, name string, opts ...string) string { - base.Cmd("pull", name).AssertOK() - ipfsCID := cidOf(t, base.Cmd(append([]string{"push"}, append(opts, "ipfs://"+name)...)...).OutLines()) - base.Cmd("rmi", name).Run() - return ipfsCID -} - -func cidOf(t *testing.T, lines []string) string { - assert.Equal(t, len(lines) >= 2, true) - return "ipfs://" + lines[len(lines)-2] -} diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index d367d0d9f86..781a2a5d7d6 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -17,44 +17,130 @@ package ipfs import ( + "fmt" "strings" "testing" + "time" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSRegistry(t *testing.T) { - testutil.DockerIncompatible(t) +func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { + var ipfsCID string + cmd := helpers.Command("push", "ipfs://"+name) + cmd.WithArgs(opts...) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(stdout, "\n") + assert.Equal(t, len(lines) >= 2, true) + ipfsCID = lines[len(lines)-2] + }, + }) + return ipfsCID +} - base := testutil.NewBase(t) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) +func TestIPFSNerdctlRegistry(t *testing.T) { + testCase := nerdtest.Setup() - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "echo", "hello").AssertOK() -} + // FIXME: this is bad and likely to collide with other tests + const listenAddr = "localhost:5555" -func TestIPFSRegistryWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) + const ipfsImageURLKey = "ipfsImageURLKey" - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) + var ipfsServer test.TestableCommand - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "ls", "/.stargz-snapshotter").AssertOK() -} + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.IPFS, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + // Start a local ipfs backed registry + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + ipfsServer.Run(nil) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotterr", + NoParallel: true, + Require: nerdtest.Stargz, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with build", + NoParallel: true, + Require: nerdtest.Build, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("built-image")) + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, data.Get(ipfsImageURLKey)) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + + helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Identifier("built-image")) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + } -func ipfsRegistryReference(addr string, c string) string { - return addr + "/ipfs/" + strings.TrimPrefix(c, "ipfs://") + testCase.Run(t) } diff --git a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go new file mode 100644 index 00000000000..0d3ac228757 --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go @@ -0,0 +1,226 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "testing" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSSimple(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImageCIDKey" + const transformedImageCIDKey = "transformedImageCIDKey" + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.IPFS, + // We constantly rmi the image by its CID which is shared across tests, so, we make this group private + // and every subtest NoParallel + nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Equals("sha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with commit and push", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the committed image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "cat", "/hello") + }, + + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with commit and push, stargz lazy pulling", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter") + }, + + Expected: test.Expects(0, nil, test.Equals("hello\nsha256:1a490fdbdb8603c0acc0ae04d8cdc78fea40bbd26acc33bdb06a854531a04c81.json\n")), + }, + { + Description: "with encryption", + NoParallel: true, + Require: test.Binary("openssl"), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Prep a key pair + keyPair := testhelpers.NewJWEKeyPair(t) + // FIXME: this will only cleanup when the group is done, not right, but it works + t.Cleanup(keyPair.Cleanup) + data.Set("pub", keyPair.Pub) + data.Set("prv", keyPair.Prv) + + // Encrypt the image, and verify it is encrypted + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, data.Get(mainImageCIDKey), data.Identifier("encrypted")) + cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Equals("1\n"), + }) + cmd = helpers.Command("image", "inspect", "--mode=native", "--format={{json (index .Manifest.Layers 0) }}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Contains("org.opencontainers.image.enc.keys.jwe"), + }) + + // Push the encrypted image and save the CID + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted"))) + + // Remove both images locally + helpers.Ensure("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Ensure("rmi", "-f", data.Get(transformedImageCIDKey)) + + // Pull back without unpacking + helpers.Ensure("pull", "--unpack=false", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey)) + } + }, + SubTests: []*test.Case{ + { + Description: "decrypt with pub key does not work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "decrypt", "--key="+data.Get("pub"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "decrypt with priv key does work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "decrypt", "--key="+data.Get("prv"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/issues/issues_linux_test.go b/cmd/nerdctl/issues/issues_linux_test.go index 6e8939f8467..0a59126fc13 100644 --- a/cmd/nerdctl/issues/issues_linux_test.go +++ b/cmd/nerdctl/issues/issues_linux_test.go @@ -20,40 +20,20 @@ package issues import ( "fmt" - "os" - "path/filepath" "testing" - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func TestMain(m *testing.M) { - testutil.M(m) -} - func TestIssue3425(t *testing.T) { nerdtest.Setup() var registry *testregistry.RegistryServer - var ipfsPath string - if rootlessutil.IsRootless() { - var err error - ipfsPath, err = rootlessutil.XDGDataHome() - ipfsPath = filepath.Join(ipfsPath, "ipfs") - assert.NilError(t, err) - } else { - ipfsPath = filepath.Join(os.Getenv("HOME"), ".ipfs") - } - testCase := &test.Case{ - Description: "TestIssue3425", Setup: func(data test.Data, helpers test.Helpers) { base := testutil.NewBase(t) registry = testregistry.NewWithNoAuth(base, 0, false) @@ -78,7 +58,7 @@ func TestIssue3425(t *testing.T) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) }, Expected: test.Expects(0, nil, nil), @@ -97,7 +77,7 @@ func TestIssue3425(t *testing.T) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) }, Expected: test.Expects(0, nil, nil), @@ -114,7 +94,7 @@ func TestIssue3425(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("save", testutil.CommonImage) }, Expected: test.Expects(0, nil, nil), @@ -136,7 +116,7 @@ func TestIssue3425(t *testing.T) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier()) }, Expected: test.Expects(0, nil, nil), @@ -145,13 +125,10 @@ func TestIssue3425(t *testing.T) { Description: "with ipfs", Require: test.Require( nerdtest.Private, + nerdtest.IPFS, test.Not(test.Windows), test.Not(nerdtest.Docker), - test.Binary("ipfs"), ), - Env: map[string]string{ - "IPFS_PATH": ipfsPath, - }, Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("image", "pull", testutil.CommonImage) helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) @@ -162,7 +139,7 @@ func TestIssue3425(t *testing.T) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("image", "push", "ipfs://"+testutil.CommonImage) }, Expected: test.Expects(0, nil, nil), diff --git a/cmd/nerdctl/main_linux_test.go b/cmd/nerdctl/issues/main_linux_test.go similarity index 75% rename from cmd/nerdctl/main_linux_test.go rename to cmd/nerdctl/issues/main_linux_test.go index 274604e27b9..64ca72e2da4 100644 --- a/cmd/nerdctl/main_linux_test.go +++ b/cmd/nerdctl/issues/main_linux_test.go @@ -14,7 +14,7 @@ limitations under the License. */ -package main +package issues import ( "testing" @@ -24,19 +24,24 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) +func TestMain(m *testing.M) { + testutil.M(m) +} + // TestIssue108 tests https://github.com/containerd/nerdctl/issues/108 // ("`nerdctl run --net=host -it` fails while `nerdctl run -it --net=host` works") func TestIssue108(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "-it --net=host", Require: test.Binary("unbuffer"), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers. - Command("run", "-it", "--rm", "--net=host", testutil.AlpineImage, "echo", "this was always working"). - WithWrapper("unbuffer") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers. + Command("run", "-it", "--rm", "--net=host", testutil.AlpineImage, "echo", "this was always working") + cmd.WithWrapper("unbuffer") + return cmd }, // Note: unbuffer will merge stdout and stderr, preventing exact match here Expected: test.Expects(0, nil, test.Contains("this was always working")), @@ -44,15 +49,16 @@ func TestIssue108(t *testing.T) { { Description: "--net=host -it", Require: test.Binary("unbuffer"), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers. - Command("run", "--rm", "--net=host", "-it", testutil.AlpineImage, "echo", "this was not working due to issue #108"). - WithWrapper("unbuffer") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers. + Command("run", "--rm", "--net=host", "-it", testutil.AlpineImage, "echo", "this was not working due to issue #108") + cmd.WithWrapper("unbuffer") + return cmd }, // Note: unbuffer will merge stdout and stderr, preventing exact match here Expected: test.Expects(0, nil, test.Contains("this was not working due to issue #108")), }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 13a68c4900a..1fa32b7d744 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -21,17 +21,14 @@ package login import ( + "errors" "fmt" - "net" - "os" "strconv" "testing" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) @@ -65,6 +62,12 @@ func (ag *Client) WithConfigPath(value string) *Client { return ag } +func (ag *Client) RunIt(host string) []string { + args := append([]string{"login"}, ag.args...) + return append(args, host) +} + +/* func (ag *Client) GetConfigPath() string { return ag.configPath } @@ -87,84 +90,378 @@ func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { } } -func TestLoginPersistence(t *testing.T) { - base := testutil.NewBase(t) - t.Parallel() +*/ - // Retrieve from the store - testCases := []struct { - auth string - }{ - { - "basic", - }, - { - "token", - }, +func TestFoo(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Registry + + var reg *registry.Server + var token *registry.TokenAuthServer + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var username, password string + username = test.RandomStringBase64(30) + "∞" + password = test.RandomStringBase64(30) + ":∞" + reg, token = nerdtest.RegistryWithTokenAuth(data, helpers, username, password, 0, true) + // reg = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, 0, false) + + reg.Setup(data, helpers) + token.Setup(data, helpers) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + data.Set("registryHost", reg.IP.String()) + data.Set("registryPort", strconv.Itoa(reg.Port)) + helpers.Write(nerdtest.HostsDir, test.ConfigValue(reg.HostsDir)) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if reg != nil { + reg.Cleanup(data, helpers) + token.Cleanup(data, helpers) + } + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cl := &Client{} + cl.WithCredentials(data.Get("registryUsername"), data.Get("registryPassword")) + // cl.WithInsecure(true) + ex := cl.RunIt(fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + return helpers.Command(ex...) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + // reg.Logs(data, helpers) + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + //token.Logs(data, helpers) + //reg.Logs(data, helpers) + }, + } } + testCase.Run(t) +} - for _, tc := range testCases { - tc := tc - t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) { - t.Parallel() +/* - username := testregistry.SafeRandomString(30) + "∞" - password := testregistry.SafeRandomString(30) + ":∞" +func WithRegistry(data test.Data, helpers test.Helpers, t *testing.T, auth string, port int, tls bool) (string, string, *registry.Server) { + username := test.RandomStringBase64(30) + "∞" + password := test.RandomStringBase64(30) + ":∞" + switch auth { + case "basic": + return username, password, nerdtest.RegistryWithBasicAuth(data, helpers, username, password, port, tls) + case "token": + return username, password, nerdtest.RegistryWithTokenAuth(data, helpers, username, password, port, tls) + default: + return "", "", nerdtest.RegistryWithNoAuth(data, helpers, t, port, tls) + } +} - // Add the requested authentication - var auth testregistry.Auth - var dependentCleanup func(error) +*/ - auth = &testregistry.NoAuth{} - if tc.auth == "basic" { - auth = &testregistry.BasicAuth{ - Username: username, - Password: password, - } - } else if tc.auth == "token" { - authCa := testca.New(base.T) - as := testregistry.NewAuthServer(base, authCa, 0, username, password, false) - auth = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, - } - dependentCleanup = as.Cleanup - } +type RegistryTestDescriptor struct { + Port int + TLS bool + AuthType string - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) + registry *registry.Server + token *registry.TokenAuthServer + //username string + //password string + t *testing.T +} - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) +func (rtd *RegistryTestDescriptor) Description() string { + desc := "registry port: " + if rtd.Port == 0 { + desc += "random" + } else { + desc += strconv.Itoa(rtd.Port) + } + desc += " auth: " + rtd.AuthType + desc += " tls: " + strconv.FormatBool(rtd.TLS) + return desc +} - // First, login successfully - c := (&Client{}). - WithCredentials(username, password) +func (rtd *RegistryTestDescriptor) Setup(data test.Data, helpers test.Helpers) { + var username, password string + username = test.RandomStringBase64(30) + "∞" + password = test.RandomStringBase64(30) + ":∞" + switch rtd.AuthType { + case "basic": + rtd.registry = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, rtd.Port, rtd.TLS) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + case "token": + rtd.registry, rtd.token = nerdtest.RegistryWithTokenAuth(data, helpers, username, password, rtd.Port, rtd.TLS) + rtd.token.Setup(data, helpers) + data.Set("registryUsername", username) + data.Set("registryPassword", password) + default: + rtd.registry = nerdtest.RegistryWithNoAuth(data, helpers, rtd.Port, rtd.TLS) + } + rtd.registry.Setup(data, helpers) + data.Set("registryHostsDir", rtd.registry.HostsDir) + data.Set("registryHost", rtd.registry.IP.String()) + data.Set("registryPort", strconv.Itoa(rtd.registry.Port)) +} + +func (rtd *RegistryTestDescriptor) Cleanup(data test.Data, helpers test.Helpers) { + if rtd.registry != nil { + //rtd.registry.Logs(data, helpers) + rtd.registry.Cleanup(data, helpers) + } + if rtd.token != nil { + //rtd.token.Logs(data, helpers) + rtd.token.Cleanup(data, helpers) + } +} - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() +func WithNothing(username string, password string, host string, port string) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // Now, log in successfully without passing any explicit credentials - nc := (&Client{}). - WithConfigPath(c.GetConfigPath()) - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() +func WithHosts(username string, password string, host string, port string, hostsDir string) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--hosts-dir", hostsDir, + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // Now fail while using invalid credentials - nc.WithCredentials("invalid", "invalid"). - Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertFail() +func WithInsecure(username string, password string, host string, port string, insecure bool) []string { + if port != "" { + port = ":" + port + } + return []string{"login", + "--insecure-registry=" + strconv.FormatBool(insecure), + "--username", username, + "--password", password, + fmt.Sprintf("%s%s", host, port)} +} - // And login again without, reverting to the last saved good state - nc = (&Client{}). - WithConfigPath(c.GetConfigPath()) +func TestLoginPersistence(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Registry + // Use a custom docker config to avoid cross test pollution + testCase.Config = test.WithConfig(nerdtest.DockerConfig, "{}") + testCase.SubTests = []*test.Case{} - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() + testDescriptors := []*RegistryTestDescriptor{ + { + Port: 0, + TLS: true, + AuthType: "token", + t: t, + }, + { + Port: 0, + TLS: true, + AuthType: "basic", + t: t, + }, + } + + for _, testDesc := range testDescriptors { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: testDesc.Description() + "-nothing", + Setup: testDesc.Setup, + Cleanup: testDesc.Cleanup, + SubTests: []*test.Case{ + { + Description: "with hostsdir, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithHosts( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + data.Get("registryHostsDir"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with hostsdir, invalid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithHosts( + "bogus", + "bogus", + data.Get("registryHost"), + data.Get("registryPort"), + data.Get("registryHostsDir"), + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "with insecure, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithInsecure( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + true, + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with insecure, invalid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithInsecure( + "bogus", + "bogus", + data.Get("registryHost"), + data.Get("registryPort"), + true, + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "with nothing, valid credentials, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, []error{errors.New("failed to verify certificate")}, nil), + }, + { + Description: "with nothing, valid credentials, localhost", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, []error{errors.New("failed to verify certificate")}, nil), + }, + /* + { + Description: "no options, ip", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + data.Get("registryHost"), + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "no options, localhost", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "no options, 127.0.0.1", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command(WithNothing( + data.Get("registryUsername"), + data.Get("registryPassword"), + "localhost", + data.Get("registryPort"), + )...) + }, + Expected: test.Expects(0, nil, nil), + }, + + */ + }, }) } + + testCase.Run(t) +} + +func TestLoginVariants(t *testing.T) { + nerdtest.Setup() + + _ = func(description string, registrySetup func(data test.Data, helpers test.Helpers)) *test.Case { + var registry *testregistry.RegistryServer + + return &test.Case{ + Description: description, + + Setup: registrySetup, + + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + } + }, + + SubTests: []*test.Case{ + { + Description: "", + // Use a custom docker config to avoid cross test pollution + // FIXME + Config: test.WithConfig(nerdtest.DockerConfig, "{}"), + Setup: func(data test.Data, helpers test.Helpers) { + // First, login successfully + helpers.Ensure("login", + "--username", data.Get("registryUsername"), + "--password", data.Get("registryPassword"), + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("login", fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + }, + Expected: test.Expects(0, nil, test.Contains("Login Succeeded")), + }, + { + Description: "", + // Use a custom docker config to avoid cross test pollution + // FIXME: is this working? inherited from parent? + Config: test.WithConfig(nerdtest.DockerConfig, "{}"), + Setup: func(data test.Data, helpers test.Helpers) { + // First, login successfully + helpers.Ensure("login", + "--username", data.Get("registryUsername"), + "--password", data.Get("registryPassword"), + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + + // Fail to login with invalid credentials + helpers.Fail("login", + "--username", "bogus", + "--password", "bogus", + fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort")), + ) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("login", fmt.Sprintf("%s:%s", data.Get("registryHost"), data.Get("registryPort"))) + }, + Expected: test.Expects(0, nil, test.Contains("Login Succeeded")), + }, + }, + } + } + } /* @@ -196,6 +493,7 @@ func TestAgainstNoAuth(t *testing.T) { */ +/* func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication @@ -537,3 +835,6 @@ func TestLoginAgainstVariants(t *testing.T) { }) } } + + +*/ diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go index c1e3caf94fd..e2ca845e490 100644 --- a/cmd/nerdctl/main_test.go +++ b/cmd/nerdctl/main_test.go @@ -33,100 +33,98 @@ func TestMain(m *testing.M) { // TestUnknownCommand tests https://github.com/containerd/nerdctl/issues/487 func TestUnknownCommand(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() var unknownSubCommand = errors.New("unknown subcommand") - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "non-existent-command", - Command: test.RunCommand("non-existent-command"), + Command: test.Command("non-existent-command"), Expected: test.Expects(1, []error{unknownSubCommand}, nil), }, { Description: "non-existent-command info", - Command: test.RunCommand("non-existent-command", "info"), + Command: test.Command("non-existent-command", "info"), Expected: test.Expects(1, []error{unknownSubCommand}, nil), }, { Description: "system non-existent-command", - Command: test.RunCommand("system", "non-existent-command"), + Command: test.Command("system", "non-existent-command"), Expected: test.Expects(1, []error{unknownSubCommand}, nil), }, { Description: "system non-existent-command info", - Command: test.RunCommand("system", "non-existent-command", "info"), + Command: test.Command("system", "non-existent-command", "info"), Expected: test.Expects(1, []error{unknownSubCommand}, nil), }, { Description: "system", - Command: test.RunCommand("system"), + Command: test.Command("system"), Expected: test.Expects(0, nil, nil), }, { Description: "system info", - Command: test.RunCommand("system", "info"), + Command: test.Command("system", "info"), Expected: test.Expects(0, nil, test.Contains("Kernel Version:")), }, { Description: "info", - Command: test.RunCommand("info"), + Command: test.Command("info"), Expected: test.Expects(0, nil, test.Contains("Kernel Version:")), }, } - testGroup.Run(t) + testCase.Run(t) } // TestNerdctlConfig validates the configuration precedence [CLI, Env, TOML, Default] and broken config rejection func TestNerdctlConfig(t *testing.T) { - nerdtest.Setup() - - tc := &test.Case{ - Description: "Nerdctl configuration", - // Docker does not support nerdctl.toml obviously - Require: test.Not(nerdtest.Docker), - SubTests: []*test.Case{ - { - Description: "Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Expected: test.Expects(0, nil, test.Equals(defaults.DefaultSnapshotter+"\n")), - }, - { - Description: "TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-toml\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Cli > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Env > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}"), - Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-env\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Cli > Env > TOML > Default", - Command: test.RunCommand("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), - Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, - Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), - Data: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), - }, - { - Description: "Broken config", - Command: test.RunCommand("info"), - Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil), - Data: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config + testCase := nerdtest.Setup() + + // Docker does not support nerdctl.toml obviously + testCase.Require = test.Not(nerdtest.Docker) + + testCase.SubTests = []*test.Case{ + { + Description: "Default", + Command: test.Command("info", "-f", "{{.Driver}}"), + Expected: test.Expects(0, nil, test.Equals(defaults.DefaultSnapshotter+"\n")), + }, + { + Description: "TOML > Default", + Command: test.Command("info", "-f", "{{.Driver}}"), + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-toml\n")), + Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Cli > TOML > Default", + Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), + Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Env > TOML > Default", + Command: test.Command("info", "-f", "{{.Driver}}"), + Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-env\n")), + Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Cli > Env > TOML > Default", + Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"), + Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"}, + Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")), + Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`), + }, + { + Description: "Broken config", + Command: test.Command("info"), + Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil), + Config: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config version = 2`), - }, }, } - tc.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/main_test_test.go b/cmd/nerdctl/main_test_test.go index a515df48536..695c52bac07 100644 --- a/cmd/nerdctl/main_test_test.go +++ b/cmd/nerdctl/main_test_test.go @@ -27,44 +27,44 @@ import ( // TestTest is testing the test tooling itself func TestTest(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tg := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "failure", - Command: test.RunCommand("undefinedcommand"), + Command: test.Command("undefinedcommand"), Expected: test.Expects(1, nil, nil), }, { Description: "success", - Command: test.RunCommand("info"), + Command: test.Command("info"), Expected: test.Expects(0, nil, nil), }, { Description: "failure with single error testing", - Command: test.RunCommand("undefinedcommand"), + Command: test.Command("undefinedcommand"), Expected: test.Expects(1, []error{errors.New("unknown subcommand")}, nil), }, { Description: "success with contains output testing", - Command: test.RunCommand("info"), + Command: test.Command("info"), Expected: test.Expects(0, nil, test.Contains("Kernel")), }, { Description: "success with negative output testing", - Command: test.RunCommand("info"), + Command: test.Command("info"), Expected: test.Expects(0, nil, test.DoesNotContain("foobar")), }, // Note that docker annoyingly returns 125 in a few conditions like this { Description: "failure with multiple error testing", - Command: test.RunCommand("-fail"), + Command: test.Command("-fail"), Expected: test.Expects(-1, []error{errors.New("unknown"), errors.New("shorthand")}, nil), }, { Description: "success with exact output testing", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.CustomCommand("echo", "foobar") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("echo", "foobar") }, Expected: test.Expects(0, nil, test.Equals("foobar\n")), }, @@ -74,14 +74,13 @@ func TestTest(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { data.Set("status", data.Get("status")+"-setup") }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - cmd := helpers.CustomCommand("printf", data.Get("status")) + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Custom("printf", data.Get("status")) data.Set("status", data.Get("status")+"-command") return cmd }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get("first-run") == "" { - data.Set("first-run", "first cleanup") + if data.Get("status") == "uninitialized" { return } if data.Get("status") != "uninitialized-setup-command" { @@ -92,56 +91,15 @@ func TestTest(t *testing.T) { SubTests: []*test.Case{ { Description: "Subtest data propagation", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.CustomCommand("printf", data.Get("status")) + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("printf", data.Get("status")) }, Expected: test.Expects(0, nil, test.Equals("uninitialized-setup-command")), }, }, Expected: test.Expects(0, nil, test.Equals("uninitialized-setup")), }, - { - Description: "env propagation and isolation", - Env: map[string]string{ - "GLOBAL_ENV": "in this test", - }, - Setup: func(data test.Data, helpers test.Helpers) { - cmd := helpers.CustomCommand("sh", "-c", "--", "printf \"$GLOBAL_ENV\"") - cmd.Run(&test.Expected{ - Output: test.Equals("in this test"), - }) - cmd.WithEnv(map[string]string{ - "GLOBAL_ENV": "overridden in setup", - }) - cmd.Run(&test.Expected{ - Output: test.Equals("overridden in setup"), - }) - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - cmd := helpers.CustomCommand("sh", "-c", "--", "printf \"$GLOBAL_ENV\"") - cmd.Run(&test.Expected{ - Output: test.Equals("in this test"), - }) - cmd.WithEnv(map[string]string{ - "GLOBAL_ENV": "overridden in command", - }) - return cmd - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - cmd := helpers.CustomCommand("sh", "-c", "--", "printf \"$GLOBAL_ENV\"") - cmd.Run(&test.Expected{ - Output: test.Equals("in this test"), - }) - cmd.WithEnv(map[string]string{ - "GLOBAL_ENV": "overridden in cleanup", - }) - cmd.Run(&test.Expected{ - Output: test.Equals("overridden in cleanup"), - }) - }, - Expected: test.Expects(0, nil, test.Equals("overridden in command")), - }, } - tg.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index ae590d94836..2d1274f19ea 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -30,25 +30,27 @@ import ( ) func TestNetworkCreate(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { - Description: "Network create", + Description: "vanilla", + // #3491 and #3508 may have helped - commenting this out for now + // Require: nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3086"), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier()) netw := nerdtest.InspectNetwork(helpers, data.Identifier()) assert.Equal(t, len(netw.IPAM.Config), 1) data.Set("subnet", netw.IPAM.Config[0].Subnet) - helpers.Ensure("network", "create", data.Identifier()+"-1") + helpers.Ensure("network", "create", data.Identifier("1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()+"-1") + helpers.Anyhow("network", "rm", data.Identifier("1")) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - data.Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier()+"-1", testutil.AlpineImage, "ip", "route")) + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + data.Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier("1"), testutil.AlpineImage, "ip", "route")) return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.AlpineImage, "ip", "route") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -63,21 +65,25 @@ func TestNetworkCreate(t *testing.T) { }, }, { - Description: "Network create with MTU", + Description: "with MTU", + // #3491 and #3508 may have helped - commenting this out for now + // Require: nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3086"), Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", "--opt", "com.docker.network.driver.mtu=9216") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.AlpineImage, "ifconfig", "eth0") }, Expected: test.Expects(0, nil, test.Contains("MTU:9216")), }, { - Description: "Network create with ipv6", - Require: nerdtest.OnlyIPv6, + Description: "with ipv6", + // #3491 and #3508 may have helped - commenting this out for now + // Require: nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3086"), + Require: nerdtest.OnlyIPv6, Setup: func(data test.Data, helpers test.Helpers) { subnetStr := "2001:db8:8::/64" data.Set("subnetStr", subnetStr) @@ -89,7 +95,7 @@ func TestNetworkCreate(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "addr", "show", "dev", "eth0") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -105,5 +111,5 @@ func TestNetworkCreate(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 5dfe9b88612..64d5631e2bf 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -30,7 +30,7 @@ import ( ) func TestNetworkInspect(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() const ( testSubnet = "10.24.24.0/24" @@ -38,23 +38,23 @@ func TestNetworkInspect(t *testing.T) { testIPRange = "10.24.24.0/25" ) - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "non existent network", - Command: test.RunCommand("network", "inspect", "nonexistent"), + Command: test.Command("network", "inspect", "nonexistent"), // FIXME: where is this error even comin from? Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil), }, { Description: "invalid name network", - Command: test.RunCommand("network", "inspect", "∞"), + Command: test.Command("network", "inspect", "∞"), // FIXME: this is not even a valid identifier Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil), }, { Description: "none", - Require: nerdtest.NerdctlNeedsFixing, - Command: test.RunCommand("network", "inspect", "none"), + Require: nerdtest.NerdctlNeedsFixing("no issue opened"), + Command: test.Command("network", "inspect", "none"), Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) @@ -65,8 +65,8 @@ func TestNetworkInspect(t *testing.T) { }, { Description: "host", - Require: nerdtest.NerdctlNeedsFixing, - Command: test.RunCommand("network", "inspect", "host"), + Require: nerdtest.NerdctlNeedsFixing("no issue opened"), + Command: test.Command("network", "inspect", "host"), Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) @@ -78,7 +78,7 @@ func TestNetworkInspect(t *testing.T) { { Description: "bridge", Require: test.Not(test.Windows), - Command: test.RunCommand("network", "inspect", "bridge"), + Command: test.Command("network", "inspect", "bridge"), Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) @@ -95,7 +95,7 @@ func TestNetworkInspect(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "remove", "custom") }, - Command: test.RunCommand("network", "inspect", "custom"), + Command: test.Command("network", "inspect", "custom"), Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { var dc []dockercompat.Network err := json.Unmarshal([]byte(stdout), &dc) @@ -107,7 +107,7 @@ func TestNetworkInspect(t *testing.T) { { Description: "match exact id", Require: test.Not(test.Windows), - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { id := strings.TrimSpace(helpers.Capture("network", "inspect", "bridge", "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id) }, @@ -122,7 +122,7 @@ func TestNetworkInspect(t *testing.T) { { Description: "match part of id", Require: test.Not(test.Windows), - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { id := strings.TrimSpace(helpers.Capture("network", "inspect", "bridge", "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id[0:25]) }, @@ -146,7 +146,7 @@ func TestNetworkInspect(t *testing.T) { id := strings.TrimSpace(helpers.Capture("network", "inspect", "bridge", "--format", "{{ .Id }}")) helpers.Anyhow("network", "remove", id[0:12]) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Get("netname")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -162,7 +162,7 @@ func TestNetworkInspect(t *testing.T) { }, }, { - Description: "Test network inspect", + Description: "basic", // IPAMConfig is not implemented on Windows yet Require: test.Not(test.Windows), Setup: func(data test.Data, helpers test.Helpers) { @@ -172,7 +172,7 @@ func TestNetworkInspect(t *testing.T) { Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "inspect", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -197,13 +197,13 @@ func TestNetworkInspect(t *testing.T) { }, }, { - Description: "Test network with namespace", + Description: "with namespace", Require: test.Not(nerdtest.Docker), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) helpers.Anyhow("namespace", "remove", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "create", data.Identifier()) }, @@ -211,23 +211,31 @@ func TestNetworkInspect(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - cmd := helpers.CustomCommand("nerdctl", "--namespace", data.Identifier()) + cmd := helpers.Custom("nerdctl", "--namespace", data.Identifier()) - cmd.Clone().WithArgs("network", "inspect", data.Identifier()).Run(&test.Expected{ + com := cmd.Clone() + com.WithArgs("network", "inspect", data.Identifier()) + com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no network found")}, }) - cmd.Clone().WithArgs("network", "remove", data.Identifier()).Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "remove", data.Identifier()) + com.Run(&test.Expected{ ExitCode: 1, Errors: []error{errors.New("no network found")}, }) - cmd.Clone().WithArgs("network", "ls").Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "ls") + com.Run(&test.Expected{ Output: test.DoesNotContain(data.Identifier()), }) - cmd.Clone().WithArgs("network", "prune", "-f").Run(&test.Expected{ + com = cmd.Clone() + com.WithArgs("network", "prune", "-f") + com.Run(&test.Expected{ Output: test.DoesNotContain(data.Identifier()), }) }, @@ -236,5 +244,5 @@ func TestNetworkInspect(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index a9d2f0124eb..af8c8d1ca0b 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -27,64 +27,68 @@ import ( ) func TestNetworkLsFilter(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "Test network list", - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("identifier", data.Identifier()) - data.Set("label", data.Identifier()+"=label-1") - data.Set("netID1", helpers.Capture("network", "create", "--label="+data.Get("label"), data.Identifier()+"-1")) - data.Set("netID2", helpers.Capture("network", "create", data.Identifier()+"-2")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()+"-1") - helpers.Anyhow("network", "rm", data.Identifier()+"-2") - }, - SubTests: []*test.Case{ - { - Description: "filter label", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) - netNames := map[string]struct{}{ - data.Get("netID1")[:12]: {}, - } + // #3491 and #3508 may have helped - commenting this out for now + // testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3086"), + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Set("identifier", data.Identifier()) + data.Set("label", "mylabel=label-1") + data.Set("net1", data.Identifier("1")) + data.Set("net2", data.Identifier("2")) + data.Set("netID1", helpers.Capture("network", "create", "--label="+data.Get("label"), data.Get("net1"))) + data.Set("netID2", helpers.Capture("network", "create", data.Get("net2"))) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier("1")) + helpers.Anyhow("network", "rm", data.Identifier("2")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "filter label", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, info) + netNames := map[string]struct{}{ + data.Get("netID1")[:12]: {}, + } - for _, name := range lines { - _, ok := netNames[name] - assert.Assert(t, ok, info) - } - }, - } - }, + for _, name := range lines { + _, ok := netNames[name] + assert.Assert(t, ok, info) + } + }, + } + }, + }, + { + Description: "filter name", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("net2")) }, - { - Description: "filter name", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("identifier")+"-2") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, info) - netNames := map[string]struct{}{ - data.Get("netID2")[:12]: {}, - } + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, info) + netNames := map[string]struct{}{ + data.Get("netID2")[:12]: {}, + } - for _, name := range lines { - _, ok := netNames[name] - assert.Assert(t, ok, info) - } - }, - } - }, + for _, name := range lines { + _, ok := netNames[name] + assert.Assert(t, ok, info) + } + }, + } }, }, } diff --git a/cmd/nerdctl/network/network_prune_linux_test.go b/cmd/nerdctl/network/network_prune_linux_test.go index 333b3aefd95..62f6417c736 100644 --- a/cmd/nerdctl/network/network_prune_linux_test.go +++ b/cmd/nerdctl/network/network_prune_linux_test.go @@ -25,49 +25,46 @@ import ( ) func TestNetworkPrune(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "TestNetworkPrune", - Require: nerdtest.Private, + testCase.Require = nerdtest.Private - SubTests: []*test.Case{ - { - Description: "Prune does not collect started container network", - NoParallel: true, - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Command: test.RunCommand("network", "prune", "-f"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.DoesNotContain(data.Identifier()), - } - }, + testCase.SubTests = []*test.Case{ + { + Description: "Prune does not collect started container network", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) }, - { - Description: "Prune does collect stopped container network", - NoParallel: true, - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) - helpers.Ensure("stop", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Command: test.RunCommand("network", "prune", "-f"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.Contains(data.Identifier()), - } - }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: test.Command("network", "prune", "-f"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.DoesNotContain(data.Identifier()), + } + }, + }, + { + Description: "Prune does collect stopped container network", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.NginxAlpineImage) + helpers.Ensure("stop", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Command: test.Command("network", "prune", "-f"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.Contains(data.Identifier()), + } }, }, } diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go index 9e5bbaf200a..b8804772f4b 100644 --- a/cmd/nerdctl/network/network_remove_linux_test.go +++ b/cmd/nerdctl/network/network_remove_linux_test.go @@ -29,109 +29,102 @@ import ( ) func TestNetworkRemove(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "TestNetworkRemove", - Require: test.Not(nerdtest.Rootless), - SubTests: []*test.Case{ - { - Description: "Simple network remove", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + testCase.Require = nerdtest.RootFul + + testCase.SubTests = []*test.Case{ + { + Description: "Simple network remove", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } + }, + }, + { + Description: "Network remove when linked to container", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "rm", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: test.Expects(1, []error{errors.New("is in use")}, nil), + }, + { + Description: "Network remove by id", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "rm", data.Get("netID")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } + }, + }, + { + Description: "Network remove by short id", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) + helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + // Verity the network is here + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") }, - { - Description: "Network remove when linked to container", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - Errors: []error{errors.New("is in use")}, - } - }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "rm", data.Get("netID")[:12]) }, - { - Description: "Network remove by id", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Get("netID")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) }, - { - Description: "Network remove by short id", - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID) - helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - // Verity the network is here - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("network", "rm", data.Get("netID")[:12]) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) - assert.Error(t, err, "Link not found", info) - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + assert.Error(t, err, "Link not found", info) + }, + } }, }, } diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index c12d0161576..3dd5d30c40d 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.Command { +func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("events", "--filter", data.Get("filter"), "--format", "json") cmd.Background(1 * time.Second) helpers.Ensure("run", "--rm", testutil.CommonImage) @@ -33,9 +33,9 @@ func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.Command } func TestEventFilters(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "CapitalizedFilter", Require: test.Not(nerdtest.Docker), @@ -96,5 +96,5 @@ func TestEventFilters(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go index 3c8f5c252da..dc4af4b0381 100644 --- a/cmd/nerdctl/system/system_info_test.go +++ b/cmd/nerdctl/system/system_info_test.go @@ -38,24 +38,24 @@ func testInfoComparator(stdout string, info string, t *testing.T) { } func TestInfo(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "info", - Command: test.RunCommand("info", "--format", "{{json .}}"), + Command: test.Command("info", "--format", "{{json .}}"), Expected: test.Expects(0, nil, testInfoComparator), }, { Description: "info convenience form", - Command: test.RunCommand("info", "--format", "json"), + Command: test.Command("info", "--format", "json"), Expected: test.Expects(0, nil, testInfoComparator), }, { Description: "info with namespace", Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("info") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("nerdctl", "info") }, Expected: test.Expects(0, nil, test.Contains("Namespace: default")), }, @@ -65,12 +65,12 @@ func TestInfo(t *testing.T) { "CONTAINERD_NAMESPACE": "test", }, Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command().Clear().WithBinary("nerdctl").WithArgs("info") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Custom("nerdctl", "info") }, Expected: test.Expects(0, nil, test.Contains("Namespace: test")), }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go index dbf78726fcd..1d47b26690c 100644 --- a/cmd/nerdctl/system/system_prune_linux_test.go +++ b/cmd/nerdctl/system/system_prune_linux_test.go @@ -23,86 +23,70 @@ import ( "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) func TestSystemPrune(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testCase := &test.Case{ - Description: "TestSystemPrune", - NoParallel: true, - SubTests: []*test.Case{ - { - Description: "volume prune all success", - // Private because of prune evidently - Require: nerdtest.Private, - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("network", "create", data.Identifier()) - helpers.Ensure("volume", "create", data.Identifier()) - anonIdentifier := helpers.Capture("volume", "create") - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), - "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) + testCase.NoParallel = true - data.Set("anonIdentifier", anonIdentifier) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "rm", data.Identifier()) - helpers.Anyhow("volume", "rm", data.Identifier()) - helpers.Anyhow("volume", "rm", data.Get("anonIdentifier")) - helpers.Anyhow("rm", "-f", data.Identifier()) - }, - Command: test.RunCommand("system", "prune", "-f", "--volumes", "--all"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - volumes := helpers.Capture("volume", "ls") - networks := helpers.Capture("network", "ls") - images := helpers.Capture("images") - containers := helpers.Capture("ps", "-a") - assert.Assert(t, strings.Contains(volumes, data.Identifier()), volumes) - assert.Assert(t, !strings.Contains(volumes, data.Get("anonIdentifier")), volumes) - assert.Assert(t, !strings.Contains(containers, data.Identifier()), containers) - assert.Assert(t, !strings.Contains(networks, data.Identifier()), networks) - assert.Assert(t, !strings.Contains(images, testutil.CommonImage), images) - }, - } - }, - }, - { - Description: "buildkit", - // FIXME: using a dedicated namespace does not work with rootful (because of buildkitd) - NoParallel: true, - // buildkitd is not available with docker - Require: test.Require(nerdtest.Build, test.Not(nerdtest.Docker)), - // FIXME: this test will happily say "green" even if the command actually fails to do its duty - // if there is nothing in the build cache. - // Ensure with setup here that we DO build something first - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("system", "prune", "-f", "--volumes", "--all") - }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - buildctlBinary, err := buildkitutil.BuildctlBinary() - if err != nil { - t.Fatal(err) - } - - host, err := buildkitutil.GetBuildkitHost(testutil.Namespace) - if err != nil { - t.Fatal(err) - } + testCase.SubTests = []*test.Case{ + { + Description: "volume prune all success", + // Private because of prune evidently + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier()) + helpers.Ensure("volume", "create", data.Identifier()) + anonIdentifier := helpers.Capture("volume", "create") + helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), + "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - buildctlArgs := buildkitutil.BuildctlBaseArgs(host) - buildctlArgs = append(buildctlArgs, "du") - - return helpers.CustomCommand(buildctlBinary, buildctlArgs...) - }, - Expected: test.Expects(0, nil, test.Contains("Total:\t\t0B")), + data.Set("anonIdentifier", anonIdentifier) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Identifier()) + helpers.Anyhow("volume", "rm", data.Identifier()) + helpers.Anyhow("volume", "rm", data.Get("anonIdentifier")) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.Command("system", "prune", "-f", "--volumes", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + volumes := helpers.Capture("volume", "ls") + networks := helpers.Capture("network", "ls") + images := helpers.Capture("images") + containers := helpers.Capture("ps", "-a") + assert.Assert(t, strings.Contains(volumes, data.Identifier()), volumes) + assert.Assert(t, !strings.Contains(volumes, data.Get("anonIdentifier")), volumes) + assert.Assert(t, !strings.Contains(containers, data.Identifier()), containers) + assert.Assert(t, !strings.Contains(networks, data.Identifier()), networks) + assert.Assert(t, !strings.Contains(images, testutil.CommonImage), images) + }, + } + }, + }, + { + Description: "buildkit", + // FIXME: using a dedicated namespace does not work with rootful (because of buildkitd) + NoParallel: true, + // buildkitd is not available with docker + Require: test.Require(nerdtest.Build, test.Not(nerdtest.Docker)), + // FIXME: this test will happily say "green" even if the command actually fails to do its duty + // if there is nothing in the build cache. + // Ensure with setup here that we DO build something first + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("system", "prune", "-f", "--volumes", "--all") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return nerdtest.BuildCtlCommand(helpers, "du") }, + Expected: test.Expects(0, nil, test.Contains("Total:\t\t0B")), }, } diff --git a/cmd/nerdctl/volume/volume_create_test.go b/cmd/nerdctl/volume/volume_create_test.go index 767f7ac12be..1cde5336199 100644 --- a/cmd/nerdctl/volume/volume_create_test.go +++ b/cmd/nerdctl/volume/volume_create_test.go @@ -18,6 +18,7 @@ package volume import ( "errors" + "regexp" "testing" "github.com/containerd/errdefs" @@ -27,27 +28,27 @@ import ( ) func TestVolumeCreate(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tg := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "arg missing should create anonymous volume", - Command: test.RunCommand("volume", "create"), - Expected: test.Expects(0, nil, nil), + Command: test.Command("volume", "create"), + Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("^[a-f0-9]{64}\n$"))), }, { Description: "invalid identifier should fail", - Command: test.RunCommand("volume", "create", "∞"), + Command: test.Command("volume", "create", "∞"), Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "too many args should fail", - Command: test.RunCommand("volume", "create", "too", "many"), + Command: test.Command("volume", "create", "too", "many"), Expected: test.Expects(1, []error{errors.New("at most 1 arg")}, nil), }, { Description: "success", - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -61,7 +62,7 @@ func TestVolumeCreate(t *testing.T) { }, { Description: "success with labels", - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", "--label", "foo1=baz1", "--label", "foo2=baz2", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -74,28 +75,23 @@ func TestVolumeCreate(t *testing.T) { }, }, { - Description: "invalid labels", - Command: func(data test.Data, helpers test.Helpers) test.Command { + Description: "invalid labels should fail", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // See https://github.com/containerd/nerdctl/issues/3126 return helpers.Command("volume", "create", "--label", "a", "--label", "", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - // NOTE: docker returns 125 on this - ExitCode: -1, - Errors: []error{errdefs.ErrInvalidArgument}, - } - }, + // NOTE: docker returns 125 on this + Expected: test.Expects(-1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "creating already existing volume should succeed", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "create", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -109,5 +105,5 @@ func TestVolumeCreate(t *testing.T) { }, } - tg.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_inspect_test.go b/cmd/nerdctl/volume/volume_inspect_test.go index edee98be906..a1c729b5c52 100644 --- a/cmd/nerdctl/volume/volume_inspect_test.go +++ b/cmd/nerdctl/volume/volume_inspect_test.go @@ -42,162 +42,170 @@ func createFileWithSize(mountPoint string, size int64) error { } func TestVolumeInspect(t *testing.T) { - nerdtest.Setup() - var size int64 = 1028 - tc := &test.Case{ - Description: "Volume inspect", - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("volprefix", data.Identifier()) - helpers.Ensure("volume", "create", data.Identifier()) - helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier()+"-second") - // Obviously note here that if inspect code gets totally hosed, this entire suite will - // probably fail right here on the Setup instead of actually testing something - vol := nerdtest.InspectVolume(helpers, data.Identifier()) - err := createFileWithSize(vol.Mountpoint, size) - assert.NilError(t, err, "File creation failed") + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ + "which is not always true. To be replaced by cp into the container.", + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + isDocker, _ := nerdtest.Docker.Check(data, helpers) + return !isDocker || test.IsRoot(), "docker cli needs to be run as root" + }, + }) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("volume", "create", data.Identifier("first")) + helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier("second")) + // Obviously note here that if inspect code gets totally hosed, this entire suite will + // probably fail right here on the Setup instead of actually testing something + vol := nerdtest.InspectVolume(helpers, data.Identifier("first")) + err := createFileWithSize(vol.Mountpoint, size) + assert.NilError(t, err, "File creation failed") + data.Set("vol1", data.Identifier("first")) + data.Set("vol2", data.Identifier("second")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Identifier("first")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("second")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "arg missing should fail", + Command: test.Command("volume", "inspect"), + Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-second") + { + Description: "invalid identifier should fail", + Command: test.Command("volume", "inspect", "∞"), + Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, - - SubTests: []*test.Case{ - { - Description: "arg missing should fail", - Command: test.RunCommand("volume", "inspect"), - Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), + { + Description: "non existent volume should fail", + Command: test.Command("volume", "inspect", "doesnotexist"), + Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), + }, + { + Description: "success", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", data.Get("vol1")) }, - { - Description: "invalid identifier should fail", - Command: test.RunCommand("volume", "inspect", "∞"), - Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)+info) + assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info) + }, + ), + } + }, + }, + { + Description: "inspect labels", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", data.Get("vol2")) }, - { - Description: "non existent volume should fail", - Command: test.RunCommand("volume", "inspect", "doesnotexist"), - Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol2")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + labels := *dc[0].Labels + assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) + assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) + assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) + }, + ), + } }, - { - Description: "success", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)+info) - assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info) - }, - ), - } - }, + }, + { + Description: "inspect size", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", "--size", data.Get("vol1")) }, - { - Description: "inspect labels", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix")+"-second") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - labels := *dc[0].Labels - assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels))) - assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"])) - assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"])) - }, - ), - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) + }, + ), + } }, - { - Description: "inspect size", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", "--size", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) - }, - ), - } - }, + }, + { + Description: "multi success", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", data.Get("vol1"), data.Get("vol2")) }, - { - Description: "multi success", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("volprefix"), data.Get("volprefix")+"-second") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.Contains(data.Get("volprefix")), - test.Contains(data.Get("volprefix")+"-second"), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)) - assert.Assert(t, dc[1].Name == data.Get("volprefix")+"-second", fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix")+"-second", dc[1].Name)) - }, - ), - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(data.Get("vol1")), + test.Contains(data.Get("vol2")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc))) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)) + assert.Assert(t, dc[1].Name == data.Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol2"), dc[1].Name)) + }, + ), + } }, - { - Description: "part success multi", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("volprefix")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, - Output: test.All( - test.Contains(data.Get("volprefix")), - func(stdout string, info string, t *testing.T) { - var dc []native.Volume - if err := json.Unmarshal([]byte(stdout), &dc); err != nil { - t.Fatal(err) - } - assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) - assert.Assert(t, dc[0].Name == data.Get("volprefix"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("volprefix"), dc[0].Name)) - }, - ), - } - }, + }, + { + Description: "part success multi", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("vol1")) }, - { - Description: "multi failure", - Command: test.RunCommand("volume", "inspect", "invalid∞", "nonexistent"), - Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, + Output: test.All( + test.Contains(data.Get("vol1")), + func(stdout string, info string, t *testing.T) { + var dc []native.Volume + if err := json.Unmarshal([]byte(stdout), &dc); err != nil { + t.Fatal(err) + } + assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))) + assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)) + }, + ), + } }, }, + { + Description: "multi failure", + Command: test.Command("volume", "inspect", "invalid∞", "nonexistent"), + Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil), + }, } - tc.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index 8f5892cb6d9..77954ee5b19 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -32,30 +32,34 @@ func TestVolumeLsSize(t *testing.T) { nerdtest.Setup() tc := &test.Case{ - Description: "Volume ls --size", - Require: test.Not(nerdtest.Docker), + Require: test.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-1") - helpers.Ensure("volume", "create", data.Identifier()+"-2") - helpers.Ensure("volume", "create", data.Identifier()+"-empty") - vol1 := nerdtest.InspectVolume(helpers, data.Identifier()+"-1") - vol2 := nerdtest.InspectVolume(helpers, data.Identifier()+"-2") + helpers.Ensure("volume", "create", data.Identifier("1")) + helpers.Ensure("volume", "create", data.Identifier("2")) + helpers.Ensure("volume", "create", data.Identifier("empty")) + vol1 := nerdtest.InspectVolume(helpers, data.Identifier("1")) + vol2 := nerdtest.InspectVolume(helpers, data.Identifier("2")) err := createFileWithSize(vol1.Mountpoint, 102400) assert.NilError(t, err, "File creation failed") err = createFileWithSize(vol2.Mountpoint, 204800) assert.NilError(t, err, "File creation failed") }, - Command: test.RunCommand("volume", "ls", "--size"), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Identifier("1")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("2")) + helpers.Anyhow("volume", "rm", "-f", data.Identifier("empty")) + }, + Command: test.Command("volume", "ls", "--size"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { var lines = strings.Split(strings.TrimSpace(stdout), "\n") assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) volSizes := map[string]string{ - data.Identifier() + "-1": "100.0 KiB", - data.Identifier() + "-2": "200.0 KiB", - data.Identifier() + "-empty": "0.0 B", + data.Identifier("1"): "100.0 KiB", + data.Identifier("2"): "200.0 KiB", + data.Identifier("empty"): "0.0 B", } var numMatches = 0 @@ -77,313 +81,320 @@ func TestVolumeLsSize(t *testing.T) { }, } }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-1") - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-2") - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-empty") - }, } tc.Run(t) } func TestVolumeLsFilter(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tc := &test.Case{ - Description: "Volume ls", - Setup: func(data test.Data, helpers test.Helpers) { - var vol1, vol2, vol3, vol4 = data.Identifier() + "-1", data.Identifier() + "-2", data.Identifier() + "-3", data.Identifier() + "-4" - var label1, label2, label3, label4 = data.Identifier() + "=label-1", data.Identifier() + "=label-2", data.Identifier() + "=label-3", data.Identifier() + "-group=label-4" + testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+ + "which is not always true. To be replaced by cp into the container.", + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + isDocker, _ := nerdtest.Docker.Check(data, helpers) + return !isDocker || test.IsRoot(), "docker cli needs to be run as root" + }, + }) - helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1) - helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2) - helpers.Ensure("volume", "create", "--label="+label3, vol3) - helpers.Ensure("volume", "create", vol4) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var vol1, vol2, vol3, vol4 = data.Identifier("1"), data.Identifier("2"), data.Identifier("3"), data.Identifier("4") + var label1, label2, label3, label4 = "mylabel=label-1", "mylabel=label-2", "mylabel=label-3", "mylabel-group=label-4" - err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600) - assert.NilError(t, err, "File creation failed") - err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000) - assert.NilError(t, err, "File creation failed") + helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1) + helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2) + helpers.Ensure("volume", "create", "--label="+label3, vol3) + helpers.Ensure("volume", "create", vol4) + + // FIXME + // This will not work with Docker rootful and Docker cli run as a user + // We should replace it with cp inside the container + err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600) + assert.NilError(t, err, "File creation failed") + err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000) + assert.NilError(t, err, "File creation failed") - data.Set("vol1", vol1) - data.Set("vol2", vol2) - data.Set("vol3", vol3) - data.Set("vol4", vol4) - data.Set("mainlabel", data.Identifier()) - data.Set("label1", label1) - data.Set("label2", label2) - data.Set("label3", label3) - data.Set("label4", label4) + data.Set("vol1", vol1) + data.Set("vol2", vol2) + data.Set("vol3", vol3) + data.Set("vol4", vol4) + data.Set("mainlabel", "mylabel") + data.Set("label1", label1) + data.Set("label2", label2) + data.Set("label3", label3) + data.Set("label4", label4) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", "-f", data.Get("vol1")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol2")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol3")) + helpers.Anyhow("volume", "rm", "-f", data.Get("vol4")) + } + testCase.SubTests = []*test.Case{ + { + Description: "No filter", + Command: test.Command("volume", "ls", "--quiet"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + data.Get("vol3"): {}, + data.Get("vol4"): {}, + } + var numMatches = 0 + for _, name := range lines { + _, ok := volNames[name] + if !ok { + continue + } + numMatches++ + } + assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches)) + }, + } + }, }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Get("vol1")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol2")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol3")) - helpers.Anyhow("volume", "rm", "-f", data.Get("vol4")) + { + Description: "Retrieving label=mainlabel", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + data.Get("vol3"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, }, - SubTests: []*test.Case{ - { - Description: "No filter", - Command: test.RunCommand("volume", "ls", "--quiet"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - data.Get("vol3"): {}, - data.Get("vol4"): {}, - } - var numMatches = 0 - for _, name := range lines { - _, ok := volNames[name] - if !ok { - continue - } - numMatches++ - } - assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches)) - }, - } - }, + { + Description: "Retrieving label=mainlabel=label2", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label2")) }, - { - Description: "Retrieving label=mainlabel", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - data.Get("vol3"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving label=mainlabel=label2", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel=", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")+"=") }, - { - Description: "Retrieving label=mainlabel=", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")+"=") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + }, + } }, - { - Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label1"), "--filter", "label="+data.Get("label2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label1"), "--filter", "label="+data.Get("label2")) }, - { - Description: "Retrieving label=mainlabel and label=grouplabel=label4", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"), "--filter", "label="+data.Get("label4")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info) + }, + } }, - { - Description: "Retrieving name=volume1", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + }, + { + Description: "Retrieving label=mainlabel and label=grouplabel=label4", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"), "--filter", "label="+data.Get("label4")) }, - { - Description: "Retrieving name=volume1 and name=volume2", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"), "--filter", "name="+data.Get("vol2")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol2"): {}, - } - for _, name := range lines { - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) - } - }, - } - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size=1024000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - data.Get("vol4"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving name=volume1", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, + }, + { + Description: "Retrieving name=volume1 and name=volume2", + // Nerdctl filter behavior is broken + Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3452"), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"), "--filter", "name="+data.Get("vol2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol2"): {}, + } + for _, name := range lines { + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } + }, + }, + { + Description: "Retrieving size=1024000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + data.Get("vol4"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size>=1024000 size<=2048000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol2"): {}, - data.Get("vol4"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving size>=1024000 size<=2048000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol2"): {}, + data.Get("vol4"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, - { - Description: "Retrieving size>204800 size<1024000", - Require: test.Not(nerdtest.Docker), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000") - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - var lines = strings.Split(strings.TrimSpace(stdout), "\n") - assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) - volNames := map[string]struct{}{ - data.Get("vol1"): {}, - data.Get("vol3"): {}, - } - var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") - var err = tab.ParseHeader(lines[0]) - assert.NilError(t, err, "Tab reader failed") - for _, line := range lines { + }, + { + Description: "Retrieving size>204800 size<1024000", + Require: test.Not(nerdtest.Docker), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var lines = strings.Split(strings.TrimSpace(stdout), "\n") + assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info) + volNames := map[string]struct{}{ + data.Get("vol1"): {}, + data.Get("vol3"): {}, + } + var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") + var err = tab.ParseHeader(lines[0]) + assert.NilError(t, err, "Tab reader failed") + for _, line := range lines { - name, _ := tab.ReadRow(line, "VOLUME NAME") - if name == "VOLUME NAME" { - continue - } - _, ok := volNames[name] - assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + name, _ := tab.ReadRow(line, "VOLUME NAME") + if name == "VOLUME NAME" { + continue } - }, - } - }, + _, ok := volNames[name] + assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info) + } + }, + } }, }, } - tc.Run(t) + + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go index b20f64984dc..c2a2d6ce3db 100644 --- a/cmd/nerdctl/volume/volume_namespace_test.go +++ b/cmd/nerdctl/volume/volume_namespace_test.go @@ -26,71 +26,81 @@ import ( ) func TestVolumeNamespace(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - tg := &test.Case{ - Description: "Namespaces", - Require: test.Not(nerdtest.Docker), - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("root_namespace", data.Identifier()) - data.Set("root_volume", data.Identifier()) - helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier()) - }, - SubTests: []*test.Case{ - { - Description: "inspect another namespace volume should fail", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "inspect", data.Get("root_volume")) - }, - Expected: test.Expects(1, []error{ - errdefs.ErrNotFound, - }, nil), - }, - { - Description: "removing another namespace volume should fail", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "remove", data.Get("root_volume")) - }, - Expected: test.Expects(1, []error{ - errdefs.ErrNotFound, - }, nil), + // Docker does not support namespaces + testCase.Require = test.Not(nerdtest.Docker) + + // Create a volume in a different namespace + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Set("root_namespace", data.Identifier()) + data.Set("root_volume", data.Identifier()) + helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier()) + } + + // Cleanup once done + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Get("root_namespace") != "" { + helpers.Anyhow("--namespace", data.Identifier(), "volume", "remove", data.Identifier()) + helpers.Anyhow("namespace", "remove", data.Identifier()) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "inspect another namespace volume should fail", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "inspect", data.Get("root_volume")) }, - { - Description: "prune should leave another namespace volume untouched", - NoParallel: true, - Command: test.RunCommand("volume", "prune", "-a", "-f"), - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.DoesNotContain(data.Get("root_volume")), - func(stdout string, info string, t *testing.T) { - helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) - }, - ), - } - }, + Expected: test.Expects(1, []error{ + errdefs.ErrNotFound, + }, nil), + }, + { + Description: "removing another namespace volume should fail", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "remove", data.Get("root_volume")) }, - { - Description: "create with the same name should work, then delete it", - NoParallel: true, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "create", data.Get("root_volume")) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", data.Get("root_volume")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - helpers.Ensure("volume", "inspect", data.Get("root_volume")) - helpers.Ensure("volume", "rm", data.Get("root_volume")) + Expected: test.Expects(1, []error{ + errdefs.ErrNotFound, + }, nil), + }, + { + Description: "prune should leave another namespace volume untouched", + // Make it private so that we do not interact with other tests in the main namespace + Require: nerdtest.Private, + Command: test.Command("volume", "prune", "-a", "-f"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("root_volume")), + func(stdout string, info string, t *testing.T) { helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) }, - } - }, + ), + } + }, + }, + { + Description: "create with the same name should work, then delete it", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "create", data.Get("root_volume")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("volume", "rm", data.Get("root_volume")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Ensure("volume", "inspect", data.Get("root_volume")) + helpers.Ensure("volume", "rm", data.Get("root_volume")) + helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) + }, + } }, }, } - tg.Run(t) + testCase.Run(t) } diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go index da859b1af43..c33922c730b 100644 --- a/cmd/nerdctl/volume/volume_prune_linux_test.go +++ b/cmd/nerdctl/volume/volume_prune_linux_test.go @@ -26,20 +26,18 @@ import ( ) func TestVolumePrune(t *testing.T) { - nerdtest.Setup() - var setup = func(data test.Data, helpers test.Helpers) { anonIDBusy := strings.TrimSpace(helpers.Capture("volume", "create")) anonIDDangling := strings.TrimSpace(helpers.Capture("volume", "create")) - namedBusy := data.Identifier() + "-busy" - namedDangling := data.Identifier() + "-free" + namedBusy := data.Identifier("busy") + namedDangling := data.Identifier("free") helpers.Ensure("volume", "create", namedBusy) helpers.Ensure("volume", "create", namedDangling) helpers.Ensure("run", "--name", data.Identifier(), - "-v", namedBusy+":/whatever", - "-v", anonIDBusy+":/other", testutil.CommonImage) + "-v", namedBusy+":/namedbusyvolume", + "-v", anonIDBusy+":/anonbusyvolume", testutil.CommonImage) data.Set("anonIDBusy", anonIDBusy) data.Set("anonIDDangling", anonIDDangling) @@ -49,62 +47,61 @@ func TestVolumePrune(t *testing.T) { var cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("rm", "-f", data.Get("anonIDBusy")) - helpers.Anyhow("rm", "-f", data.Get("anonIDDangling")) - helpers.Anyhow("rm", "-f", data.Get("namedBusy")) - helpers.Anyhow("rm", "-f", data.Get("namedDangling")) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDDangling")) + helpers.Anyhow("volume", "rm", "-f", data.Get("namedBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Get("namedDangling")) } + testCase := nerdtest.Setup() // This set must be marked as private, since we cannot prune without interacting with other tests. - testCase := &test.Case{ - Description: "Prune", - Require: nerdtest.Private, - SubTests: []*test.Case{ - { - Description: "prune anonymous only", - NoParallel: true, - Command: test.RunCommand("volume", "prune", "-f"), - Setup: setup, - Cleanup: cleanup, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.DoesNotContain(data.Get("anonIDBusy")), - test.Contains(data.Get("anonIDDangling")), - test.DoesNotContain(data.Get("namedBusy")), - test.DoesNotContain(data.Get("namedDangling")), - func(stdout string, info string, t *testing.T) { - helpers.Ensure("volume", "inspect", data.Get("anonIDBusy")) - helpers.Fail("volume", "inspect", data.Get("anonIDDangling")) - helpers.Ensure("volume", "inspect", data.Get("namedBusy")) - helpers.Ensure("volume", "inspect", data.Get("namedDangling")) - }, - ), - } - }, + testCase.Require = nerdtest.Private + // Furthermore, these two subtests cannot be run in parallel + testCase.SubTests = []*test.Case{ + { + Description: "prune anonymous only", + NoParallel: true, + Setup: setup, + Cleanup: cleanup, + Command: test.Command("volume", "prune", "-f"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("anonIDBusy")), + test.Contains(data.Get("anonIDDangling")), + test.DoesNotContain(data.Get("namedBusy")), + test.DoesNotContain(data.Get("namedDangling")), + func(stdout string, info string, t *testing.T) { + helpers.Ensure("volume", "inspect", data.Get("anonIDBusy")) + helpers.Fail("volume", "inspect", data.Get("anonIDDangling")) + helpers.Ensure("volume", "inspect", data.Get("namedBusy")) + helpers.Ensure("volume", "inspect", data.Get("namedDangling")) + }, + ), + } }, - { - Description: "prune all", - NoParallel: true, - Command: test.RunCommand("volume", "prune", "-f", "--all"), - Setup: setup, - Cleanup: cleanup, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: test.All( - test.DoesNotContain(data.Get("anonIDBusy")), - test.Contains(data.Get("anonIDDangling")), - test.DoesNotContain(data.Get("namedBusy")), - test.Contains(data.Get("namedDangling")), - func(stdout string, info string, t *testing.T) { - helpers.Ensure("volume", "inspect", data.Get("anonIDBusy")) - helpers.Fail("volume", "inspect", data.Get("anonIDDangling")) - helpers.Ensure("volume", "inspect", data.Get("namedBusy")) - helpers.Fail("volume", "inspect", data.Get("namedDangling")) - }, - ), - } - }, + }, + { + Description: "prune all", + NoParallel: true, + Setup: setup, + Cleanup: cleanup, + Command: test.Command("volume", "prune", "-f", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.DoesNotContain(data.Get("anonIDBusy")), + test.Contains(data.Get("anonIDDangling")), + test.DoesNotContain(data.Get("namedBusy")), + test.Contains(data.Get("namedDangling")), + func(stdout string, info string, t *testing.T) { + helpers.Ensure("volume", "inspect", data.Get("anonIDBusy")) + helpers.Fail("volume", "inspect", data.Get("anonIDDangling")) + helpers.Ensure("volume", "inspect", data.Get("namedBusy")) + helpers.Fail("volume", "inspect", data.Get("namedDangling")) + }, + ), + } }, }, } diff --git a/cmd/nerdctl/volume/volume_remove_linux_test.go b/cmd/nerdctl/volume/volume_remove_linux_test.go index ba775614766..50bd936d6da 100644 --- a/cmd/nerdctl/volume/volume_remove_linux_test.go +++ b/cmd/nerdctl/volume/volume_remove_linux_test.go @@ -35,22 +35,22 @@ import ( // Behavior in such cases is largely unspecified, as there is no easy way to compare with Docker. // Anyhow, borked filesystem conditions is not something we should be expected to deal with in a smart way. func TestVolumeRemove(t *testing.T) { - nerdtest.Setup() + testCase := nerdtest.Setup() - testGroup := &test.Group{ + testCase.SubTests = []*test.Case{ { Description: "arg missing should fail", - Command: test.RunCommand("volume", "rm"), + Command: test.Command("volume", "rm"), Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil), }, { Description: "invalid identifier should fail", - Command: test.RunCommand("volume", "rm", "∞"), + Command: test.Command("volume", "rm", "∞"), Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "non existent volume should fail", - Command: test.RunCommand("volume", "rm", "doesnotexist"), + Command: test.Command("volume", "rm", "doesnotexist"), Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil), }, { @@ -62,22 +62,22 @@ func TestVolumeRemove(t *testing.T) { "--name", data.Identifier(), testutil.CommonImage) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", data.Identifier()) - }, - - Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), - Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "rm", data.Identifier()) + }, + + Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), }, { Description: "busy anonymous volume should fail", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("run", "-v", "/volume", "--name", data.Identifier(), testutil.CommonImage) // Inspect the container and find the anonymous volume id inspect := nerdtest.InspectContainer(helpers, data.Identifier()) var anonName string @@ -87,20 +87,21 @@ func TestVolumeRemove(t *testing.T) { break } } - assert.Assert(t, anonName != "", "Failed to find anonymous volume id") + assert.Assert(t, anonName != "", "Failed to find anonymous volume id", inspect) data.Set("anonName", anonName) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Get("anonName")) + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { // Try to remove that anon volume return helpers.Command("volume", "rm", data.Get("anonName")) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), - - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, }, { Description: "freed volume should succeed", @@ -111,7 +112,12 @@ func TestVolumeRemove(t *testing.T) { helpers.Ensure("rm", "-f", data.Identifier()) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("volume", "rm", "-f", data.Identifier()) + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("volume", "rm", data.Identifier()) }, @@ -120,19 +126,10 @@ func TestVolumeRemove(t *testing.T) { Output: test.Equals(data.Identifier() + "\n"), } }, - - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - }, }, { Description: "dangling volume should succeed", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", data.Identifier()) - }, - Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) }, @@ -141,6 +138,10 @@ func TestVolumeRemove(t *testing.T) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "rm", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: test.Equals(data.Identifier() + "\n"), @@ -150,20 +151,20 @@ func TestVolumeRemove(t *testing.T) { { Description: "part success multi-remove", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier()+"-busy", data.Identifier()) - }, - Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("volume", "create", data.Identifier()) - helpers.Ensure("volume", "create", data.Identifier()+"-busy") - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()+"-busy"), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("volume", "create", data.Identifier("busy")) + helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("volume", "rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-busy") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy"), data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -181,22 +182,22 @@ func TestVolumeRemove(t *testing.T) { { Description: "success multi-remove", - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", data.Identifier()+"-1", data.Identifier()+"-2") - }, - Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-1") - helpers.Ensure("volume", "create", data.Identifier()+"-2") + helpers.Ensure("volume", "create", data.Identifier("1")) + helpers.Ensure("volume", "create", data.Identifier("2")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-1", data.Identifier()+"-2") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("1"), data.Identifier("2")) + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "rm", data.Identifier("1"), data.Identifier("2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: test.Equals(data.Identifier() + "-1\n" + data.Identifier() + "-2" + "\n"), + Output: test.Equals(data.Identifier("1") + "\n" + data.Identifier("2") + "\n"), } }, }, @@ -204,17 +205,17 @@ func TestVolumeRemove(t *testing.T) { Description: "failing multi-remove", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("volume", "create", data.Identifier()+"-busy") - helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()+"-busy"), "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("volume", "create", data.Identifier("busy")) + helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("volume", "rm", "-f", data.Identifier()+"-busy") + helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy")) }, - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier()+"-busy") + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy")) }, Expected: test.Expects(1, []error{ @@ -225,5 +226,5 @@ func TestVolumeRemove(t *testing.T) { }, } - testGroup.Run(t) + testCase.Run(t) } diff --git a/docs/testing/tools.md b/docs/testing/tools.md index 36cee3c0510..66d416d25d6 100644 --- a/docs/testing/tools.md +++ b/docs/testing/tools.md @@ -28,17 +28,12 @@ import ( ) func TestMyThing(t *testing.T) { - nerdtest.Setup() - // Declare your test - myTest := &test.Case{ - Description: "A first test", - // This is going to run `nerdctl info` (or `docker info`) - Command: test.RunCommand("info"), - // Verify the command exits with 0, and stdout contains the word `Kernel` - Expected: test.Expects(0, nil, test.Contains("Kernel")), - } - + myTest := nerdtest.Setup() + // This is going to run `nerdctl info` (or `docker info`) + mytest.Command = test.Command("info") + // Verify the command exits with 0, and stdout contains the word `Kernel` + myTest.Expected = test.Expects(0, nil, test.Contains("Kernel")) // Run it myTest.Run(t) } @@ -53,7 +48,7 @@ You already saw two (`test.Expects` and `test.Contains`): First, `test.Expects(exitCode int, errors []error, outputCompare Comparator)`, which is convenient to quickly describe what you expect overall. -`exitCode` is obvious. +`exitCode` is obvious (note that passing -1 as an exit code will just verify the commands does fail without comparing the code). `errors` is a slice of go `error`, that allows you to compare what is seen on stderr with existing errors (for example: `errdefs.ErrNotFound`), or more generally @@ -62,13 +57,14 @@ any string you want to match. `outputCompare` can be either your own comparison function, or one of the comparison helper. -Secondly, `test.Contains`, is a `Comparator`. +Secondly, `test.Contains` - which is a `Comparator`. ### Comparators Besides `test.Contains(string)`, there are a few more: - `test.DoesNotContain(string)` - `test.Equals(string)` +- `test.Match(*regexp.Regexp)` - `test.All(comparators ...Comparator)`, which allows you to bundle together a bunch of other comparators The following example shows how to implement your own custom `Comparator` @@ -105,7 +101,7 @@ that is being used in the command or at other stages of your test (Setup). For example, creating a container with a certain name, you might want to verify that this name is then visible in the list of containers. -To achieve that, you should write your own `Expecter`, leveraging test `Data`. +To achieve that, you should write your own `Manager`, leveraging test `Data`. Here is an example, where we are using `data.Get("sometestdata")`. @@ -129,9 +125,8 @@ func TestMyThing(t *testing.T) { // Declare your test myTest := &test.Case{ - Description: "A subtest with custom data, manager, and comparator", Data: test.WithData("sometestdata", "blah"), - Command: test.RunCommand("info"), + Command: test.Command("info"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, @@ -152,28 +147,41 @@ func TestMyThing(t *testing.T) { ## On `Data` -`Data` is provided to first allow storing mutable key-value information that pertain to the test. +`Data` is provided to allow storing mutable key-value information that pertain to the test. -While it can be provided through `WithData(key string, value string)` (or `WithConfig`, see below), +While it can be provided through `test.WithData(key string, value string)`, inside the testcase definition, it can also be dynamically manipulated inside `Setup`, or `Command`. Note that `Data` additionally exposes the following functions: -- `Identifier()` which returns the unique id associated with the _current_ test (or subtest) +- `Identifier(words ...string)` which returns a unique identifier associated with the _current_ test (or subtest) - `TempDir()` which returns the private, temporary directory associated with the test ... along with the `Get(key)` and `Set(key, value)` methods. -Secondly, `Data` allows defining and manipulating "configuration" data. +Note that Data is copied down to subtests, which is convenient to pass "down" +information relevant to a bunch of subtests (eg: like a registry IP). + +## On Config + +`Config` is similar to `Data`, although it is meant specifically for predefined +keys that impact the base behavior of the binary you are testing. -In the case of nerdctl here, the following configuration options are defined: -- `WithConfig(NerdctlToml, "foo")` which allows specifying a custom config -- `WithConfig(DataRoot, "foo")` allowing to point to a custom data-root -- `WithConfig(HostsDir, "foo")` to point to a specific hosts directory -- `WithConfig(Namespace, "foo")` allows passing a specific namespace (otherwise defaults to `nerdctl-test`) +You can initiate your config using `test.WithConfig(key, value)`, and you can +manipulate it further using `helpers.Read` and`helpers.Write`. + +Currently, the following keys are defined: +- `DockerConfig` allowing to set custom content for the `$DOCKER_CONFIG/config.json` file +- `Namespace` (default to `nerdctl-test` if unspecified, but see "mode private") +- `NerdctlToml` to set custom content for the `$NERDCTL_TOML` file +- `HostsDir` to specify the value of the arg `--hosts-dir` +- `DataRoot` to specify the value of the arg `--data-root` +- `Debug` to enable debug (works for both nerdctl and docker) + +Note that config defined on the test case is copied over for subtests. ## Commands -For simple cases, `test.RunCommand(args ...string)` is the way to go. +For simple cases, `test.Command(args ...string)` is the way to go. It will execute the binary to test (nerdctl or docker), with the provided arguments, and will by default get cwd inside the temporary directory associated with the test. @@ -189,8 +197,7 @@ your setup and cleanup routines (see below). If you would like to override the environment specifically for a command, but not for others (eg: in `Setup` or `Cleanup`), you can do so with custom commands (see below). -Note that environment as defined statically in the test will be inherited by subtests, -unless explicitly overridden. +Note that environment as defined statically in the test will be copied over for subtests. ### Working directory @@ -199,9 +206,9 @@ of the test. This behavior can be overridden using custom commands. -### Custom commands +### Custom Executor -Custom commands allow you to manipulate test `Data`, override important aspects +Custom `Executor`s allow you to manipulate test `Data`, override important aspects of the command to execute (`Env`, `WorkingDir`), or otherwise give you full control on what the command does. @@ -227,10 +234,9 @@ func TestMyThing(t *testing.T) { // Declare your test myTest := &test.Case{ - Description: "A subtest with custom data, manager, and comparator", Data: test.WithData("sometestdata", "blah"), - Command: func(data test.Data, helpers test.Helpers) test.Command { - return helpers.Command("run", "--name", data.Identifier()+data.Get("sometestdata")) + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--name", data.Get("sometestdata")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -250,24 +256,35 @@ func TestMyThing(t *testing.T) { } ``` +Note that inside your `Executor` you do have access to the full palette of command options, +including: +- `Background(timeout time.Duration)` which allows you to background a command execution +- `WithWrapper(binary string, args ...string)` which allows you to "wrap" your command with another binary +- `WithStdin(io.Reader)` which allows you to pass a reader to the command stdin +- `WithCwd(string)` which allows you to specify the working directory (default to the test temp directory) +- `Clone()` which returns a copy of the command, with env, cwd, etc + +and also `WithBinary` and `WithArgs`. + ### On `helpers` Inside a custom `Executor`, `Manager`, or `Butler`, you have access to a collection of `helpers` to simplify command execution: -- `helpers.Ensure(args ...string)` will run a command and ensure it exits succesfully +- `helpers.Ensure(args ...string)` will run a command and ensure it exits successfully - `helpers.Fail(args ...string)` will run a command and ensure it fails - `helpers.Anyhow(args ...string)` will run a command but does not care if it succeeds or fails - `helpers.Capture(args ...string)` will run a command, ensure it is successful, and return the output - `helpers.Command(args ...string)` will return a command that can then be tested against expectations -- `helpers.CustomCommand(binary string, args ...string)` will do the same for any arbitrary command (not limited to nerdctl) +- `helpers.Custom(binary string, args ...string)` will do the same for any arbitrary command (not limited to nerdctl) +- `helpers.T()` which returns the appropriate `*testing.T` for your context ## Setup and Cleanup Tests routinely require a set of actions to be performed _before_ you can run the command you want to test. A setup routine will get executed before your `Command`, and have access to and can -manipulate your test `Data`. +manipulate your test `Data` and `Config`. Conversely, you very likely want to clean things up once your test is done. While temporary directories are cleaned for you with no action needed on your part, @@ -296,7 +313,6 @@ func TestMyThing(t *testing.T) { // Declare your test myTest := &test.Case{ - Description: "A subtest with custom data, manager, and comparator", Data: test.WithData("sometestdata", "blah"), Setup: func(data *test.Data, helpers test.Helpers){ helpers.Ensure("volume", "create", "foo") @@ -306,7 +322,7 @@ func TestMyThing(t *testing.T) { helpers.Anyhow("volume", "rm", "foo") helpers.Anyhow("volume", "rm", "bar") }, - Command: func(data test.Data, helpers test.Helpers) test.Command { + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--name", data.Identifier()+data.Get("sometestdata")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -331,23 +347,13 @@ func TestMyThing(t *testing.T) { Subtests are just regular tests, attached to the `SubTests` slice of a test. -Note that a subtest will inherit its parent `Data` and `Env`, in the state they are at +Note that a subtest will inherit its parent `Data`, `Config` and `Env`, in the state they are at after the parent test has run its `Setup` and `Command` routines (but before `Cleanup`). -This does _not_ apply to `Identifier()` and `TempDir()`, which are unique to the sub-test. +This does _not_ apply to `Identifier()` and `TempDir()`, which are unique to the subtest. Also note that a test does not have to have a `Command`. This is a convenient pattern if you just need a common `Setup` for a bunch of subtests. -## Groups - -A `test.Group` is just a convenient way to represent a slice of tests. - -Note that unlike a `test.Case`, a group cannot define properties inherited by -subtests, nor `Setup` or `Cleanup` routines. - -- if you just have a bunch of subtests you want to run, put them in a `test.Group` -- if you want to have a global setup, or otherwise set a common property first for your subtests, use a `test.Case` with `SubTests` - ## Parallelism All tests (and subtests) are assumed to be parallelizable. @@ -362,6 +368,9 @@ Note that if you want better isolation, it is usually better to use the requirem `test.Case` has a `Require` property that allow enforcing specific, per-test requirements. +A `Requirement` is expected to make you `Skip` tests when the environment does not match +expectations. + Here are a few: ```go test.Windows // a test runs only on Windows (or Not(Windows)) @@ -374,31 +383,53 @@ test.Require(req ...Requirement) // a test runs only if all requirements are ful nerdtest.Docker // a test only run on Docker - normally used with test.Not(nerdtest.Docker) nerdtest.Soci // a test requires the soci snapshotter -nerdtest.Rootless // a test requires Rootless (or Not(Rootless), indicating it requires Rootful) +nerdtest.Stargz // a test requires the stargz snapshotter +nerdtest.Rootless // a test requires Rootless +nerdtest.RootFul // a test requires Rootful nerdtest.Build // a test requires buildkit nerdtest.CGroup // a test requires cgroup -nerdtest.OnlyIPv6 // a test is meant to run solely in the ipv6 environment nerdtest.NerdctlNeedsFixing // indicates that a test cannot be run on nerdctl yet as a fix is required +nerdtest.BrokenTest // indicates that a test needs to be fixed and has been restricted to run only in certain cases +nerdtest.OnlyIPv6 // a test is meant to run solely in the ipv6 environment +nerdtest.OnlyKubernetes // a test is meant to run solely in the Kubernetes environment +nerdtest.IsFlaky // indicates that a test will fail in a flaky way - this may be the test fault, or more likely something racy in nerdctl nerdtest.Private // see below ``` ### About `nerdtest.Private` -While all requirements above are self-descriptive or obvious, and are going to skip -tests for environments that do not match the requirements, `nerdtest.Private` is a +While all requirements above are self-descriptive or obvious, `nerdtest.Private` is a special case. -What it does when required is: create a private namespace, data-root, hosts-dir, nerdctl.toml and -DOCKER_CONFIG that is private to the test. - -Note that subtests are going to inherit that environment as well. +If set, it will run tests inside a dedicated namespace that is private to the test. +Note that subtests by default are going to be set in that same namespace, unless they +ask for private as well, or they reset the `Namespace` config key. -If the target is Docker - which does not support namespaces for eg - asking for `private` -will merely disable parallelization. +If the target is Docker - which does not support namespaces - asking for `Private` +will disable parallelization. The purpose of private is to provide a truly clean-room environment for tests -that are guaranteed to have side effects on others, or that do require an exclusive, pristine +that are going to have side effects on others, or that do require an exclusive, pristine environment. Using private is generally preferable to disabling parallelization, as doing the latter -would slow down the run and won't have the same guarantees about the environment. +would slow down the run and won't have the same isolation guarantees about the environment. + +## Advanced command customization + +Testing any non-trivial binary likely assume a good amount of custom code +to set up the right default behavior wrt environment, flags, etc. + +To do that, you can pass a `test.Testable` implementation to the `test.Customize` method. + +It basically lets you define your own `CustomizableCommand`, along with a hook to deal with +ambient requirements that is run after `test.Require` and before `test.Setup`. + +`CustomizableCommand` are typically embedding a `test.GenericCommand` and overriding both the +`Run` and `Clone` methods. + +Check the `nerdtest` implementation for details. + +## Utilities + +TBD \ No newline at end of file diff --git a/go.mod b/go.mod index f59fac3953e..0c0e980b6f5 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/fluent/fluent-logger-golang v1.9.0 github.com/fsnotify/fsnotify v1.7.0 github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/go-yaml/yaml v2.1.0+incompatible github.com/ipfs/go-cid v0.4.1 github.com/klauspost/compress v1.17.10 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index dc101766143..e95e03908e6 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/pkg/mountutil/mountutil_test.go b/pkg/mountutil/mountutil_test.go index 619f4269c66..a70d6fb0436 100644 --- a/pkg/mountutil/mountutil_test.go +++ b/pkg/mountutil/mountutil_test.go @@ -34,4 +34,5 @@ func (mv *MockVolumeStore) CreateWithoutLock(name string, labels []string) (*nat return &native.Volume{Name: "test_volume", Mountpoint: "/test/volume"}, nil } +// nolint:unused var mockVolumeStore = &MockVolumeStore{} diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 81badc8edf1..1c71e512f6c 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -619,6 +619,7 @@ func structToMap(in interface{}) (map[string]interface{}, error) { } // ParseMTU parses the mtu option +// nolint:unused func parseMTU(mtu string) (int, error) { if mtu == "" { return 0, nil // default diff --git a/pkg/testutil/nerdtest/ambient.go b/pkg/testutil/nerdtest/ambient.go new file mode 100644 index 00000000000..d59ebc6a18e --- /dev/null +++ b/pkg/testutil/nerdtest/ambient.go @@ -0,0 +1,31 @@ +/* + 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 "github.com/containerd/nerdctl/v2/pkg/testutil" + +func environmentHasIPv6() bool { + return testutil.GetEnableIPv6() +} + +func environmentHasKubernetes() bool { + return testutil.GetEnableKubernetes() +} + +func environmentIsForFlaky() bool { + return testutil.GetFlakyEnvironment() +} diff --git a/pkg/testutil/nerdtest/ca/ca.go b/pkg/testutil/nerdtest/ca/ca.go new file mode 100644 index 00000000000..da367d464e1 --- /dev/null +++ b/pkg/testutil/nerdtest/ca/ca.go @@ -0,0 +1,162 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ca + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +type CA struct { + KeyPath string + CertPath string + + t *testing.T + key *rsa.PrivateKey + cert *x509.Certificate + closeF func() error +} + +func (ca *CA) Close() error { + return ca.closeF() +} + +const keyLength = 4096 + +func New(data test.Data, t *testing.T) *CA { + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assert.NilError(t, err) + + cert := &x509.Certificate{ + SerialNumber: serialNumber(t), + Subject: pkix.Name{ + Organization: []string{"nerdctl test organization"}, + CommonName: fmt.Sprintf("nerdctl CA (%s)", t.Name()), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + dir, err := os.MkdirTemp(data.TempDir(), "ca") + assert.NilError(t, err) + keyPath := filepath.Join(dir, "ca.key") + certPath := filepath.Join(dir, "ca.cert") + writePair(t, keyPath, certPath, cert, cert, key, key) + + return &CA{ + KeyPath: keyPath, + CertPath: certPath, + t: t, + key: key, + cert: cert, + closeF: func() error { + return os.RemoveAll(dir) + }, + } +} + +type Cert struct { + KeyPath string + CertPath string + closeF func() error +} + +func (c *Cert) Close() error { + return c.closeF() +} + +func (ca *CA) NewCert(host string, additional ...string) *Cert { + t := ca.t + + key, err := rsa.GenerateKey(rand.Reader, keyLength) + assert.NilError(t, err) + + additional = append([]string{host}, additional...) + + cert := &x509.Certificate{ + SerialNumber: serialNumber(t), + Subject: pkix.Name{ + Organization: []string{"nerdctl test organization"}, + CommonName: fmt.Sprintf("nerdctl %s (%s)", host, t.Name()), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCRLSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: additional, + } + for _, h := range additional { + if ip := net.ParseIP(h); ip != nil { + cert.IPAddresses = append(cert.IPAddresses, ip) + } + } + + dir, err := os.MkdirTemp(t.TempDir(), "cert") + assert.NilError(t, err) + certPath := filepath.Join(dir, "a.cert") + keyPath := filepath.Join(dir, "a.key") + writePair(t, keyPath, certPath, cert, ca.cert, key, ca.key) + + return &Cert{ + CertPath: certPath, + KeyPath: keyPath, + closeF: func() error { + return os.RemoveAll(dir) + }, + } +} + +func writePair(t *testing.T, keyPath, certPath string, cert, caCert *x509.Certificate, key, caKey *rsa.PrivateKey) { + keyF, err := os.Create(keyPath) + assert.NilError(t, err) + defer keyF.Close() + assert.NilError(t, pem.Encode(keyF, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) + assert.NilError(t, keyF.Close()) + + certB, err := x509.CreateCertificate(rand.Reader, cert, caCert, &key.PublicKey, caKey) + assert.NilError(t, err) + certF, err := os.Create(certPath) + assert.NilError(t, err) + defer certF.Close() + assert.NilError(t, pem.Encode(certF, &pem.Block{Type: "CERTIFICATE", Bytes: certB})) + assert.NilError(t, certF.Close()) +} + +func serialNumber(t *testing.T) *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 60) + sn, err := rand.Int(rand.Reader, serialNumberLimit) + assert.NilError(t, err) + return sn +} diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go new file mode 100644 index 00000000000..99fb0e3b0f5 --- /dev/null +++ b/pkg/testutil/nerdtest/command.go @@ -0,0 +1,200 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package nerdtest + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +const defaultNamespace = testutil.Namespace + +// IMPORTANT note on file writing here: +// Inside the context of a single test, there is no concurrency, as setup, command and cleanup operate in sequence +// Furthermore, the tempdir is private by definition. +// Writing files here in a non-safe manner is thus OK. +type target = string + +const ( + targetNerdctl = target("nerdctl") + targetDocker = target("docker") +) + +func getTarget() string { + // Indirecting to testutil for now + return testutil.GetTarget() +} + +func newNerdCommand(conf test.Config, t *testing.T) *nerdCommand { + // Decide what binary we are running + var err error + var binary string + trgt := getTarget() + switch trgt { + case targetNerdctl: + binary, err = exec.LookPath(trgt) + if err != nil { + t.Fatalf("unable to find binary %q: %v", trgt, err) + } + // Set the default namespace if we do not have something already + if conf.Read(Namespace) == "" { + conf.Write(Namespace, defaultNamespace) + } + case targetDocker: + binary, err = exec.LookPath(trgt) + if err != nil { + t.Fatalf("unable to find binary %q: %v", trgt, err) + } + if err = exec.Command("docker", "compose", "version").Run(); err != nil { + t.Fatalf("docker does not support compose: %v", err) + } + default: + t.Fatalf("unknown target %q", getTarget()) + } + + // Create the base command, with the right binary, t + ret := &nerdCommand{} + ret.WithBinary(binary) + // Not interested in these - and insulate us from parent environment side effects + ret.WithBlacklist([]string{ + "LS_COLORS", + "DOCKER_CONFIG", + "CONTAINERD_SNAPSHOTTER", + "NERDCTL_TOML", + "CONTAINERD_ADDRESS", + "CNI_PATH", + "NETCONFPATH", + "NERDCTL_EXPERIMENTAL", + "NERDCTL_HOST_GATEWAY_IP", + }) + return ret +} + +type nerdCommand struct { + test.GenericCommand + + hasWrittenToml bool + hasWrittenDockerConfig bool +} + +func (nc *nerdCommand) Run(expect *test.Expected) { + nc.prep() + if getTarget() == targetDocker { + // We are not in the business of testing docker *error* output, so, spay expectation here + if expect != nil { + expect.Errors = nil + } + } + nc.GenericCommand.Run(expect) +} + +func (nc *nerdCommand) Background(timeout time.Duration) { + nc.prep() + nc.GenericCommand.Background(timeout) +} + +// Run does override the generic command run, as we are testing both docker and nerdctl +func (nc *nerdCommand) prep() { + nc.T().Helper() + + // If no DOCKER_CONFIG was explicitly provided, set ourselves inside the current working directory + if nc.Env["DOCKER_CONFIG"] == "" { + nc.Env["DOCKER_CONFIG"] = nc.GenericCommand.TempDir + } + + if customDCConfig := nc.GenericCommand.Config.Read(DockerConfig); customDCConfig != "" { + if !nc.hasWrittenDockerConfig { + dest := filepath.Join(nc.Env["DOCKER_CONFIG"], "config.json") + err := os.WriteFile(dest, []byte(customDCConfig), 0400) + assert.NilError(nc.T(), err, "failed to write custom docker config json file for test") + nc.hasWrittenDockerConfig = true + } + } + + if getTarget() == targetDocker { + // Allow debugging with docker syntax + if nc.Config.Read(Debug) != "" { + nc.PrependArgs("--log-level=debug") + } + } else if getTarget() == targetNerdctl { + // Set the namespace + if nc.Config.Read(Namespace) != "" { + nc.PrependArgs("--namespace=" + string(nc.Config.Read(Namespace))) + } + + if nc.Config.Read(stargz) == enabled { + nc.Env["CONTAINERD_SNAPSHOTTER"] = "stargz" + } + + if nc.Config.Read(ipfs) == enabled { + var ipfsPath string + if rootlessutil.IsRootless() { + var err error + ipfsPath, err = platform.DataHome() + ipfsPath = filepath.Join(ipfsPath, "ipfs") + assert.NilError(nc.T(), err) + } else { + ipfsPath = filepath.Join(os.Getenv("HOME"), ".ipfs") + } + + nc.Env["IPFS_PATH"] = ipfsPath + } + + // If no NERDCTL_TOML was explicitly provided, set it to the private dir + if nc.Env["NERDCTL_TOML"] == "" { + nc.Env["NERDCTL_TOML"] = filepath.Join(nc.GenericCommand.TempDir, "nerdctl.toml") + } + + // If we have custom toml content, write it if it does not exist already + if nc.Config.Read(NerdctlToml) != "" { + if !nc.hasWrittenToml { + dest := nc.Env["NERDCTL_TOML"] + err := os.WriteFile(dest, []byte(nc.Config.Read(NerdctlToml)), 0400) + assert.NilError(nc.T(), err, "failed to write NerdctlToml") + nc.hasWrittenToml = true + } + } + + if nc.Config.Read(HostsDir) != "" { + nc.PrependArgs("--hosts-dir=" + string(nc.Config.Read(HostsDir))) + } + if nc.Config.Read(DataRoot) != "" { + nc.PrependArgs("--data-root=" + string(nc.Config.Read(DataRoot))) + } + if nc.Config.Read(Debug) != "" { + nc.PrependArgs("--debug-full") + } + } +} + +func (nc *nerdCommand) Clone() test.TestableCommand { + return &nerdCommand{ + GenericCommand: *(nc.GenericCommand.Clone().(*test.GenericCommand)), + hasWrittenToml: nc.hasWrittenToml, + hasWrittenDockerConfig: nc.hasWrittenDockerConfig, + } +} diff --git a/pkg/testutil/nerdtest/hoststoml/hoststoml.go b/pkg/testutil/nerdtest/hoststoml/hoststoml.go new file mode 100644 index 00000000000..f6ae48b1247 --- /dev/null +++ b/pkg/testutil/nerdtest/hoststoml/hoststoml.go @@ -0,0 +1,67 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package hoststoml + +import ( + "net" + "os" + "path/filepath" + "strconv" + + "github.com/pelletier/go-toml/v2" +) + +type hostsTomlHost struct { + CA string `toml:"ca,omitempty"` + SkipVerify bool `toml:"skip_verify,omitempty"` + Client [][]string `toml:"client,omitempty"` +} + +// See https://github.com/containerd/containerd/blob/main/docs/hosts.md +type HostsToml struct { + CA string `toml:"ca,omitempty"` + SkipVerify bool `toml:"skip_verify,omitempty"` + Client [][]string `toml:"client,omitempty"` + Headers map[string]string `toml:"header,omitempty"` + Server string `toml:"server,omitempty"` + Endpoints map[string]*hostsTomlHost `toml:"host,omitempty"` +} + +func (ht *HostsToml) Save(dir string, hostIP string, port int) error { + var err error + var r *os.File + + hostSubDir := hostIP + if port != 0 { + hostSubDir = net.JoinHostPort(hostIP, strconv.Itoa(port)) + } + + hostsSubDir := filepath.Join(dir, hostSubDir) + err = os.MkdirAll(hostsSubDir, 0700) + if err != nil { + return err + } + + if r, err = os.Create(filepath.Join(dir, hostSubDir, "hosts.toml")); err == nil { + defer r.Close() + enc := toml.NewEncoder(r) + enc.SetIndentTables(true) + err = enc.Encode(ht) + } + + return err +} diff --git a/pkg/testutil/nerdtest/platform/platform_freebsd.go b/pkg/testutil/nerdtest/platform/platform_freebsd.go new file mode 100644 index 00000000000..8128c930167 --- /dev/null +++ b/pkg/testutil/nerdtest/platform/platform_freebsd.go @@ -0,0 +1,29 @@ +/* + 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 platform + +func DataHome() (string, error) { + panic("not supported") +} + +var ( + // The following are here solely for freebsd to compile / lint. They are not used, as the corresponding tests are running only on linux. + RegistryImageStable = "registry:2" + RegistryImageNext = "ghcr.io/distribution/distribution:" + KuboImage = "ipfs/kubo:v0.16.0" + DockerAuthImage = "cesanta/docker_auth:1.7" +) diff --git a/pkg/testutil/nerdtest/platform/platform_linux.go b/pkg/testutil/nerdtest/platform/platform_linux.go new file mode 100644 index 00000000000..3aeeb0f03c8 --- /dev/null +++ b/pkg/testutil/nerdtest/platform/platform_linux.go @@ -0,0 +1,33 @@ +/* + 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 platform + +import ( + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" +) + +func DataHome() (string, error) { + return rootlessutil.XDGDataHome() +} + +var ( + RegistryImageStable = testutil.RegistryImageStable + RegistryImageNext = testutil.RegistryImageNext + KuboImage = testutil.KuboImage + DockerAuthImage = testutil.DockerAuthImage +) diff --git a/pkg/testutil/nerdtest/platform/platform_windows.go b/pkg/testutil/nerdtest/platform/platform_windows.go new file mode 100644 index 00000000000..56be8501931 --- /dev/null +++ b/pkg/testutil/nerdtest/platform/platform_windows.go @@ -0,0 +1,37 @@ +/* + 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 platform + +import ( + "fmt" +) + +func DataHome() (string, error) { + panic("not supported") +} + +// The following are here solely for windows to compile. They are not used, as the corresponding tests are running only on linux. +func mirrorOf(s string) string { + return fmt.Sprintf("ghcr.io/stargz-containers/%s-org", s) +} + +var ( + RegistryImageStable = mirrorOf("registry:2") + RegistryImageNext = "ghcr.io/distribution/distribution:" + KuboImage = mirrorOf("ipfs/kubo:v0.16.0") + DockerAuthImage = mirrorOf("cesanta/docker_auth:1.7") +) diff --git a/pkg/testutil/nerdtest/registry/cesanta.go b/pkg/testutil/nerdtest/registry/cesanta.go new file mode 100644 index 00000000000..c1efebf43ef --- /dev/null +++ b/pkg/testutil/nerdtest/registry/cesanta.go @@ -0,0 +1,233 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "encoding/json" + "fmt" + "net" + "os" + "strconv" + "testing" + "time" + + "github.com/go-yaml/yaml" + "golang.org/x/crypto/bcrypt" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +type CesantaConfigServer struct { + Addr string `yaml:"addr,omitempty"` + Certificate string + Key string +} + +type CesantaConfigToken struct { + Issuer string `yaml:"issuer,omitempty"` + Certificate string `yaml:"certificate,omitempty"` + Key string `yaml:"key,omitempty"` + Expiration int `yaml:"expiration,omitempty"` +} + +type CesantaConfigUser struct { + Password string `yaml:"password,omitempty"` +} + +type CesantaMatchConditions struct { + Account string `yaml:"account,omitempty"` +} + +type CesantaConfigACLEntry struct { + Match CesantaMatchConditions `yaml:"match"` + Actions []string `yaml:"actions,flow"` +} + +type CesantaConfigACL []CesantaConfigACLEntry + +type CesantaConfig struct { + Server CesantaConfigServer `yaml:"server"` + Token CesantaConfigToken `yaml:"token"` + Users map[string]CesantaConfigUser `yaml:"users,omitempty"` + ACL CesantaConfigACL `yaml:"acl,omitempty"` +} + +func (cc *CesantaConfig) Save(path string) error { + var err error + var r *os.File + if r, err = os.Create(path); err == nil { + defer r.Close() + err = yaml.NewEncoder(r).Encode(cc) + } + return err +} + +func ensureContainerStarted(helpers test.Helpers, con string) { + const maxRetry = 5 + const sleep = time.Second + success := false + for i := 0; i < maxRetry && !success; i++ { + time.Sleep(sleep) + count := i + cmd := helpers.Command("container", "inspect", con) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + if dc[0].State.Running { + success = true + return + } + if count == maxRetry-1 { + // FIXME: there is currently no simple way to capture stderr + // Sometimes, it is convenient for debugging, like here + // Here we cheat with unbuffer which will bundle stderr and stdout together + // This is just bad + t.Error(helpers.Err("logs", con)) + t.Fatalf("container %s still not running after %d retries", con, count) + } + }, + }) + } +} + +func NewCesantaAuthServer(data test.Data, helpers test.Helpers, ca *ca.CA, port int, user, pass string, tls bool) *TokenAuthServer { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(helpers.T(), err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + bpass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + assert.NilError(helpers.T(), err, fmt.Errorf("failed bcrypt encrypting password: %w", err)) + // Prepare configuration file for authentication server + // Details: https://github.com/cesanta/docker_auth/blob/1.7.1/examples/simple.yml + configFile, err := os.CreateTemp(data.TempDir(), "authconfig") + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating temporary directory for config file: %w", err)) + configFileName := configFile.Name() + + cc := &CesantaConfig{ + Server: CesantaConfigServer{ + Addr: ":5100", + }, + Token: CesantaConfigToken{ + Issuer: "Cesanta auth server", + Expiration: 900, + }, + Users: map[string]CesantaConfigUser{ + user: { + Password: string(bpass), + }, + }, + ACL: CesantaConfigACL{ + { + Match: CesantaMatchConditions{ + Account: user, + }, + Actions: []string{"*"}, + }, + }, + } + + scheme := "http" + if tls { + scheme = "https" + cc.Server.Certificate = "/auth/domain.crt" + cc.Server.Key = "/auth/domain.key" + } else { + cc.Token.Certificate = "/auth/domain.crt" + cc.Token.Key = "/auth/domain.key" + } + + err = cc.Save(configFileName) + assert.NilError(helpers.T(), err, fmt.Errorf("failed writing configuration: %w", err)) + + cert := ca.NewCert(hostIP.String()) + // FIXME: this will fail in many circumstances. Review strategy on how to acquire a free port. + // We probably have better code for that already somewhere. + port, err = portlock.Acquire(port) + assert.NilError(helpers.T(), err, fmt.Errorf("failed acquiring port: %w", err)) + containerName := data.Identifier(fmt.Sprintf("cesanta-auth-server-%d-%t", port, tls)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + errCertClose := cert.Close() + errConfigClose := configFile.Close() + errConfigRemove := os.Remove(configFileName) + if errPortRelease != nil { + helpers.T().Error(errPortRelease.Error()) + } + if errCertClose != nil { + helpers.T().Error(errCertClose.Error()) + } + if errConfigClose != nil { + helpers.T().Error(errConfigClose.Error()) + } + if errConfigRemove != nil { + helpers.T().Error(errConfigRemove.Error()) + } + } + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure( + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:5100", listenIP, port), + "--name", containerName, + "-v", cert.CertPath+":/auth/domain.crt", + "-v", cert.KeyPath+":/auth/domain.key", + "-v", configFileName+":/config/auth_config.yml", + platform.DockerAuthImage, + "/config/auth_config.yml", + ) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/auth", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 10, + true) + assert.NilError(helpers.T(), err, fmt.Errorf("failed starting auth container in a timely manner: %w", err)) + + } + + return &TokenAuthServer{ + IP: hostIP, + Port: port, + Scheme: scheme, + CertPath: cert.CertPath, + Auth: &TokenAuth{ + Address: scheme + "://" + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + CertPath: cert.CertPath, + }, + Setup: setup, + Cleanup: cleanup, + Logs: func(data test.Data, helpers test.Helpers) { + helpers.T().Error(helpers.Err("logs", containerName)) + }, + } +} diff --git a/pkg/testutil/nerdtest/registry/common.go b/pkg/testutil/nerdtest/registry/common.go new file mode 100644 index 00000000000..0f7496b049a --- /dev/null +++ b/pkg/testutil/nerdtest/registry/common.go @@ -0,0 +1,108 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "fmt" + "net" + "os" + "path/filepath" + + "golang.org/x/crypto/bcrypt" + + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +// Auth describes a struct able to serialize authenticator information into arguments to be fed to a registry container run +type Auth interface { + Params(data test.Data) []string +} + +type NoAuth struct { +} + +func (na *NoAuth) Params(data test.Data) []string { + return []string{} +} + +type TokenAuth struct { + Address string + CertPath string +} + +// FIXME: this is specific to Docker Registry +// Like need something else for Harbor and Gitlab +func (ta *TokenAuth) Params(data test.Data) []string { + return []string{ + "--env", "REGISTRY_AUTH=token", + "--env", "REGISTRY_AUTH_TOKEN_REALM=" + ta.Address + "/auth", + "--env", "REGISTRY_AUTH_TOKEN_SERVICE=Docker registry", + "--env", "REGISTRY_AUTH_TOKEN_ISSUER=Cesanta auth server", + "--env", "REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/auth/domain.crt", + "-v", ta.CertPath + ":/auth/domain.crt", + } +} + +type BasicAuth struct { + Realm string + HtFile string + Username string + Password string +} + +func (ba *BasicAuth) Params(data test.Data) []string { + if ba.Realm == "" { + ba.Realm = "Basic Realm" + } + if ba.HtFile == "" && ba.Username != "" && ba.Password != "" { + pass := ba.Password + encryptedPass, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + tmpDir, _ := os.MkdirTemp(data.TempDir(), "htpasswd") + ba.HtFile = filepath.Join(tmpDir, "htpasswd") + _ = os.WriteFile(ba.HtFile, []byte(fmt.Sprintf(`%s:%s`, ba.Username, string(encryptedPass[:]))), 0600) + } + ret := []string{ + "--env", "REGISTRY_AUTH=htpasswd", + "--env", "REGISTRY_AUTH_HTPASSWD_REALM=" + ba.Realm, + "--env", "REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd", + } + if ba.HtFile != "" { + ret = append(ret, "-v", ba.HtFile+":/htpasswd") + } + return ret +} + +type TokenAuthServer struct { + Scheme string + IP net.IP + Port int + CertPath string + Cleanup func(data test.Data, helpers test.Helpers) + Setup func(data test.Data, helpers test.Helpers) + Logs func(data test.Data, helpers test.Helpers) + Auth Auth +} + +type Server struct { + Scheme string + IP net.IP + Port int + Cleanup func(data test.Data, helpers test.Helpers) + Setup func(data test.Data, helpers test.Helpers) + Logs func(data test.Data, helpers test.Helpers) + HostsDir string // contains ":/hosts.toml" +} diff --git a/pkg/testutil/nerdtest/registry/docker.go b/pkg/testutil/nerdtest/registry/docker.go new file mode 100644 index 00000000000..0731d67f86f --- /dev/null +++ b/pkg/testutil/nerdtest/registry/docker.go @@ -0,0 +1,154 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "fmt" + "net" + "os" + "strconv" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func NewDockerRegistry(data test.Data, helpers test.Helpers, currentCA *ca.CA, port int, auth Auth) *Server { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(helpers.T(), err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + // XXX RELEASE PORT IN CLEANUP HERE + // FIXME: this will fail in many circumstances. Review strategy on how to acquire a free port. + // We probably have better code for that already somewhere. + port, err = portlock.Acquire(port) + assert.NilError(helpers.T(), err, fmt.Errorf("failed acquiring port: %w", err)) + + containerName := data.Identifier(fmt.Sprintf("docker-registry-server-%d-%t", port, currentCA != nil)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + args := []string{ + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:5000", listenIP, port), + "--name", containerName, + } + scheme := "http" + var cert *ca.Cert + if currentCA != nil { + scheme = "https" + cert = currentCA.NewCert(hostIP.String(), "127.0.0.1", "localhost", "::1") + args = append(args, + "--env", "REGISTRY_HTTP_TLS_CERTIFICATE=/registry/domain.crt", + "--env", "REGISTRY_HTTP_TLS_KEY=/registry/domain.key", + "-v", cert.CertPath+":/registry/domain.crt", + "-v", cert.KeyPath+":/registry/domain.key", + ) + } + + // Attach authentication params returns by authenticator + args = append(args, auth.Params(data)...) + + // Get the right registry version + registryImage := platform.RegistryImageStable + up := os.Getenv("DISTRIBUTION_VERSION") + if up != "" { + if up[0:1] != "v" { + up = "v" + up + } + registryImage = platform.RegistryImageNext + up + } + args = append(args, registryImage) + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + + if cert != nil { + assert.NilError(helpers.T(), cert.Close(), fmt.Errorf("failed cleaning certificates: %w", err)) + } + + assert.NilError(helpers.T(), errPortRelease, fmt.Errorf("failed releasing port: %w", err)) + } + + // FIXME: in the future, we will want to further manipulate hosts toml file from the test + // This should then return the struct, instead of saving it on its own + hostsDir, err := func() (string, error) { + hDir, err := os.MkdirTemp(data.TempDir(), "certs.d") + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating directory certs.d: %w", err)) + + if currentCA != nil { + hostTomlContent := &hoststoml.HostsToml{ + CA: currentCA.CertPath, + } + + err = hostTomlContent.Save(hDir, hostIP.String(), port) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "127.0.0.1", port) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "localhost", port) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + if port == 443 { + err = hostTomlContent.Save(hDir, hostIP.String(), 0) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "127.0.0.1", 0) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + err = hostTomlContent.Save(hDir, "localhost", 0) + assert.NilError(helpers.T(), err, fmt.Errorf("failed creating hosts.toml file: %w", err)) + + } + } + + return hDir, nil + }() + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure(args...) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/v2", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 10, + true) + assert.NilError(helpers.T(), err, fmt.Errorf("failed starting docker registry in a timely manner: %w", err)) + } + + return &Server{ + Scheme: scheme, + IP: hostIP, + Port: port, + Cleanup: cleanup, + Setup: setup, + Logs: func(data test.Data, helpers test.Helpers) { + helpers.T().Error(helpers.Err("logs", containerName)) + }, + HostsDir: hostsDir, + } +} diff --git a/pkg/testutil/nerdtest/registry/kubo.go b/pkg/testutil/nerdtest/registry/kubo.go new file mode 100644 index 00000000000..8cb350a65b0 --- /dev/null +++ b/pkg/testutil/nerdtest/registry/kubo.go @@ -0,0 +1,90 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package registry + +import ( + "fmt" + "net" + "strconv" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" + "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func NewKuboRegistry(data test.Data, helpers test.Helpers, t *testing.T, currentCA *ca.CA, port int, auth Auth) *Server { + // listen on 0.0.0.0 to enable 127.0.0.1 + listenIP := net.ParseIP("0.0.0.0") + hostIP, err := nettestutil.NonLoopbackIPv4() + assert.NilError(t, err, fmt.Errorf("failed finding ipv4 non loopback interface: %w", err)) + port, err = portlock.Acquire(port) + assert.NilError(t, err, fmt.Errorf("failed acquiring port: %w", err)) + + containerName := data.Identifier(fmt.Sprintf("kubo-registry-server-%d-%t", port, currentCA != nil)) + // Cleanup possible leftovers first + helpers.Ensure("rm", "-f", containerName) + + args := []string{ + "run", + "--pull=never", + "-d", + "-p", fmt.Sprintf("%s:%d:%d", listenIP, port, port), + "--name", containerName, + "--entrypoint=/bin/sh", + platform.KuboImage, + "-c", "--", + fmt.Sprintf("ipfs init && ipfs config Addresses.API /ip4/0.0.0.0/tcp/%d && ipfs daemon --offline", port), + } + + scheme := "http" + + cleanup := func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", containerName) + errPortRelease := portlock.Release(port) + + assert.NilError(t, errPortRelease, fmt.Errorf("failed releasing port: %w", err)) + } + + setup := func(data test.Data, helpers test.Helpers) { + helpers.Ensure(args...) + ensureContainerStarted(helpers, containerName) + _, err = nettestutil.HTTPGet(fmt.Sprintf("%s://%s/api/v0", + scheme, + net.JoinHostPort(hostIP.String(), strconv.Itoa(port)), + ), + 30, + true) + logs := helpers.Capture("logs", containerName) + assert.NilError(t, err, fmt.Errorf("failed starting kubo registry in a timely manner: %w - logs: %s", err, logs)) + } + + return &Server{ + IP: hostIP, + Port: port, + Scheme: scheme, + Cleanup: cleanup, + Setup: setup, + Logs: func(data test.Data, helpers test.Helpers) { + helpers.T().Error(helpers.Err("logs", containerName)) + }, + } +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index f75f3e8b7f7..6c707508699 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -19,125 +19,290 @@ package nerdtest import ( "encoding/json" "fmt" - "testing" + "os" + "os/exec" + "strings" "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/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) +var BuildkitHost test.ConfigKey = "bkHost" + +// These are used for ambient requirements var ipv6 test.ConfigKey = "IPv6Test" +var kubernetes test.ConfigKey = "KubeTest" +var flaky test.ConfigKey = "FlakyTest" var only test.ConfigValue = "Only" -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" - } - } - }, - }) +// These are used for down the road configuration and custom behavior inside command +var modePrivate test.ConfigKey = "PrivateMode" +var stargz test.ConfigKey = "Stargz" +var ipfs test.ConfigKey = "IPFS" +var enabled test.ConfigValue = "Enabled" - return ret, mess -}) +// OnlyIPv6 marks a test as suitable to be run exclusively inside an ipv6 environment +// This is an ambient requirement +var OnlyIPv6 = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + helpers.Write(ipv6, only) + ret = environmentHasIPv6() + if !ret { + mess = "runner skips IPv6 compatible tests in the non-IPv6 environment" + } + return ret, mess + }, +} + +// OnlyKubernetes marks a test as meant to be tested on Kubernetes +// This is an ambient requirement +var OnlyKubernetes = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + helpers.Write(kubernetes, only) + ret = environmentHasKubernetes() + if !ret { + mess = "runner skips Kubernetes compatible tests in the non-Kubernetes environment" + } + 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" +// IsFlaky marks a test as randomly failing. +// This is an ambient requirement +var IsFlaky = func(issueLink string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + // We do not even want to get to the setup phase here + helpers.Write(flaky, only) + ret = environmentIsForFlaky() + if !ret { + mess = "runner skips flaky compatible tests in the non-flaky environment" + } + return ret, mess + }, } - return ret, mess -}) - -var NerdctlNeedsFixing = 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 nerdctl, but it is currently broken and not working for this" +} + +// Docker marks a test as suitable solely for Docker and not Nerdctl +// Generally used as test.Not(nerdtest.Docker), which of course it the opposite +var Docker = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = getTarget() == targetDocker + if ret { + mess = "current target is docker" + } else { + mess = "current target is not docker" + } + return ret, mess + }, +} + +// NerdctlNeedsFixing marks a test as unsuitable to be run for Nerdctl, because of a specific known issue which +// url must be passed as an argument +var NerdctlNeedsFixing = func(issueLink string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = getTarget() == targetDocker + if ret { + mess = "current target is docker" + } else { + mess = "current target is nerdctl, but we will skip as nerdctl currently has issue: " + issueLink + } + return ret, mess + }, } - 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" +} + +// BrokenTest marks a test as currently broken, with explanation provided in message, along with +// additional requirements / restrictions describing what it can run on. +var BrokenTest = func(message string, req *test.Requirement) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + ret, mess := req.Check(data, helpers) + return ret, message + "\n" + mess + }, + Setup: req.Setup, + Cleanup: req.Cleanup, } - 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 { +} + +// RootLess marks a test as suitable only for the rootless environment +var RootLess = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + // Make sure we DO not return "IsRootless true" for docker + ret = getTarget() == targetNerdctl && rootlessutil.IsRootless() + if ret { + mess = "environment is root-less" + } else { + mess = "environment is root-ful" + } + return ret, mess + }, +} + +// RootFul marks a test as suitable only for rootful env +var RootFul = test.Not(RootLess) + +// CGroup requires that cgroup is enabled +var CGroup = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = true + mess = "cgroup is enabled" + stdout := helpers.Capture("info", "--format", "{{ json . }}") + var dinf dockercompat.Info + err := json.Unmarshal([]byte(stdout), &dinf) + assert.NilError(helpers.T(), err, "failed to parse docker info") + switch dinf.CgroupDriver { + case "none", "": ret = false - mess = fmt.Sprintf("buildkitd is not enabled: %+v", err) + mess = "cgroup is none" } - } - 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 + }, +} + +// Soci requires that the soci snapshotter is enabled +var Soci = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = false + mess = "soci is not enabled" + stdout := helpers.Capture("info", "--format", "{{ json . }}") + var dinf dockercompat.Info + err := json.Unmarshal([]byte(stdout), &dinf) + assert.NilError(helpers.T(), err, "failed to parse docker info") + for _, p := range dinf.Plugins.Storage { + if p == "soci" { + ret = true + mess = "soci is enabled" + } + } + return ret, mess + }, +} + +var Stargz = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + ret = false + mess = "stargz is not enabled" + stdout := helpers.Capture("info", "--format", "{{ json . }}") + var dinf dockercompat.Info + err := json.Unmarshal([]byte(stdout), &dinf) + assert.NilError(helpers.T(), err, "failed to parse docker info") + for _, p := range dinf.Plugins.Storage { + if p == "stargz" { + ret = true + mess = "stargz is enabled" + } + } + // Need this to happen now for Cleanups to work + // FIXME: we should be able to access the env (at least through helpers.Command().) instead of this gym + helpers.Write(stargz, enabled) + return ret, mess + }, +} + +// Registry marks a test as requiring a registry to be deployed +var Registry = test.Require( + // Registry requires Linux currently + test.Linux, + (func() *test.Requirement { + // Provisional: see note in cleanup + // var reg *registry.Server + + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + return true, "" + }, + Setup: func(data test.Data, helpers test.Helpers) { + // Ensure we have registry images now, so that we can run --pull=never + // This is useful for two reasons: + // - if ghcr.io is out, we want to fail early + // - when we start a large number of registries in subtests, no need to round-trip to ghcr everytime + // This of course assumes that the subtests are NOT going to prune / rmi images + registryImage := platform.RegistryImageStable + up := os.Getenv("DISTRIBUTION_VERSION") + if up != "" { + if up[0:1] != "v" { + up = "v" + up + } + registryImage = platform.RegistryImageNext + up } + helpers.Ensure("pull", "--quiet", registryImage) + helpers.Ensure("pull", "--quiet", platform.DockerAuthImage) + helpers.Ensure("pull", "--quiet", platform.KuboImage) }, - }) + Cleanup: func(data test.Data, helpers test.Helpers) { + // FIXME: figure out what to do with reg setup/cleanup routines + // Provisionally, reg is available here in the closure + }, + } + })(), +) + +// Build marks a test as suitable only if buildkitd is enabled (only tested for nerdctl obviously) +var Build = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + // FIXME: shouldn't we run buildkitd in a container? At least for testing, that would be so much easier than + // against the host install + ret := true + mess := "buildkitd is enabled" - return ret, mess -}) + if getTarget() == targetNerdctl { + bkHostAddr, err := buildkitutil.GetBuildkitHost(defaultNamespace) + if err != nil { + ret = false + mess = fmt.Sprintf("buildkitd is not enabled: %+v", err) + return ret, mess + } + // We also require the buildctl binary in the path + _, err = exec.LookPath("buildctl") + if err != nil { + ret = false + mess = fmt.Sprintf("buildctl is not in the path: %+v", err) + return ret, mess + } + helpers.Write(BuildkitHost, test.ConfigValue(bkHostAddr)) + } + return ret, mess + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + }, +} + +var IPFS = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + // FIXME: we should be able to access the env (at least through helpers.Command().) instead of this gym + helpers.Write(ipfs, enabled) + // FIXME: this is incomplete. We obviously need a daemon running, properly configured + return test.Binary("ipfs").Check(data, helpers) + }, +} + +// Private makes a test run inside a dedicated namespace, with a private config.toml, hosts directory, and DOCKER_CONFIG path +// If the target is docker, parallelism is forcefully disabled +var Private = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + // We need this to happen NOW and not in setup, as otherwise cleanup with operate on the default namespace + namespace := data.Identifier("private") + helpers.Write(Namespace, test.ConfigValue(namespace)) + data.Set("_deletenamespace", namespace) + // FIXME: is this necessary? Should NoParallel be subsumed into config? + helpers.Write(modePrivate, enabled) + return true, "private mode creates a dedicated namespace for nerdctl, and disable parallelism for docker" + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + containerList := strings.TrimSpace(strings.Replace(helpers.Capture("ps", "-aq"), "\n", " ", -1)) + if containerList != "" { + helpers.Ensure("rm", "-f", containerList) + } + helpers.Ensure("system", "prune", "-f", "--all", "--volumes") + // FIXME: there are conditions where we still have some stuff in there and this fails... + helpers.Anyhow("namespace", "remove", data.Get("_deletenamespace")) + }, +} diff --git a/pkg/testutil/nerdtest/test.go b/pkg/testutil/nerdtest/test.go index 78046cb9cca..7bfd485cdf2 100644 --- a/pkg/testutil/nerdtest/test.go +++ b/pkg/testutil/nerdtest/test.go @@ -17,154 +17,51 @@ package nerdtest import ( - "os" - "path/filepath" "testing" - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func Setup() { - test.CustomCommand(nerdctlSetup) -} - -// Nerdctl specific config key and values -var NerdctlToml test.ConfigKey = "NerdctlToml" var DockerConfig test.ConfigKey = "DockerConfig" +var Namespace test.ConfigKey = "Namespace" +var NerdctlToml test.ConfigKey = "NerdctlToml" var HostsDir test.ConfigKey = "HostsDir" var DataRoot test.ConfigKey = "DataRoot" -var Namespace test.ConfigKey = "Namespace" +var Debug test.ConfigKey = "Debug" -type NerdCommand struct { - test.GenericCommand - // FIXME: annoying - forces custom Clone, etc - Target string -} - -// Run does override the generic command run, as we are testing both docker and nerdctl -func (nc *NerdCommand) Run(expect *test.Expected) { - // We are not in the business of testing docker error output, so, spay expect for errors testing, if any - if expect != nil && nc.Target != testutil.Nerdctl { - expect.Errors = nil +func Setup() *test.Case { + test.Customize(&nerdctlSetup{}) + return &test.Case{ + Env: map[string]string{}, } - - nc.GenericCommand.Run(expect) } -// Clone is overridden as well, as we need to pass along the target -func (nc *NerdCommand) Clone() test.Command { - return &NerdCommand{ - GenericCommand: *((nc.GenericCommand.Clone()).(*test.GenericCommand)), - Target: nc.Target, - } +type nerdctlSetup struct { } -func nerdctlSetup(testCase *test.Case, t *testing.T) test.Command { - t.Helper() - - var testUtilBase *testutil.Base - dt := testCase.Data - var pvNamespace string - inherited := false +func (ns *nerdctlSetup) CustomCommand(testCase *test.Case, t *testing.T) test.CustomizableCommand { + return newNerdCommand(testCase.Config, t) +} - if dt.ReadConfig(ipv6) != only && testutil.GetEnableIPv6() { +func (ns *nerdctlSetup) AmbientRequirements(testCase *test.Case, t *testing.T) { + // Ambient requirements, bail out now if these do not match + if environmentHasIPv6() && testCase.Config.Read(ipv6) != only { t.Skip("runner skips non-IPv6 compatible tests in the IPv6 environment") } - if dt.ReadConfig(mode) == modePrivate { - // If private was inherited, we already got a configured namespace - if dt.ReadConfig(Namespace) != "" { - pvNamespace = string(dt.ReadConfig(Namespace)) - inherited = true - } else { - // Otherwise, we need to set everything up - pvNamespace = testCase.Data.Identifier() - dt.WithConfig(Namespace, test.ConfigValue(pvNamespace)) - testCase.Env["DOCKER_CONFIG"] = testCase.Data.TempDir() - testCase.Env["NERDCTL_TOML"] = filepath.Join(testCase.Data.TempDir(), "nerdctl.toml") - dt.WithConfig(HostsDir, test.ConfigValue(testCase.Data.TempDir())) - // Setting data root is more trouble than anything and does not significantly increase isolation - // dt.WithConfig(DataRoot, test.ConfigValue(testCase.Data.TempDir())) - } - testUtilBase = testutil.NewBaseWithNamespace(t, pvNamespace) - if testUtilBase.Target == testutil.Docker { - // For docker, just disable parallel - testCase.NoParallel = true - } - } else if dt.ReadConfig(Namespace) != "" { - pvNamespace = string(dt.ReadConfig(Namespace)) - testUtilBase = testutil.NewBaseWithNamespace(t, pvNamespace) - } else { - testUtilBase = testutil.NewBase(t) + if environmentHasKubernetes() && testCase.Config.Read(kubernetes) != only { + t.Skip("runner skips non-Kubernetes compatible tests in the Kubernetes environment") } - // If we were passed custom content for NerdctlToml, save it - // Not happening if this is not nerdctl of course - if testUtilBase.Target == testutil.Nerdctl { - if dt.ReadConfig(NerdctlToml) != "" { - dest := filepath.Join(testCase.Data.TempDir(), "nerdctl.toml") - testCase.Env["NERDCTL_TOML"] = dest - err := os.WriteFile(dest, []byte(dt.ReadConfig(NerdctlToml)), 0400) - assert.NilError(t, err, "failed to write custom nerdctl toml file for test") - } - if dt.ReadConfig(DockerConfig) != "" { - dest := filepath.Join(testCase.Data.TempDir(), "config.json") - testCase.Env["DOCKER_CONFIG"] = filepath.Dir(dest) - err := os.WriteFile(dest, []byte(dt.ReadConfig(DockerConfig)), 0400) - assert.NilError(t, err, "failed to write custom docker config json file for test") - } + if environmentIsForFlaky() && testCase.Config.Read(flaky) != only { + t.Skip("runner skips non-flaky tests in the flaky environment") } - // Build the base - baseCommand := &NerdCommand{} - baseCommand.WithBinary(testUtilBase.Binary) - baseCommand.WithArgs(testUtilBase.Args...) - baseCommand.WithEnv(testCase.Env) - baseCommand.WithT(t) - baseCommand.WithTempDir(testCase.Data.TempDir()) - baseCommand.Target = testUtilBase.Target - - if testUtilBase.Target == testutil.Nerdctl { - if dt.ReadConfig(HostsDir) != "" { - baseCommand.WithArgs("--hosts-dir=" + string(dt.ReadConfig(HostsDir))) - } - - if dt.ReadConfig(DataRoot) != "" { - baseCommand.WithArgs("--data-root=" + string(dt.ReadConfig(DataRoot))) - } - } - - // If we were in a custom namespace, not inherited - make sure we clean up the namespace - if testUtilBase.Target == testutil.Nerdctl && pvNamespace != "" && !inherited { - cleanup := func() { - // Stop all containers, then prune everything - containerList := baseCommand.Clone() - containerList.WithArgs("ps", "-q") - containerList.Run(&test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - if stdout != "" { - containerRm := baseCommand.Clone() - containerRm.WithArgs("rm", "-f", stdout) - containerRm.Run(&test.Expected{}) - } - }, - }) - - systemPrune := baseCommand.Clone() - systemPrune.WithArgs("system", "prune", "-f", "--all", "--volumes") - systemPrune.Run(&test.Expected{}) - - cleanNamespace := baseCommand.Clone() - cleanNamespace.WithArgs("namespace", "remove", pvNamespace) - cleanNamespace.Run(nil) - } - cleanup() - t.Cleanup(cleanup) + if getTarget() == targetDocker && testCase.Config.Read(modePrivate) == enabled { + // For docker, we do disable parallel since there is no namespace where we can isolate + testCase.NoParallel = true } - // Attach the base command - return baseCommand + // We do not want private to get inherited by subtests, as we want them to be in the same namespace set here + testCase.Config.Write(modePrivate, "") } diff --git a/pkg/testutil/nerdtest/third-party.go b/pkg/testutil/nerdtest/third-party.go new file mode 100644 index 00000000000..21199f3a815 --- /dev/null +++ b/pkg/testutil/nerdtest/third-party.go @@ -0,0 +1,63 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package nerdtest + +import ( + "os/exec" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/ca" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func BuildCtlCommand(helpers test.Helpers, args ...string) test.TestableCommand { + assert.Assert(helpers.T(), string(helpers.Read(BuildkitHost)) != "", "You first need to Require Build to use buildctl") + buildctl, _ := exec.LookPath("buildctl") + cmd := helpers.Custom(buildctl) + cmd.WithArgs("--addr=" + string(helpers.Read(BuildkitHost))) + cmd.WithArgs(args...) + return cmd +} + +func RegistryWithTokenAuth(data test.Data, helpers test.Helpers, user, pass string, port int, tls bool) (*registry.Server, *registry.TokenAuthServer) { + rca := ca.New(data, helpers.T()) + as := registry.NewCesantaAuthServer(data, helpers, rca, 0, user, pass, tls) + re := registry.NewDockerRegistry(data, helpers, rca, port, as.Auth) + return re, as +} + +func RegistryWithNoAuth(data test.Data, helpers test.Helpers, port int, tls bool) *registry.Server { + var rca *ca.CA + if tls { + rca = ca.New(data, helpers.T()) + } + return registry.NewDockerRegistry(data, helpers, rca, port, ®istry.NoAuth{}) +} + +func RegistryWithBasicAuth(data test.Data, helpers test.Helpers, user, pass string, port int, tls bool) *registry.Server { + auth := ®istry.BasicAuth{ + Username: user, + Password: pass, + } + var rca *ca.CA + if tls { + rca = ca.New(data, helpers.T()) + } + return registry.NewDockerRegistry(data, helpers, rca, port, auth) +} diff --git a/pkg/testutil/nerdtest/helpers.go b/pkg/testutil/nerdtest/utilities.go similarity index 83% rename from pkg/testutil/nerdtest/helpers.go rename to pkg/testutil/nerdtest/utilities.go index 346a6c9122a..384f5132110 100644 --- a/pkg/testutil/nerdtest/helpers.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -25,15 +25,19 @@ import ( "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) +func IsDocker() bool { + return testutil.GetTarget() == "docker" +} + // 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) @@ -43,14 +47,10 @@ func InspectContainer(helpers test.Helpers, name string) dockercompat.Container return dc[0] } -func InspectVolume(helpers test.Helpers, name string, args ...string) native.Volume { +func InspectVolume(helpers test.Helpers, name string) native.Volume { var dc []native.Volume - cmdArgs := append([]string{"volume", "inspect"}, args...) - cmdArgs = append(cmdArgs, name) - - cmd := helpers.Command(cmdArgs...) + cmd := helpers.Command("volume", "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) @@ -60,14 +60,10 @@ func InspectVolume(helpers test.Helpers, name string, args ...string) native.Vol return dc[0] } -func InspectNetwork(helpers test.Helpers, name string, args ...string) dockercompat.Network { +func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { var dc []dockercompat.Network - cmdArgs := append([]string{"network", "inspect"}, args...) - cmdArgs = append(cmdArgs, name) - - cmd := helpers.Command(cmdArgs...) + cmd := helpers.Command("network", "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) @@ -81,7 +77,6 @@ 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) @@ -91,16 +86,16 @@ func InspectImage(helpers test.Helpers, name string) dockercompat.Image { return dc[0] } +const ( + maxRetry = 5 + sleep = time.Second +) + 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) @@ -110,7 +105,7 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { return } if count == maxRetry-1 { - t.Fatalf("conainer %s not running", con) + t.Fatalf("container %s still not running after %d retries", con, count) } time.Sleep(sleep) }, diff --git a/pkg/testutil/test/case.go b/pkg/testutil/test/case.go index 36384c6962b..58f9f573757 100644 --- a/pkg/testutil/test/case.go +++ b/pkg/testutil/test/case.go @@ -17,159 +17,181 @@ package test import ( + "slices" "testing" "gotest.tools/v3/assert" ) -// Group informally describes a slice of tests -type Group []*Case - -func (tg *Group) Run(t *testing.T) { - t.Helper() - // If the group contains only one test, no need to create a subtest - sub := len(*tg) > 1 - if sub { - t.Parallel() - } - // Run each subtest - for _, tc := range *tg { - tc.subIt = sub - tc.Run(t) - } -} - // Case describes an entire test-case, including data, setup and cleanup routines, command and expectations type Case struct { // Description contains a human-readable short desc, used as a seed for the identifier and as a title for the test Description string // NoParallel disables parallel execution if set to true + // This obviously implies that all tests run in parallel, by default. This is a design choice. NoParallel bool - // Env contains a map of environment variables to use for commands run in Setup, Command and Cleanup + // Env contains a map of environment variables to use as a base for all commands run in Setup, Command and Cleanup // Note that the environment is inherited by subtests Env map[string]string // Data contains test specific data, accessible to all operations, also inherited by subtests Data Data + // Config contains specific information meaningful to the binary being tested. + // It is also inherited by subtests + Config Config + // Requirement + Require *Requirement // Setup Setup Butler - // Expected - Expected Manager // Command Command Executor + // Expected + Expected Manager // Cleanup Cleanup Butler - // Requirement - Require Requirement // SubTests SubTests []*Case // Private - helpers Helpers - t *testing.T - parent *Case - baseCommand Command - - subIt bool + helpers Helpers + t *testing.T + parent *Case } // Run prepares and executes the test, and any possible subtests func (test *Case) Run(t *testing.T) { t.Helper() // Run the test - testRun := func(tt *testing.T) { - tt.Helper() - test.seal(tt) - - if registeredInit == nil { - bc := &GenericCommand{} - bc.WithEnv(test.Env) - bc.WithT(tt) - bc.WithTempDir(test.Data.TempDir()) - test.baseCommand = bc + testRun := func(subT *testing.T) { + subT.Helper() + + assert.Assert(subT, test.t == nil, "You cannot run a test multiple times") + + // Attach testing.T + test.t = subT + assert.Assert(test.t, test.Description != "" || test.parent == nil, "A test description cannot be empty") + assert.Assert(test.t, test.Command == nil || test.Expected != nil, + "Expectations for a test command cannot be nil. You may want to use Setup instead.") + + // Ensure we have env + if test.Env == nil { + test.Env = map[string]string{} + } + + // If we have a parent, get parent env, data and config + var parentData Data + var parentConfig Config + if test.parent != nil { + parentData = test.parent.Data + parentConfig = test.parent.Config + for k, v := range test.parent.Env { + if _, ok := test.Env[k]; !ok { + test.Env[k] = v + } + } + } + + // Inherit and attach Data and Config + test.Data = configureData(test.t, test.Data, parentData) + test.Config = configureConfig(test.Config, parentConfig) + + var b CustomizableCommand + if registeredTestable == nil { + b = &GenericCommand{} } else { - test.baseCommand = registeredInit(test, test.t) + b = registeredTestable.CustomCommand(test, test.t) } - test.exec(tt) - } + b.WithCwd(test.Data.TempDir()) - if test.subIt { - t.Run(test.Description, testRun) - } else { - testRun(t) - } -} + b.withT(test.t) + b.withTempDir(test.Data.TempDir()) + b.withEnv(test.Env) + b.withConfig(test.Config) -// seal is a private method to prepare the test -func (test *Case) seal(t *testing.T) { - t.Helper() - assert.Assert(t, test.t == nil, "You cannot run a test multiple times") - assert.Assert(t, test.Description != "", "A test description cannot be empty") - assert.Assert(t, test.Command == nil || test.Expected != nil, - "Expectations for a test command cannot be nil. You may want to use Setup instead.") - - // Ensure we have env - if test.Env == nil { - test.Env = map[string]string{} - } + // Attach the base command, and t + test.helpers = &helpersInternal{ + cmdInternal: b, + t: test.t, + } - // If we have a parent, get parent env and data - var parentData Data - if test.parent != nil { - parentData = test.parent.Data - for k, v := range test.parent.Env { - if _, ok := test.Env[k]; !ok { - test.Env[k] = v + setups := []func(data Data, helpers Helpers){} + cleanups := []func(data Data, helpers Helpers){} + + // Check the requirements before going any further + if test.Require != nil { + shouldRun, message := test.Require.Check(test.Data, test.helpers) + if !shouldRun { + test.t.Skipf("test skipped as: %s", message) + } + if test.Require.Setup != nil { + setups = append(setups, test.Require.Setup) + } + if test.Require.Cleanup != nil { + cleanups = append(cleanups, test.Require.Cleanup) } } - } - // Attach testing.T - test.t = t - // Inherit and attach Data - test.Data = configureData(t, test.Data, parentData) + // Register setup if any + if test.Setup != nil { + setups = append(setups, test.Setup) + } - // Check the requirements - if test.Require != nil { - test.Require(test.Data, true, t) - } -} + // Register cleanup if any + if test.Cleanup != nil { + cleanups = append(cleanups, test.Cleanup) + } -// exec is a private method that will take care of the test setup, command and cleanup execution -func (test *Case) exec(t *testing.T) { - t.Helper() - test.helpers = &helpers{ - test.baseCommand, - } + // Run optional post requirement hook + if registeredTestable != nil { + registeredTestable.AmbientRequirements(test, test.t) + } - // Set parallel unless asked not to - if !test.NoParallel { - t.Parallel() - } + // Set parallel unless asked not to + if !test.NoParallel { + test.t.Parallel() + } + + // Execute cleanups now + test.t.Log("======================== Pre-test cleanup ========================") + for _, cleanup := range cleanups { + cleanup(test.Data, test.helpers) + } - // Register cleanup if there is any, and run it to collect any leftovers from previous runs - if test.Cleanup != nil { - test.Cleanup(test.Data, test.helpers) - t.Cleanup(func() { - test.Cleanup(test.Data, test.helpers) + // Register the cleanups, in reverse + test.t.Cleanup(func() { + test.t.Log("======================== Post-test cleanup ========================") + slices.Reverse(cleanups) + for _, cleanup := range cleanups { + cleanup(test.Data, test.helpers) + } }) - } - // Run setup - if test.Setup != nil { - test.Setup(test.Data, test.helpers) - } + // Run the setups + test.t.Log("======================== Test setup ========================") + for _, setup := range setups { + setup(test.Data, test.helpers) + } - // Run the command if any, with expectations - if test.Command != nil { - test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers)) + // Run the command if any, with expectations + // Note: if we have a command, we already know we DO have Expected + test.t.Log("======================== Test Run ========================") + if test.Command != nil { + test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers)) + } + + // Now go for the subtests + test.t.Log("======================== Processing subtests ========================") + for _, subTest := range test.SubTests { + subTest.parent = test + subTest.Run(test.t) + } } - for _, subTest := range test.SubTests { - subTest.parent = test - subTest.subIt = true - subTest.Run(t) + if test.parent != nil { + t.Run(test.Description, testRun) + } else { + testRun(t) } } diff --git a/pkg/testutil/test/command.go b/pkg/testutil/test/command.go index 3e1b840db6e..fd7c4fa3b6a 100644 --- a/pkg/testutil/test/command.go +++ b/pkg/testutil/test/command.go @@ -30,94 +30,88 @@ import ( // GenericCommand is a concrete Command implementation type GenericCommand struct { - WorkingDir string - Env map[string]string + Config Config + TempDir string + Env map[string]string + + t *testing.T - t *testing.T - tempDir string helperBinary string helperArgs []string + prependArgs []string mainBinary string mainArgs []string - result *icmd.Result + + envBlackList []string stdin io.Reader async bool timeout time.Duration + workingDir string + + result *icmd.Result + rawStdErr string } -func (gc *GenericCommand) WithBinary(binary string) Command { +func (gc *GenericCommand) WithBinary(binary string) { gc.mainBinary = binary - return gc } -func (gc *GenericCommand) WithArgs(args ...string) Command { +func (gc *GenericCommand) WithArgs(args ...string) { gc.mainArgs = append(gc.mainArgs, args...) - return gc -} - -// WithEnv will overload the command env with values from the passed map -func (gc *GenericCommand) WithEnv(env map[string]string) Command { - if gc.Env == nil { - gc.Env = map[string]string{} - } - for k, v := range env { - gc.Env[k] = v - } - return gc } -func (gc *GenericCommand) WithWrapper(binary string, args ...string) Command { +func (gc *GenericCommand) WithWrapper(binary string, args ...string) { gc.helperBinary = binary gc.helperArgs = args - return gc } -// WithStdin sets the standard input of Cmd to the specified reader -func (gc *GenericCommand) WithStdin(r io.Reader) Command { +func (gc *GenericCommand) WithStdin(r io.Reader) { gc.stdin = r - return gc } -func (gc *GenericCommand) Background(timeout time.Duration) Command { - // Run it - gc.async = true - i := gc.boot() - gc.result = icmd.StartCmd(i) - gc.timeout = timeout - return gc +func (gc *GenericCommand) WithCwd(path string) { + gc.workingDir = path } -// TODO: it should be possible to: -// - timeout execution +// TODO: it should be possible to timeout execution +// Primitives (gc.timeout) is here, it is just a matter of exposing a WithTimeout method +// - UX to be decided +// - validate use case: would we ever need this? func (gc *GenericCommand) Run(expect *Expected) { + if gc.t != nil { + gc.t.Helper() + } + var result *icmd.Result var env []string if gc.async { result = icmd.WaitOnCmd(gc.timeout, gc.result) env = gc.result.Cmd.Env } else { - icmdCmd := gc.boot() - env = icmdCmd.Env + iCmdCmd := gc.boot() + env = iCmdCmd.Env // Run it - result = icmd.RunCmd(icmdCmd) + result = icmd.RunCmd(iCmdCmd) } + gc.rawStdErr = result.Stderr() + // Check our expectations, if any if expect != nil { - // Build the debug string - additionally attach the env (which icmd does not do) + // Build the debug string - additionally attach the env (which iCmd does not do) debug := result.String() + "Env:\n" + strings.Join(env, "\n") // ExitCode goes first if expect.ExitCode == -1 { assert.Assert(gc.t, result.ExitCode != 0, - "Expected exit code to be different than 0"+debug) + "Expected exit code to be different than 0\n"+debug) } else { assert.Assert(gc.t, expect.ExitCode == result.ExitCode, - fmt.Sprintf("Expected exit code: %d", expect.ExitCode)+debug) + fmt.Sprintf("Expected exit code: %d\n", expect.ExitCode)+debug) } // Range through the expected errors and confirm they are seen on stderr for _, expectErr := range expect.Errors { - assert.Assert(gc.t, strings.Contains(result.Stderr(), expectErr.Error()), - fmt.Sprintf("Expected error: %q to be found in stderr", expectErr.Error())+debug) + assert.Assert(gc.t, strings.Contains(gc.rawStdErr, expectErr.Error()), + fmt.Sprintf("Expected error: %q to be found in stderr\n", expectErr.Error())+debug) } // Finally, check the output if we are asked to if expect.Output != nil { @@ -126,6 +120,91 @@ func (gc *GenericCommand) Run(expect *Expected) { } } +func (gc *GenericCommand) Stderr() string { + return gc.rawStdErr +} + +func (gc *GenericCommand) Background(timeout time.Duration) { + // Run it + gc.async = true + i := gc.boot() + gc.timeout = timeout + gc.result = icmd.StartCmd(i) +} + +func (gc *GenericCommand) withEnv(env map[string]string) { + if gc.Env == nil { + gc.Env = map[string]string{} + } + for k, v := range env { + gc.Env[k] = v + } +} + +func (gc *GenericCommand) withTempDir(path string) { + gc.TempDir = path +} + +func (gc *GenericCommand) WithBlacklist(env []string) { + gc.envBlackList = env +} + +func (gc *GenericCommand) withConfig(config Config) { + gc.Config = config +} + +func (gc *GenericCommand) PrependArgs(args ...string) { + gc.prependArgs = append(gc.prependArgs, args...) +} + +func (gc *GenericCommand) Clone() TestableCommand { + // Copy the command and return a new one - with almost everything from the parent command + cc := *gc + cc.result = nil + cc.stdin = nil + cc.timeout = 0 + cc.rawStdErr = "" + // Clone Env + cc.Env = make(map[string]string, len(gc.Env)) + for k, v := range gc.Env { + cc.Env[k] = v + } + return &cc +} + +func (gc *GenericCommand) T() *testing.T { + return gc.t +} + +func (gc *GenericCommand) clear() TestableCommand { + cc := *gc + cc.mainBinary = "" + cc.helperBinary = "" + cc.mainArgs = []string{} + cc.prependArgs = []string{} + cc.helperArgs = []string{} + // Clone Env + cc.Env = make(map[string]string, len(gc.Env)) + // Reset configuration + cc.Config = &config{} + for k, v := range gc.Env { + cc.Env[k] = v + } + return &cc +} + +func (gc *GenericCommand) withT(t *testing.T) { + gc.t = t +} + +func (gc *GenericCommand) read(key ConfigKey) ConfigValue { + return gc.Config.Read(key) +} + +func (gc *GenericCommand) write(key ConfigKey, value ConfigValue) { + gc.Config.Write(key, value) +} + func (gc *GenericCommand) boot() icmd.Cmd { // This is a helper function, not to appear in the debugging output if gc.t != nil { @@ -133,7 +212,7 @@ func (gc *GenericCommand) boot() icmd.Cmd { } binary := gc.mainBinary - args := gc.mainArgs + args := append(gc.prependArgs, gc.mainArgs...) if gc.helperBinary != "" { args = append([]string{binary}, args...) args = append(gc.helperArgs, args...) @@ -141,58 +220,35 @@ func (gc *GenericCommand) boot() icmd.Cmd { } // Create the command and set the env - // TODO: do we really need icmd? - icmdCmd := icmd.Command(binary, args...) - icmdCmd.Env = []string{} + // TODO: do we really need iCmd? + gc.t.Log(binary, strings.Join(args, " ")) + + iCmdCmd := icmd.Command(binary, args...) + iCmdCmd.Env = []string{} for _, v := range os.Environ() { - // Ignore LS_COLORS from the env, just too much noise - if !strings.HasPrefix(v, "LS_COLORS") { - icmdCmd.Env = append(icmdCmd.Env, v) + add := true + for _, b := range gc.envBlackList { + if strings.HasPrefix(v, b+"=") { + add = false + break + } + } + if add { + iCmdCmd.Env = append(iCmdCmd.Env, v) } } // Ensure the subprocess gets executed in a temporary directory unless explicitly instructed otherwise - icmdCmd.Dir = gc.WorkingDir - if icmdCmd.Dir == "" { - icmdCmd.Dir = gc.tempDir - } + iCmdCmd.Dir = gc.workingDir if gc.stdin != nil { - icmdCmd.Stdin = gc.stdin + iCmdCmd.Stdin = gc.stdin } // Attach any extra env we have for k, v := range gc.Env { - icmdCmd.Env = append(icmdCmd.Env, fmt.Sprintf("%s=%s", k, v)) + iCmdCmd.Env = append(iCmdCmd.Env, fmt.Sprintf("%s=%s", k, v)) } - return icmdCmd -} - -func (gc *GenericCommand) Clone() Command { - // Copy the command and return a new one - with WorkingDir, binary, args, etc - cc := *gc - // Clone Env - cc.Env = make(map[string]string, len(gc.Env)) - for k, v := range gc.Env { - cc.Env[k] = v - } - return &cc -} - -func (gc *GenericCommand) Clear() Command { - gc.mainBinary = "" - gc.helperBinary = "" - gc.mainArgs = []string{} - gc.helperArgs = []string{} - return gc -} - -func (gc *GenericCommand) WithT(t *testing.T) Command { - gc.t = t - return gc -} - -func (gc *GenericCommand) WithTempDir(tempDir string) { - gc.tempDir = tempDir + return iCmdCmd } diff --git a/pkg/testutil/test/config.go b/pkg/testutil/test/config.go new file mode 100644 index 00000000000..6cf5ceb0c25 --- /dev/null +++ b/pkg/testutil/test/config.go @@ -0,0 +1,71 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package test + +// WithConfig returns a config object with a certain config property set +func WithConfig(key ConfigKey, value ConfigValue) Config { + cfg := &config{} + cfg.Write(key, value) + return cfg +} + +// Contains the implementation of the Config interface + +func configureConfig(cfg Config, parent Config) Config { + if cfg == nil { + cfg = &config{ + config: make(map[ConfigKey]ConfigValue), + } + } + if parent != nil { + // Note: implementation dependent + cfg.(*config).adopt(parent) + } + return cfg +} + +type config struct { + config map[ConfigKey]ConfigValue +} + +func (cfg *config) Write(key ConfigKey, value ConfigValue) Config { + if cfg.config == nil { + cfg.config = make(map[ConfigKey]ConfigValue) + } + cfg.config[key] = value + return cfg +} + +func (cfg *config) Read(key ConfigKey) ConfigValue { + if cfg.config == nil { + cfg.config = make(map[ConfigKey]ConfigValue) + } + if val, ok := cfg.config[key]; ok { + return val + } + return "" +} + +func (cfg *config) adopt(parent Config) { + // Note: implementation dependent + for k, v := range parent.(*config).config { + // Only copy keys that are not set already + if _, ok := cfg.config[k]; !ok { + cfg.Write(k, v) + } + } +} diff --git a/pkg/testutil/test/data.go b/pkg/testutil/test/data.go index a2cc2d58b87..97c7659c6d5 100644 --- a/pkg/testutil/test/data.go +++ b/pkg/testutil/test/data.go @@ -24,40 +24,41 @@ import ( "testing" ) -// Contains the implementation of the Data interface - -type data struct { - config map[ConfigKey]ConfigValue - - system map[SystemKey]SystemValue - - labels map[string]string - testID string - tempDir string +// WithData returns a data object with a certain key value set +func WithData(key string, value string) Data { + dat := &data{} + dat.Set(key, value) + return dat } -func (dt *data) WithConfig(key ConfigKey, value ConfigValue) Data { - if dt.config == nil { - dt.config = make(map[ConfigKey]ConfigValue) - } - dt.config[key] = value - return dt -} +// Contains the implementation of the Data interface -func (dt *data) ReadConfig(key ConfigKey) ConfigValue { - if dt.config == nil { - dt.config = make(map[ConfigKey]ConfigValue) +func configureData(t *testing.T, seedData Data, parent Data) Data { + if seedData == nil { + seedData = &data{} } - if val, ok := dt.config[key]; ok { - return val + dat := &data{ + // Note: implementation dependent + labels: seedData.(*data).labels, + tempDir: t.TempDir(), + testID: func(suffix ...string) string { + suffix = append([]string{t.Name()}, suffix...) + return defaultIdentifierHashing(suffix...) + }, } - return "" + if parent != nil { + dat.adopt(parent) + } + return dat +} + +type data struct { + labels map[string]string + testID func(suffix ...string) string + tempDir string } func (dt *data) Get(key string) string { - if dt.labels == nil { - dt.labels = map[string]string{} - } return dt.labels[key] } @@ -69,8 +70,8 @@ func (dt *data) Set(key string, value string) Data { return dt } -func (dt *data) Identifier() string { - return dt.testID +func (dt *data) Identifier(suffix ...string) string { + return dt.testID(suffix...) } func (dt *data) TempDir() string { @@ -78,43 +79,13 @@ func (dt *data) TempDir() string { } func (dt *data) adopt(parent Data) { - for k, v := range parent.getLabels() { + // Note: implementation dependent + for k, v := range parent.(*data).labels { // Only copy keys that are not set already if _, ok := dt.labels[k]; !ok { dt.Set(k, v) } } - for k, v := range parent.getConfig() { - // Only copy keys that are not set already - if _, ok := dt.config[k]; !ok { - dt.WithConfig(k, v) - } - } -} - -func (dt *data) Sink(key SystemKey, value SystemValue) { - if _, ok := dt.system[key]; !ok { - dt.system[key] = value - } else { - // XXX should we really panic? - panic(fmt.Sprintf("Unable to set system key %s multiple times", key)) - } -} - -func (dt *data) Surface(key SystemKey) SystemValue { - if v, ok := dt.system[key]; ok { - return v - } - // XXX should we really panic? - panic(fmt.Sprintf("Unable to retrieve system key %s", key)) -} - -func (dt *data) getLabels() map[string]string { - return dt.labels -} - -func (dt *data) getConfig() map[ConfigKey]ConfigValue { - return dt.config } func defaultIdentifierHashing(names ...string) string { @@ -138,20 +109,3 @@ func defaultIdentifierHashing(names ...string) string { } return name + "-" + signature } - -// TODO: allow to pass custom hashing methods? -func configureData(t *testing.T, seedData Data, parent Data) Data { - if seedData == nil { - seedData = &data{} - } - dat := &data{ - config: seedData.getConfig(), - labels: seedData.getLabels(), - tempDir: t.TempDir(), - testID: defaultIdentifierHashing(t.Name()), - } - if parent != nil { - dat.adopt(parent) - } - return dat -} diff --git a/pkg/testutil/test/expected.go b/pkg/testutil/test/expected.go index 81d617acfdf..9057974c909 100644 --- a/pkg/testutil/test/expected.go +++ b/pkg/testutil/test/expected.go @@ -18,32 +18,20 @@ package test import ( "fmt" + "regexp" "strings" "testing" "gotest.tools/v3/assert" ) -func RunCommand(args ...string) Executor { - return func(data Data, helpers Helpers) Command { +// RunCommand is the simplest way to express a test.TestableCommand for very basic cases when access to test data is not necessary +func Command(args ...string) Executor { + return func(data Data, helpers Helpers) TestableCommand { return helpers.Command(args...) } } -// WithData returns a data object with a certain key value set -func WithData(key string, value string) Data { - dat := &data{} - dat.Set(key, value) - return dat -} - -// WithConfig returns a data object with a certain config property set -func WithConfig(key ConfigKey, value ConfigValue) Data { - dat := &data{} - dat.WithConfig(key, value) - return dat -} - // Expects is provided as a simple helper covering "expectations" for simple use-cases where access to the test data is not necessary func Expects(exitCode int, errors []error, output Comparator) Manager { return func(_ Data, _ Helpers) *Expected { @@ -55,7 +43,7 @@ func Expects(exitCode int, errors []error, output Comparator) Manager { } } -// All can be used as a parameter for expected.Output and allow passing a collection of conditions to match +// All can be used as a parameter for expected.Output to group a set of comparators func All(comparators ...Comparator) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() @@ -69,7 +57,7 @@ func All(comparators ...Comparator) Comparator { func Contains(compare string) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() - assert.Assert(t, strings.Contains(stdout, compare), fmt.Sprintf("Expected output to contain: %q", compare)+info) + assert.Check(t, strings.Contains(stdout, compare), fmt.Sprintf("Output does not contain: %q", compare)+info) } } @@ -77,7 +65,7 @@ func Contains(compare string) Comparator { func DoesNotContain(compare string) Comparator { return func(stdout string, info string, t *testing.T) { t.Helper() - assert.Assert(t, !strings.Contains(stdout, compare), fmt.Sprintf("Expected output to not contain: %q", compare)+info) + assert.Check(t, !strings.Contains(stdout, compare), fmt.Sprintf("Output does contain: %q", compare)+info) } } @@ -88,3 +76,12 @@ func Equals(compare string) Comparator { assert.Equal(t, compare, stdout, info) } } + +// Provisional - expected use, but have not seen it so far +// Match is to be used for expected.Output to ensure we match a regexp +func Match(reg *regexp.Regexp) Comparator { + return func(stdout string, info string, t *testing.T) { + t.Helper() + assert.Check(t, reg.MatchString(stdout), fmt.Sprintf("Output does not match: %s", reg), info) + } +} diff --git a/pkg/testutil/test/helpers.go b/pkg/testutil/test/helpers.go index 64a734dd86c..99d769d7c3e 100644 --- a/pkg/testutil/test/helpers.go +++ b/pkg/testutil/test/helpers.go @@ -18,37 +18,66 @@ package test import "testing" +// Helpers provides a set of helpers to run commands with simple expectations, available at all stages of a test (Setup, Cleanup, etc...) type Helpers interface { + // Ensure runs a command and verifies it is succeeding Ensure(args ...string) + // Anyhow runs a command and ignores its result Anyhow(args ...string) + // Fail runs a command and verifies it failed Fail(args ...string) + // Capture runs a command, verifies it succeeded, and returns stdout Capture(args ...string) string + // Err runs a command, and returns stderr regardless of its outcome + // This is mostly useful for debugging + Err(args ...string) string - Command(args ...string) Command - CustomCommand(binary string, args ...string) Command + // Command will return a populated command from the default internal command, with the provided arguments, + // ready to be Run or further configured + Command(args ...string) TestableCommand + // Custom will return a bare command, without configuration nor defaults (still has the Env) + Custom(binary string, args ...string) TestableCommand + + // Read return the config value associated with a key + Read(key ConfigKey) ConfigValue + // Write saves a value in the config + Write(key ConfigKey, value ConfigValue) + + // T returns the current testing object + T() *testing.T } -type helpers struct { - cmd Command +// This is the implementation of Helpers + +type helpersInternal struct { + cmdInternal CustomizableCommand + + t *testing.T } -func (hel *helpers) Ensure(args ...string) { - hel.Command(args...).Run(&Expected{}) +// Ensure will run a command and make sure it is successful +func (help *helpersInternal) Ensure(args ...string) { + help.Command(args...).Run(&Expected{ + ExitCode: 0, + }) } -func (hel *helpers) Anyhow(args ...string) { - hel.Command(args...).Run(nil) +// Anyhow will run a command regardless of outcome (may or may not fail) +func (help *helpersInternal) Anyhow(args ...string) { + help.Command(args...).Run(nil) } -func (hel *helpers) Fail(args ...string) { - hel.Command(args...).Run(&Expected{ +// Fail will run a command and make sure it does fail +func (help *helpersInternal) Fail(args ...string) { + help.Command(args...).Run(&Expected{ ExitCode: 1, }) } -func (hel *helpers) Capture(args ...string) string { +// Capture will run a command, ensure it is successful and return stdout +func (help *helpersInternal) Capture(args ...string) string { var ret string - hel.Command(args...).Run(&Expected{ + help.Command(args...).Run(&Expected{ Output: func(stdout string, info string, t *testing.T) { ret = stdout }, @@ -56,16 +85,37 @@ func (hel *helpers) Capture(args ...string) string { return ret } -func (hel *helpers) Command(args ...string) Command { - cc := hel.cmd.Clone() +// Capture will run a command, ensure it is successful and return stdout +func (help *helpersInternal) Err(args ...string) string { + cmd := help.Command(args...) + cmd.Run(nil) + return cmd.Stderr() +} + +// Command will return a clone of your base command without running it +func (help *helpersInternal) Command(args ...string) TestableCommand { + cc := help.cmdInternal.Clone() cc.WithArgs(args...) return cc } -func (hel *helpers) CustomCommand(binary string, args ...string) Command { - cc := hel.cmd.Clone() - cc.Clear() +// Custom will return a command for the requested binary and args, with the environment of your test +// (eg: Env, Cwd, etc.) +func (help *helpersInternal) Custom(binary string, args ...string) TestableCommand { + cc := help.cmdInternal.clear() cc.WithBinary(binary) cc.WithArgs(args...) return cc } + +func (help *helpersInternal) Read(key ConfigKey) ConfigValue { + return help.cmdInternal.read(key) +} + +func (help *helpersInternal) Write(key ConfigKey, value ConfigValue) { + help.cmdInternal.write(key, value) +} + +func (help *helpersInternal) T() *testing.T { + return help.t +} diff --git a/pkg/testutil/test/requirement.go b/pkg/testutil/test/requirement.go index c148fef1914..d9117c21692 100644 --- a/pkg/testutil/test/requirement.go +++ b/pkg/testutil/test/requirement.go @@ -20,57 +20,49 @@ import ( "fmt" "os/exec" "runtime" - "testing" ) -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 skip && !ret { - t.Helper() - t.Skipf("Test skipped as %s", mess) - } +func Binary(name string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers) (bool, string) { + mess := fmt.Sprintf("executable %q has been found in PATH", name) + ret := true + if _, err := exec.LookPath(name); err != nil { + ret = false + mess = fmt.Sprintf("executable %q doesn't exist in PATH", name) + } - return ret, mess + return ret, mess + }, } } -func Binary(name string) Requirement { - return MakeRequirement(func(data Data, 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 { - ret = false - mess = fmt.Sprintf("executable %q doesn't exist in PATH", name) - } - - return ret, mess - }) -} - -func OS(os string) Requirement { - return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { - mess = fmt.Sprintf("current operating system is %q", runtime.GOOS) - ret = true - if runtime.GOOS != os { - ret = false - } +func OS(os string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers) (bool, string) { + mess := fmt.Sprintf("current operating system is %q", runtime.GOOS) + ret := true + if runtime.GOOS != os { + ret = false + } - return ret, mess - }) + return ret, mess + }, + } } -func Arch(arch string) Requirement { - return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { - mess = fmt.Sprintf("current architecture is %q", runtime.GOARCH) - ret = true - if runtime.GOARCH != arch { - ret = false - } +func Arch(arch string) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers) (bool, string) { + mess := fmt.Sprintf("current architecture is %q", runtime.GOARCH) + ret := true + if runtime.GOARCH != arch { + ret = false + } - return ret, mess - }) + return ret, mess + }, + } } var Amd64 = Arch("amd64") @@ -79,26 +71,44 @@ var Windows = OS("windows") var Linux = OS("linux") var Darwin = OS("darwin") -func Not(requirement Requirement) Requirement { - return MakeRequirement(func(data Data, t *testing.T) (ret bool, mess string) { - b, mess := requirement(data, false, t) +// NOTE: Not will always lose setups and cleanups... - return !b, mess - }) +func Not(requirement *Requirement) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers) (bool, string) { + ret, mess := requirement.Check(data, helpers) + return !ret, mess + }, + } } -func Require(thing ...Requirement) Requirement { - return func(data Data, skip bool, t *testing.T) (ret bool, mess string) { - for _, th := range thing { - b, m := th(data, false, t) - if !b { - if skip { - t.Helper() - t.Skipf("Test skipped as %s", m) +func Require(requirements ...*Requirement) *Requirement { + return &Requirement{ + Check: func(data Data, helpers Helpers) (bool, string) { + ret := true + var mess, subMess string + for _, requirement := range requirements { + ret, subMess = requirement.Check(data, helpers) + mess += subMess + if !ret { + return ret, mess + } + } + return ret, mess + }, + Setup: func(data Data, helpers Helpers) { + for _, requirement := range requirements { + if requirement.Setup != nil { + requirement.Setup(data, helpers) + } + } + }, + Cleanup: func(data Data, helpers Helpers) { + for _, requirement := range requirements { + if requirement.Cleanup != nil { + requirement.Cleanup(data, helpers) } - return false, "" } - } - return true, "" + }, } } diff --git a/pkg/testutil/test/test.go b/pkg/testutil/test/test.go index 858563be789..e8f3a9dfe4f 100644 --- a/pkg/testutil/test/test.go +++ b/pkg/testutil/test/test.go @@ -22,53 +22,35 @@ import ( "time" ) -// A Requirement is a function that can evaluate random requirement and possibly skip a test -// See test.MakeRequirement to make your own -type Requirement func(data Data, skip bool, t *testing.T) (bool, string) +// A Requirement offers a way to verify random conditions to decide if a test should be skipped or run. +// It can furthermore (optionally) provide custom Setup and Cleanup routines. +type Requirement struct { + // Check is expected to perform random operations and return a boolean and an explanatory message + Check Evaluator + // Setup, if provided, will be run before any test-specific Setup routine, in the order that requirements have been declared + Setup Butler + // Cleanup, if provided, will be run after any test-specific Cleanup routine, in the revers order that requirements have been declared + Cleanup Butler +} + +// An Evaluator is a function that decides whether a test should run or not +type Evaluator func(data Data, helpers Helpers) (bool, string) -// A Butler is the function signature meant to be attached to a Setup or Cleanup routine for a test.Case +// A Butler is the function signature meant to be attached to a Setup or Cleanup routine for a Case or Requirement type Butler func(data Data, helpers Helpers) -// An Executor is the function signature meant to be attached to a test.Case Command -type Executor func(data Data, helpers Helpers) Command +// An Executor is the function signature meant to be attached to the Command property of a Case +type Executor func(data Data, helpers Helpers) TestableCommand // A Manager is the function signature to be run to produce expectations to be fed to a command type Manager func(data Data, helpers Helpers) *Expected -// The Command interface represents a low-level command to execute, typically to be compared with an Expected -// A Command can be used as a Case Command obviously, but also as part of a Setup or Cleanup routine, -// and as the basis of any type of helper. -// A Command can be cloned, in which case, the subcommand inherits a copy of all of its Env and parameters. -// Typically, a Case has a base-command, from which all commands involved in the test are derived. -type Command interface { - // WithBinary specifies what binary to execute - WithBinary(binary string) Command - // WithArgs specifies the args to pass to the binary. Note that WithArgs is additive. - WithArgs(args ...string) Command - // WithEnv adds the passed map to the environment of the command to be executed - WithEnv(env map[string]string) Command - // WithWrapper allows wrapping a command with another command (for example: `time`, `unbuffer`) - WithWrapper(binary string, args ...string) Command - // WithStdin allows passing a reader to be used for stdin for the command - WithStdin(r io.Reader) Command - // Run does execute the command, and compare the output with the provided expectation. - // Passing nil for `Expected` will just run the command regardless of outcome. - // An empty `&Expected{}` is (of course) equivalent to &Expected{Exit: 0}, meaning the command is verified to be - // successful - Run(expect *Expected) - // Clone returns a copy of the command - Clone() Command - // Clear will clear binary and arguments, but retain the env, or any other custom properties - Clear() Command - // Allow starting a command in the background - Background(timeout time.Duration) Command -} - +// A Comparator is the function signature to implement for the Output property of an Expected type Comparator func(stdout string, info string, t *testing.T) // Expected expresses the expected output of a command type Expected struct { - // ExitCode to expect + // ExitCode ExitCode int // Errors contains any error that (once serialized) should be seen in stderr Errors []error @@ -76,16 +58,11 @@ type Expected struct { Output Comparator } -type ConfigKey string -type ConfigValue string - -type SystemKey string -type SystemValue string - // Data is meant to hold information about a test: // - first, any random key value data that the test implementer wants to carry / modify - this is test data -// - second, configuration specific to the binary being tested - typically defined by the specialized command being tested -// - third, immutable "system" info (unique identifier, tempdir, or other SystemKey/Value pairs) +// - second, some commonly useful immutable test properties (a way to generate unique identifiers for that test, +// temporary directory, etc.) +// Note that Data is inherited, from parent test to subtest (except for Identifier and TempDir of course) type Data interface { // Get returns the value of a certain key for custom data Get(key string) string @@ -93,27 +70,91 @@ type Data interface { Set(key string, value string) Data // Identifier returns the test identifier that can be used to name resources - Identifier() string + Identifier(suffix ...string) string // TempDir returns the test temporary directory TempDir() string - // Sink allows to define ONCE a certain system property - Sink(key SystemKey, value SystemValue) - // Surface allows retrieving a certain system property - Surface(key SystemKey) SystemValue - - // WithConfig allows setting a declared ConfigKey to a ConfigValue - WithConfig(key ConfigKey, value ConfigValue) Data - ReadConfig(key ConfigKey) ConfigValue - - // Private methods - getLabels() map[string]string - getConfig() map[ConfigKey]ConfigValue +} + +type ConfigKey string +type ConfigValue string + +// Config is meant to hold information relevant to the binary (eg: flags defining certain behaviors, etc.) +type Config interface { + // Write + Write(key ConfigKey, value ConfigValue) Config + // Read + Read(key ConfigKey) ConfigValue +} + +// The TestableCommand interface represents a low-level command to execute, typically to be compared with an Expected +// A TestableCommand can be used as a Case Command obviously, but also as part of a Setup or Cleanup routine, +// and as the basis of any type of helper. +// For more powerful usecase outside of test cases, see below CustomizableCommand +type TestableCommand interface { + // WithBinary specifies what binary to execute + WithBinary(binary string) + // WithArgs specifies the args to pass to the binary. Note that WithArgs can be used multiple times and is additive. + WithArgs(args ...string) + // WithWrapper allows wrapping a command with another command (for example: `time`, `unbuffer`) + WithWrapper(binary string, args ...string) + // WithStdin allows passing a reader to be used for stdin for the command + WithStdin(r io.Reader) + // WithCwd allows specifying the working directory for the command + WithCwd(path string) + // Clone returns a copy of the command + Clone() TestableCommand + + // Run does execute the command, and compare the output with the provided expectation. + // Passing nil for `Expected` will just run the command regardless of outcome. + // An empty `&Expected{}` is (of course) equivalent to &Expected{Exit: 0}, meaning the command is verified to be + // successful + Run(expect *Expected) + // Background allows starting a command in the background + Background(timeout time.Duration) + // Stderr allows retrieving the raw stderr output of the command + Stderr() string +} + +// ///////////////////////////////////////////// +// CustomizableCommand is an interface meant for people who want to heavily customize the base command of their test case +// It is passed along +type CustomizableCommand interface { + TestableCommand + + PrependArgs(args ...string) + // WithBlacklist allows to filter out unwanted variables from the embedding environment - default it pass any that is + // defined by WithEnv + WithBlacklist(env []string) + + // withEnv *copies* the passed map to the environment of the command to be executed + // Note that this will override any variable defined in the embedding environment + withEnv(env map[string]string) + // withTempDir specifies a temporary directory to use + withTempDir(path string) + // WithConfig allows passing custom config properties from the test to the base command + withConfig(config Config) + withT(t *testing.T) + // Clear does a clone, but will clear binary and arguments, but retain the env, or any other custom properties + // Gotcha: if GenericCommand is embedded with a custom Run and an overridden clear to return the embedding type + // the result will be the embedding command, no longer the GenericCommand + clear() TestableCommand + + // Will manipulate specific configuration option on the command + // Note that config is a copy of the test config + // Any modification done here will not be passed along to subtests, although they are shared amongst all commands of the test. + write(key ConfigKey, value ConfigValue) + read(key ConfigKey) ConfigValue +} + +type Testable interface { + CustomCommand(testCase *Case, t *testing.T) CustomizableCommand + AmbientRequirements(testCase *Case, t *testing.T) } var ( - registeredInit func(test *Case, t *testing.T) Command + registeredTestable Testable ) -func CustomCommand(custom func(test *Case, t *testing.T) Command) { - registeredInit = custom +func Customize(testable Testable) { + registeredTestable = testable } diff --git a/pkg/testutil/test/utilities.go b/pkg/testutil/test/utilities.go new file mode 100644 index 00000000000..b12715d7b82 --- /dev/null +++ b/pkg/testutil/test/utilities.go @@ -0,0 +1,42 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package test + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "os" +) + +// IsRoot returns true if we are root... simple +func IsRoot() bool { + return os.Geteuid() == 0 +} + +// RandomStringBase64 generates a base64 encoded random string +func RandomStringBase64(n int) string { + b := make([]byte, n) + l, err := rand.Read(b) + if err != nil { + panic(err) + } + if l != n { + panic(fmt.Errorf("expected %d bytes, got %d bytes", n, l)) + } + return base64.URLEncoding.EncodeToString(b) +} diff --git a/pkg/testutil/testregistry/certsd_linux.go b/pkg/testutil/testregistry/certsd_linux.go index 955bc4ba12f..2a9587e08c4 100644 --- a/pkg/testutil/testregistry/certsd_linux.go +++ b/pkg/testutil/testregistry/certsd_linux.go @@ -17,31 +17,11 @@ package testregistry import ( - "fmt" - "net" - "os" - "path/filepath" - "strconv" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/hoststoml" ) func generateCertsd(dir string, certPath string, hostIP string, port int) error { - joined := hostIP - if port != 0 { - joined = net.JoinHostPort(hostIP, strconv.Itoa(port)) - } - - hostsSubDir := filepath.Join(dir, joined) - err := os.MkdirAll(hostsSubDir, 0700) - if err != nil { - return err - } - - hostsTOMLPath := filepath.Join(hostsSubDir, "hosts.toml") - // See https://github.com/containerd/containerd/blob/main/docs/hosts.md - hostsTOML := fmt.Sprintf(` -server = "https://%s" -[host."https://%s"] - ca = %q - `, joined, joined, certPath) - return os.WriteFile(hostsTOMLPath, []byte(hostsTOML), 0700) + return (&hoststoml.HostsToml{ + CA: certPath, + }).Save(dir, hostIP, port) } diff --git a/pkg/testutil/testregistry/testregistry_linux.go b/pkg/testutil/testregistry/testregistry_linux.go index 61342b1a28f..72701d5998d 100644 --- a/pkg/testutil/testregistry/testregistry_linux.go +++ b/pkg/testutil/testregistry/testregistry_linux.go @@ -29,6 +29,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/platform" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" "github.com/containerd/nerdctl/v2/pkg/testutil/testca" @@ -56,17 +57,17 @@ type TokenAuthServer struct { } func EnsureImages(base *testutil.Base) { - registryImage := testutil.RegistryImageStable + registryImage := platform.RegistryImageStable up := os.Getenv("DISTRIBUTION_VERSION") if up != "" { if up[0:1] != "v" { up = "v" + up } - registryImage = testutil.RegistryImageNext + up + registryImage = platform.RegistryImageNext + up } - base.Cmd("pull", registryImage).AssertOK() - base.Cmd("pull", testutil.DockerAuthImage).AssertOK() - base.Cmd("pull", testutil.KuboImage).AssertOK() + base.Cmd("pull", "--quiet", registryImage).AssertOK() + base.Cmd("pull", "--quiet", platform.DockerAuthImage).AssertOK() + base.Cmd("pull", "--quiet", platform.KuboImage).AssertOK() } func NewAuthServer(base *testutil.Base, ca *testca.CA, port int, user, pass string, tls bool) *TokenAuthServer { diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index a7237ad6c82..4287db9b305 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -549,6 +549,8 @@ var ( flagTestKillDaemon bool flagTestIPv6 bool flagTestKube bool + flagVerbose bool + flagTestFlaky bool ) var ( @@ -560,6 +562,10 @@ func M(m *testing.M) { flag.BoolVar(&flagTestKillDaemon, "test.allow-kill-daemon", false, "enable tests that kill the daemon") flag.BoolVar(&flagTestIPv6, "test.only-ipv6", false, "enable tests on IPv6") flag.BoolVar(&flagTestKube, "test.only-kubernetes", false, "enable tests on Kubernetes") + flag.BoolVar(&flagTestFlaky, "test.only-flaky", false, "enable testing of flaky tests only") + if flag.Lookup("test.v") != nil { + flagVerbose = true + } flag.Parse() os.Exit(func() int { @@ -625,6 +631,10 @@ func GetEnableKubernetes() bool { return flagTestKube } +func GetFlakyEnvironment() bool { + return flagTestFlaky +} + func GetDaemonIsKillable() bool { return flagTestKillDaemon } @@ -633,6 +643,8 @@ func IsDocker() bool { return GetTarget() == Docker } +func GetVerbose() bool { return flagVerbose } + func DockerIncompatible(t testing.TB) { if IsDocker() { t.Skip("test is incompatible with Docker")