From f5f3a8fe8426a732833d8d3636cdba70f4dd3755 Mon Sep 17 00:00:00 2001 From: apostasie Date: Wed, 25 Sep 2024 16:11:58 -0700 Subject: [PATCH] Migrate image tests to new tooling Signed-off-by: apostasie --- cmd/nerdctl/image/image_convert_linux_test.go | 149 ++++--- cmd/nerdctl/image/image_convert_test.go | 70 ---- cmd/nerdctl/image/image_encrypt_linux_test.go | 81 ++-- cmd/nerdctl/image/image_history_test.go | 189 ++++----- cmd/nerdctl/image/image_inspect_test.go | 301 ++++++++------ cmd/nerdctl/image/image_list_test.go | 3 +- cmd/nerdctl/image/image_load_linux_test.go | 70 ---- cmd/nerdctl/image/image_load_test.go | 81 ++++ cmd/nerdctl/image/image_prune_test.go | 276 ++++++++----- cmd/nerdctl/image/image_pull_linux_test.go | 317 +++++++++------ cmd/nerdctl/image/image_push_linux_test.go | 383 ++++++++++-------- cmd/nerdctl/image/image_remove_linux_test.go | 107 ----- cmd/nerdctl/image/image_remove_test.go | 312 ++++++++++++++ cmd/nerdctl/image/image_save_linux_test.go | 50 --- cmd/nerdctl/image/image_save_test.go | 193 +++++++-- 15 files changed, 1553 insertions(+), 1029 deletions(-) delete mode 100644 cmd/nerdctl/image/image_convert_test.go delete mode 100644 cmd/nerdctl/image/image_load_linux_test.go create mode 100644 cmd/nerdctl/image/image_load_test.go delete mode 100644 cmd/nerdctl/image/image_remove_linux_test.go create mode 100644 cmd/nerdctl/image/image_remove_test.go delete mode 100644 cmd/nerdctl/image/image_save_linux_test.go diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index ae90cca5af9..dcb6b83418c 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -20,63 +20,120 @@ 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) - - base := testutil.NewBase(t) - t.Parallel() +func TestImageConvert(t *testing.T) { + nerdtest.Setup() - 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 := &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()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier()) + }, + 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()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--nydus", testutil.CommonImage, data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstd", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3", testutil.CommonImage, data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "zstdchunked", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3", testutil.CommonImage, data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + } - // use `nydusify` check whether the convertd nydus image is valid + testCase.Run(t) - // skip if rootless - if rootlessutil.IsRootless() { - t.Skip("Nydusify check is not supported rootless mode.") - } +} - // skip if nydusify and nydusd are not installed - testutil.RequireExecutable(t, "nydusify") - testutil.RequireExecutable(t, "nydusd") +func TestImageConvertNydusVerify(t *testing.T) { + nerdtest.Setup() - // 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), + test.Not(nerdtest.Rootless), ), - 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) + helpers.Ensure("image", "convert", "--nydus", "--oci", + testutil.CommonImage, data.Identifier()) + data.Set("remoteImage", fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port)) + helpers.Ensure("tag", data.Identifier(), data.Get("remoteImage")) + helpers.Ensure("push", data.Get("remoteImage")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("remoteImage")) + helpers.Anyhow("rmi", data.Identifier()) + if registry != nil { + registry.Cleanup(nil) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("nydusify", + "check", + "--source", + testutil.CommonImage, + "--target", + data.Get("remoteImage"), + "--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..3513e14dc86 100644 --- a/cmd/nerdctl/image/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image/image_encrypt_linux_test.go @@ -18,39 +18,64 @@ 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 + + testCase := &test.Case{ + Description: "TestImageEncryptJWE", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + // This test needs to rmi the common image + nerdtest.Private, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + keyPair.Cleanup() + helpers.Anyhow("rmi", "-f", data.Get("encryptImageRef")) + } + helpers.Anyhow("rmi", "-f", data.Identifier()) + }, + 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("encryptImageRef", encryptImageRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + helpers.Fail("pull", data.Get("encryptImageRef")) + helpers.Ensure("pull", "--unpack=false", data.Get("encryptImageRef")) + helpers.Fail("image", "decrypt", "--key="+keyPair.Pub, data.Get("encryptImageRef"), data.Identifier()) // decryption needs prv key, not pub key + return helpers.Command("image", "decrypt", "--key="+keyPair.Prv, data.Get("encryptImageRef"), data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index 21bef4f5692..5241e08f66c 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -18,9 +18,8 @@ package image import ( "encoding/json" - "fmt" + "errors" "io" - "runtime" "strings" "testing" "time" @@ -28,6 +27,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) type historyObj struct { @@ -39,53 +40,20 @@ type historyObj struct { Comment string } -func imageHistoryJSONHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) []historyObj { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, "--format", "json") - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - fmt.Println(cmdResult.Stderr()) - - dec := json.NewDecoder(strings.NewReader(cmdResult.Stdout())) +func decode(stdout string) ([]historyObj, error) { + dec := json.NewDecoder(strings.NewReader(stdout)) object := []historyObj{} for { var v historyObj if err := dec.Decode(&v); err == io.EOF { break } else if err != nil { - base.T.Fatal(err) + return nil, errors.New("failed to decode history object") } object = append(object, v) } - return object -} - -func imageHistoryRawHelper(base *testutil.Base, reference string, noTrunc bool, quiet bool, human bool) string { - cmd := []string{"image", "history"} - if noTrunc { - cmd = append(cmd, "--no-trunc") - } - if quiet { - cmd = append(cmd, "--quiet") - } - cmd = append(cmd, fmt.Sprintf("--human=%t", human)) - cmd = append(cmd, reference) - - cmdResult := base.Cmd(cmd...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0, cmdResult.Stdout()) - - return cmdResult.Stdout() + return object, nil } func TestImageHistory(t *testing.T) { @@ -97,69 +65,88 @@ func TestImageHistory(t *testing.T) { // possibly one is unpacked on the filessystem while the other is the tar file size? // - we do not truncate ids when --quiet has been provided // this is a conscious decision here - truncating with --quiet does not make much sense - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - - // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? - // Disabling for now - if runtime.GOOS == "windows" { - t.Skip("Windows is not supported for this test right now") - } - // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms - if runtime.GOARCH != "arm64" { - t.Skip("Windows is not supported for this test right now") + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImageHistory", + Require: test.Require( + test.Not(nerdtest.Docker), + // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image? + test.Not(test.Windows), + // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms + test.Arm64, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--platform", "linux/arm64", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "trunc, no quiet, human", + Command: test.RunCommand("image", "history", "--human=true", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, len(history), 2, info) + assert.Equal(t, history[0].Size, "0B", info) + // FIXME: how is this going to age? + assert.Equal(t, history[0].CreatedSince, "3 years ago", info) + assert.Equal(t, history[0].Snapshot, "", info) + assert.Equal(t, history[0].Comment, "", info) + + localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") + localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") + compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) + compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) + assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String(), info) + assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", info) + assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String(), info) + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", info) + + assert.Equal(t, history[1].Size, "5.947MB", info) + assert.Equal(t, history[1].CreatedSince, "3 years ago", info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info) + assert.Equal(t, history[1].Comment, "", info) + }), + }, + { + Description: "no human - dates and sizes and not prettyfied", + Command: test.RunCommand("image", "history", "--human=false", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[0].Size, "0", info) + assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt, info) + assert.Equal(t, history[1].Size, "5947392", info) + assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt, info) + }), + }, + { + Description: "no trunc - do not truncate sha or cmd", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + history, err := decode(stdout) + assert.NilError(t, err, info) + assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") + assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") + }), + }, + { + Description: "Quiet has no effect with format, so, go no-json, no-trunc", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + { + Description: "With quiet, trunc has no effect", + Command: test.RunCommand("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage), + Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) { + assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + }), + }, + }, } - base.Cmd("pull", "--platform", "linux/arm64", testutil.CommonImage).AssertOK() - - localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00") - localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00") - - // Human, no quiet, truncate - history := imageHistoryJSONHelper(base, testutil.CommonImage, false, false, true) - compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt) - compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt) - - // Two layers - assert.Equal(base.T, len(history), 2) - // First layer is a comment - zero size, no snap, - assert.Equal(base.T, history[0].Size, "0B") - assert.Equal(base.T, history[0].CreatedSince, "3 years ago") - assert.Equal(base.T, history[0].Snapshot, "") - assert.Equal(base.T, history[0].Comment, "") - - assert.Equal(base.T, compTime1.UTC().String(), localTimeL1.UTC().String()) - assert.Equal(base.T, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]") - - assert.Equal(base.T, compTime2.UTC().String(), localTimeL2.UTC().String()) - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…") - - assert.Equal(base.T, history[1].Size, "5.947MB") - assert.Equal(base.T, history[1].CreatedSince, "3 years ago") - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…") - assert.Equal(base.T, history[1].Comment, "") - - // No human - dates and sizes and not prettyfied - history = imageHistoryJSONHelper(base, testutil.CommonImage, false, false, false) - - assert.Equal(base.T, history[0].Size, "0") - assert.Equal(base.T, history[0].CreatedSince, history[0].CreatedAt) - - assert.Equal(base.T, history[1].Size, "5947392") - assert.Equal(base.T, history[1].CreatedSince, history[1].CreatedAt) - - // No trunc - do not truncate sha or cmd - history = imageHistoryJSONHelper(base, testutil.CommonImage, true, false, true) - assert.Equal(base.T, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a") - assert.Equal(base.T, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ") - - // Quiet has no effect with format, so, go no-json, no-trunc - rawHistory := imageHistoryRawHelper(base, testutil.CommonImage, true, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") - - // With quiet, trunc has no effect - rawHistory = imageHistoryRawHelper(base, testutil.CommonImage, false, true, true) - assert.Equal(base.T, rawHistory, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n") + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 14fcb90caf8..59fbccd24c5 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -18,7 +18,6 @@ package image import ( "encoding/json" - "runtime" "strings" "testing" @@ -26,69 +25,50 @@ import ( "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestImageInspectContainsSomeStuff(t *testing.T) { - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - - assert.Assert(base.T, len(inspect.RootFS.Layers) > 0) - assert.Assert(base.T, inspect.RootFS.Type != "") - assert.Assert(base.T, inspect.Architecture != "") - assert.Assert(base.T, inspect.Size > 0) -} - -func TestImageInspectWithFormat(t *testing.T) { - base := testutil.NewBase(t) - - base.Cmd("pull", testutil.CommonImage).AssertOK() - - // test RawFormat support - base.Cmd("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}").AssertOK() - - // test typedFormat support - base.Cmd("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}").AssertOK() -} - -func inspectImageHelper(base *testutil.Base, identifier ...string) []dockercompat.Image { - args := append([]string{"image", "inspect"}, identifier...) - cmdResult := base.Cmd(args...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0) - var dc []dockercompat.Image - if err := json.Unmarshal([]byte(cmdResult.Stdout()), &dc); err != nil { - base.T.Fatal(err) +func TestImageInspectSimpleCases(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImageInspect", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + }, + SubTests: []*test.Case{ + { + Description: "Contains some stuff", + Command: test.RunCommand("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) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + assert.Assert(t, len(dc[0].RootFS.Layers) > 0, info) + assert.Assert(t, dc[0].Architecture != "", info) + assert.Assert(t, dc[0].Size > 0, info) + }), + }, + { + Description: "RawFormat support (.Id)", + Command: test.RunCommand("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}}"), + Expected: test.Expects(0, nil, nil), + }, + }, } - return dc + + testCase.Run(t) } func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { - testutil.DockerIncompatible(t) - - if runtime.GOOS == "windows" { - t.Skip("Windows is not supported for this test right now") - } - - base := testutil.NewBase(t) - - // Overall, we need a clean slate before doing these lookups. - // More specifically, because we trigger https://github.com/containerd/nerdctl/issues/3016 - // we cannot do selective rmi, so, just nuke everything - ids := base.Cmd("image", "list", "-q").Out() - allIDs := strings.Split(ids, "\n") - for _, id := range allIDs { - id = strings.TrimSpace(id) - if id != "" { - base.Cmd("rmi", "-f", id).Run() - } - } - - base.Cmd("pull", "alpine", "--platform", "linux/amd64").AssertOK() - base.Cmd("pull", "busybox", "--platform", "linux/amd64").AssertOK() - base.Cmd("pull", "busybox:stable", "--platform", "linux/amd64").AssertOK() - base.Cmd("pull", "registry-1.docker.io/library/busybox", "--platform", "linux/amd64").AssertOK() - base.Cmd("pull", "registry-1.docker.io/library/busybox:stable", "--platform", "linux/amd64").AssertOK() + nerdtest.Setup() tags := []string{ "", @@ -102,76 +82,141 @@ func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) { "registry-1.docker.io/library/busybox", } - // Build reference values for comparison - reference := inspectImageHelper(base, "busybox") - assert.Equal(base.T, 1, len(reference)) - // Extract image sha - sha := strings.TrimPrefix(reference[0].RepoDigests[0], "busybox@sha256:") - - differentReference := inspectImageHelper(base, "alpine") - assert.Equal(base.T, 1, len(differentReference)) - - // Testing all name and tags variants - for _, name := range names { - for _, tag := range tags { - t.Logf("Testing %s", name+tag) - result := inspectImageHelper(base, name+tag) - assert.Equal(base.T, 1, len(result)) - assert.Equal(base.T, reference[0].ID, result[0].ID) - } - } - - // Testing all name and tags variants, with a digest - for _, name := range names { - for _, tag := range tags { - t.Logf("Testing %s", name+tag+"@"+sha) - result := inspectImageHelper(base, name+tag+"@sha256:"+sha) - assert.Equal(base.T, 1, len(result)) - assert.Equal(base.T, reference[0].ID, result[0].ID) - } - } - - // Testing repo digest and short digest with or without prefix - for _, id := range []string{"sha256:" + sha, sha, sha[0:8], "sha256:" + sha[0:8]} { - t.Logf("Testing %s", id) - result := inspectImageHelper(base, id) - assert.Equal(base.T, 1, len(result)) - assert.Equal(base.T, reference[0].ID, result[0].ID) - } - - // Demonstrate image name precedence over digest lookup - // Using the shortened sha should no longer get busybox, but rather the newly tagged Alpine - t.Logf("Testing (alpine tagged) %s", sha[0:8]) - // Tag a different image with the short id - base.Cmd("tag", "alpine", sha[0:8]).AssertOK() - result := inspectImageHelper(base, sha[0:8]) - assert.Equal(base.T, 1, len(result)) - assert.Equal(base.T, differentReference[0].ID, result[0].ID) - - // Prove that wrong references with an existing digest do not get retrieved when asking by digest - for _, id := range []string{"doesnotexist", "doesnotexist:either", "busybox:bogustag"} { - t.Logf("Testing %s", id+"@"+sha) - args := append([]string{"image", "inspect"}, id+"@"+sha) - cmdResult := base.Cmd(args...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0) - assert.Equal(base.T, cmdResult.Stdout(), "") + testCase := &test.Case{ + Description: "TestImageInspectDifferentValidReferencesForTheSameImage", + Require: test.Require( + test.Not(nerdtest.Docker), + test.Not(test.Windows), + // We need a clean slate + nerdtest.Private, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "alpine", "--platform", "linux/amd64") + helpers.Ensure("pull", "busybox", "--platform", "linux/amd64") + helpers.Ensure("pull", "busybox:stable", "--platform", "linux/amd64") + helpers.Ensure("pull", "registry-1.docker.io/library/busybox", "--platform", "linux/amd64") + helpers.Ensure("pull", "registry-1.docker.io/library/busybox:stable", "--platform", "linux/amd64") + }, + SubTests: []*test.Case{ + { + Description: "name and tags +/- sha combinations", + Command: test.RunCommand("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) { + var dc []dockercompat.Image + 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) + reference := dc[0].ID + sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") + + for _, name := range names { + for _, tag := range tags { + it := nerdtest.InspectImage(helpers, name+tag) + assert.Equal(t, it.ID, reference) + it = nerdtest.InspectImage(helpers, name+tag+"@sha256:"+sha) + assert.Equal(t, it.ID, reference) + } + } + }, + } + }, + }, + { + Description: "by digest, short or long, with or without prefix", + Command: test.RunCommand("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) { + var dc []dockercompat.Image + 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) + reference := dc[0].ID + sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") + + for _, id := range []string{"sha256:" + sha, sha, sha[0:8], "sha256:" + sha[0:8]} { + it := nerdtest.InspectImage(helpers, id) + assert.Equal(t, it.ID, reference) + } + + // Now, tag alpine with a short id + // Build reference values for comparison + alpine := nerdtest.InspectImage(helpers, "alpine") + + // Demonstrate image name precedence over digest lookup + // Using the shortened sha should no longer get busybox, but rather the newly tagged Alpine + helpers.Ensure("tag", "alpine", sha[0:8]) + it := nerdtest.InspectImage(helpers, sha[0:8]) + assert.Equal(t, it.ID, alpine.ID) + }, + } + }, + }, + { + Description: "prove that wrong references with correct digest do not get resolved", + Command: test.RunCommand("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) { + var dc []dockercompat.Image + 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) + sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:") + + for _, id := range []string{"doesnotexist", "doesnotexist:either", "busybox:bogustag"} { + cmd := helpers.Command("image", "inspect", id+"@sha256:"+sha) + cmd.Run(&test.Expected{ + Output: test.Equals(""), + }) + } + }, + } + }, + }, + { + Description: "prove that invalid reference return no result without crashing", + Command: test.RunCommand("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) { + var dc []dockercompat.Image + 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) + + for _, id := range []string{"∞∞∞∞∞∞∞∞∞∞", "busybox:∞∞∞∞∞∞∞∞∞∞"} { + cmd := helpers.Command("image", "inspect", id) + cmd.Run(&test.Expected{ + Output: test.Equals(""), + }) + } + }, + } + }, + }, + { + Description: "retrieving multiple entries at once", + Command: test.RunCommand("image", "inspect", "busybox", "busybox", "busybox:stable"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var dc []dockercompat.Image + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 3, len(dc), "Unexpectedly did not get 3 results\n"+info) + reference := nerdtest.InspectImage(helpers, "busybox") + assert.Equal(t, dc[0].ID, reference.ID) + assert.Equal(t, dc[1].ID, reference.ID) + assert.Equal(t, dc[2].ID, reference.ID) + }, + } + }, + }, + }, } - // Prove that invalid reference return no result without crashing - for _, id := range []string{"∞∞∞∞∞∞∞∞∞∞", "busybox:∞∞∞∞∞∞∞∞∞∞"} { - t.Logf("Testing %s", id) - args := append([]string{"image", "inspect"}, id) - cmdResult := base.Cmd(args...).Run() - assert.Equal(base.T, cmdResult.ExitCode, 0) - assert.Equal(base.T, cmdResult.Stdout(), "") - } - - // Retrieving multiple entries at once - t.Logf("Testing %s", "busybox busybox busybox:stable") - result = inspectImageHelper(base, "busybox", "busybox", "busybox:stable") - assert.Equal(base.T, 3, len(result)) - assert.Equal(base.T, reference[0].ID, result[0].ID) - assert.Equal(base.T, reference[0].ID, result[1].ID) - assert.Equal(base.T, reference[0].ID, result[2].ID) - + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 72e902fa62b..93cd9a7965a 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -125,7 +125,8 @@ func TestImagesFilterDangling(t *testing.T) { testutil.RequiresBuild(t) testutil.RegisterBuildCacheCleanup(t) base := testutil.NewBase(t) - base.Cmd("images", "prune", "--all").AssertOK() + base.Cmd("container", "prune", "-f").AssertOK() + base.Cmd("image", "prune", "--all", "-f").AssertOK() dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] diff --git a/cmd/nerdctl/image/image_load_linux_test.go b/cmd/nerdctl/image/image_load_linux_test.go deleted file mode 100644 index 4d7b0f83dce..00000000000 --- a/cmd/nerdctl/image/image_load_linux_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestLoadStdinFromPipe(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - img := testutil.Identifier(t) - tmp := t.TempDir() - output := filepath.Join(tmp, "output") - - setup := func() { - base.Cmd("pull", testutil.CommonImage).AssertOK() - base.Cmd("tag", testutil.CommonImage, img).AssertOK() - base.Cmd("save", img, "-o", filepath.Join(tmp, "common.tar")).AssertOK() - base.Cmd("rmi", "-f", img).AssertOK() - } - - tearDown := func() { - base.Cmd("rmi", "-f", img).AssertOK() - } - - t.Cleanup(tearDown) - tearDown() - - setup() - - loadCmd := strings.Join(base.Cmd("load").Command, " ") - combined, err := exec.Command("sh", "-euxc", fmt.Sprintf("`cat %s/common.tar | %s > %s`", tmp, loadCmd, output)).CombinedOutput() - assert.NilError(t, err, "failed with error %s and combined output is %s", err, string(combined)) - - fb, err := os.ReadFile(output) - assert.NilError(t, err) - - assert.Assert(t, strings.Contains(string(fb), fmt.Sprintf("Loaded image: %s:latest", img))) - base.Cmd("images").AssertOutContains(img) -} - -func TestLoadStdinEmpty(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Cmd("load").AssertFail() -} diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go new file mode 100644 index 00000000000..5619d829018 --- /dev/null +++ b/cmd/nerdctl/image/image_load_test.go @@ -0,0 +1,81 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestLoadStdinFromPipe(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinFromPipe", + Require: test.Linux, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + helpers.Ensure("save", data.Identifier(), "-o", filepath.Join(data.TempDir(), "common.tar")) + helpers.Ensure("rmi", "-f", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + cmd := helpers.Command("load") + reader, err := os.Open(filepath.Join(data.TempDir(), "common.tar")) + assert.NilError(t, err, "failed to open common.tar") + cmd.WithStdin(reader) + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + test.Contains(fmt.Sprintf("Loaded image: %s:latest", data.Identifier())), + func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("images"), data.Identifier())) + }, + ), + } + }, + } + + testCase.Run(t) +} + +func TestLoadStdinEmpty(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestLoadStdinEmpty", + Require: test.Linux, + Command: test.RunCommand("load"), + Expected: test.Expects(1, nil, nil), + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index 94ef0c625f2..d1fd8d8574e 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -18,117 +18,191 @@ 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 + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagePrune", + // Cannot use a custom namespace with buildkitd right now, so, no parallel it is + NoParallel: true, + Require: nerdtest.Build, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + // We need to delete everything here for prune to make any sense + base := testutil.NewBase(t) + testhelpers.RmiAll(base) + }, + SubTests: []*test.Case{ + { + Description: "without all", + NoParallel: true, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + 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) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + }, + ), + } + }, + }, + { + Description: "with all", + // 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("builder", "prune", "--all", "--force") + helpers.Anyhow("rmi", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s + CMD ["echo", "nerdctl-test-image-prune"] + `, testutil.CommonImage) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", buildCtx) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, ""), "Missing ") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + helpers.Ensure("run", "--name", data.Identifier(), data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + assert.Assert(t, !strings.Contains(imgList, ""), imgList) + helpers.Ensure("rm", "-f", data.Identifier()) + removed := helpers.Capture("image", "prune", "--force", "--all") + assert.Assert(t, strings.Contains(removed, data.Identifier()), info) + imgList = helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with filter label", + // 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("builder", "prune", "--all", "--force") + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=baz").AssertOK() - base.Cmd("images", "--all").AssertOutContains(imageName) - - base.Cmd("image", "prune", "--force", "--all", "--filter", "label=foo=bar").AssertOK() - base.Cmd("images", "--all").AssertOutNotContains(imageName) -} - -func TestImagePruneFilterUntil(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - base := testutil.NewBase(t) - // For deterministically testing the filter, set the image's created timestamp to 2 hours in the past. - base.Env = append(base.Env, fmt.Sprintf("SOURCE_DATE_EPOCH=%d", time.Now().Add(-2*time.Hour).Unix())) - - imageName := testutil.Identifier(t) - teardown := func() { - // Image should have been pruned; but cleanup on failure. - base.Cmd("rmi", "--force", imageName).Run() + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", data.Identifier(), buildCtx) + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all", "--filter", "label=foo=baz"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + imgList := helpers.Capture("images") + assert.Assert(t, strings.Contains(imgList, data.Identifier()), info) + }, + func(stdout string, info string, t *testing.T) { + prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar") + assert.Assert(t, strings.Contains(prune, data.Identifier()), info) + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info) + }, + ), + } + }, + }, + { + Description: "with until", + // 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("builder", "prune", "--all", "--force") + helpers.Anyhow("rmi", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +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()) + }, + Command: test.RunCommand("image", "prune", "--force", "--all", "--filter", "until=12h"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: test.All( + 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) { + // Pause to ensure enough time has passed for the image to be cleaned on next prune. + time.Sleep(1 * time.Second) + prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "until=10ms") + assert.Assert(t, strings.Contains(prune, data.Identifier()), info) + imgList := helpers.Capture("images") + assert.Assert(t, !strings.Contains(imgList, data.Identifier()), 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..6cd74591ade 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -18,152 +18,219 @@ 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() + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + helpers.Anyhow("rmi", "-f", testImageRef) + } + }, + SubTests: []*test.Case{ + { + Description: "Pull with the correct key", + Command: func(data test.Data, helpers test.Helpers) test.Command { + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, testImageRef+":one") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Pull with unrelated key", + Env: map[string]string{ + "COSIGN_PASSWORD": "2", + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + newKeyPair := testhelpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") + testImageRef := fmt.Sprintf("%s/%s", "127.0.0.1", data.Identifier()) + return helpers.Command("pull", "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey, testImageRef+":two") + }, + Expected: test.Expects(1, nil, nil), + }, + }, + } - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() + testCase.Run(t) } func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - reg := testregistry.NewWithNoAuth(base, 80, false) - defer reg.Cleanup(nil) - testImageRef := fmt.Sprintf("%s/%s:%s", - reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - t.Logf("testImageRef=%q", testImageRef) - dockerfile := fmt.Sprintf(`FROM %s -CMD ["echo", "nerdctl-build-test-string"] - `, testutil.CommonImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("--insecure-registry", "push", testImageRef).AssertOK() - base.Cmd("--insecure-registry", "pull", testImageRef).AssertOK() -} - -func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - base.Env = append(base.Env, "COSIGN_PASSWORD=1") - keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1") - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - localhostIP := "127.0.0.1" - t.Logf("localhost IP=%q", localhostIP) - testImageRef := fmt.Sprintf("%s:%d/%s", - localhostIP, reg.Port, tID) - t.Logf("testImageRef=%q", testImageRef) - - dockerfile := fmt.Sprintf(`FROM %s + nerdtest.Setup() + + var registry *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "TestImagePullPlainHttpWithDefaultPort", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Build, + ), + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 80, false) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK() - base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK() - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.PublicKey).AssertOK() - - base.Env = append(base.Env, "COSIGN_PASSWORD=2") - newKeyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2") - base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey).AssertFail() -} - -func TestPullSoci(t *testing.T) { - testutil.DockerIncompatible(t) - tests := []struct { - name string - sociIndexDigest string - image string - remoteSnapshotsExpectedCount int - }{ - { - name: "Run without specifying SOCI index", - sociIndexDigest: "", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + helpers.Ensure("build", "-t", testImageRef, buildCtx) + helpers.Ensure("--insecure-registry", "push", testImageRef) + helpers.Ensure("rmi", "-f", testImageRef) }, - { - name: "Run with bad SOCI index", - sociIndexDigest: "sha256:thisisabadindex0000000000000000000000000000000000000000000000000", - image: testutil.FfmpegSociImage, - remoteSnapshotsExpectedCount: 11, + Command: func(data test.Data, helpers test.Helpers) test.Command { + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + return helpers.Command("--insecure-registry", "pull", testImageRef) }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - helpers.RequiresSoci(base) - - //counting initial snapshot mounts - initialMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) + Expected: test.Expects(0, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("builder", "prune", "--all", "--force") + if registry != nil { + registry.Cleanup(nil) + testImageRef := fmt.Sprintf("%s/%s:%s", + registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + helpers.Anyhow("rmi", "-f", testImageRef) } + }, + } - remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge") - - pullOutput := base.Cmd("--snapshotter=soci", "pull", tt.image).Out() - base.T.Logf("pull output: %s", pullOutput) - - actualMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) - } - remoteSnapshotsActualCount := strings.Count(string(actualMounts), "fuse.rawBridge") - base.T.Logf("number of actual mounts: %v", remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - - rmiOutput := base.Cmd("rmi", testutil.FfmpegSociImage).Out() - base.T.Logf("rmi output: %s", rmiOutput) - - base.T.Logf("number of expected mounts: %v", tt.remoteSnapshotsExpectedCount) + testCase.Run(t) +} - if tt.remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) { - t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d", - tt.remoteSnapshotsExpectedCount, remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - } - }) +func TestImagePullSoci(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "TestImagePullSoci", + Require: test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Soci, + ), + + // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount` + // They also feel prone to raciness... + SubTests: []*test.Case{ + { + Description: "Run without specifying SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", ""), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.CustomCommand("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), + info) + }, + } + }, + }, + { + Description: "Run with bad SOCI index", + NoParallel: true, + Data: test. + WithData("remoteSnapshotsExpectedCount", "11"). + Set("sociIndexDigest", "sha256:thisisabadindex0000000000000000000000000000000000000000000000000"), + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.CustomCommand("mount") + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", testutil.FfmpegSociImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.CustomCommand("mount") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount")) + remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") + assert.Equal(t, + data.Get("remoteSnapshotsExpectedCount"), + 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..e8fd15617bc 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,229 @@ 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) + registryNoAuthHTTPDefault.Cleanup(nil) + registryTokenAuthHTTPSRandom.Cleanup(nil) + } + }, + + SubTests: []*test.Case{ + { + Description: "plain http", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", data.Get("testImageRef")) + }, + Expected: test.Expects(1, []error{errors.New("server gave HTTP response to HTTPS client")}, nil), + }, + { + Description: "plain http with insecure", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "plain http with localhost", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "plain http with insecure, default port", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s/%s:%s", + registryNoAuthHTTPDefault.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with insecure, with login", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + helpers.Ensure("--insecure-registry", "login", "-u", "admin", "-p", "badmin", + fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) + + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with hosts dir, with login", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.CommonImage, testImageRef) + helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin", + fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port)) + + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "non distributable artifacts", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NonDistBlobImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) + resp, err := http.Get(blobURL) + assert.Assert(t, err, "error making http request") + if resp.Body != nil { + resp.Body.Close() + } + assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available") + }, + } + }, + }, + { + Description: "non distributable artifacts (with)", + Require: test.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NonDistBlobImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Get("testImageRef")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest) + resp, err := http.Get(blobURL) + assert.Assert(t, err, "error making http request") + if resp.Body != nil { + resp.Body.Close() + } + assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") + }, + } + }, + }, + { + Description: "soci", + Require: test.Require( + nerdtest.Soci, + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.UbuntuImage) + testImageRef := fmt.Sprintf("%s:%d/%s:%s", + registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.UbuntuImage, ":")[1]) + data.Set("testImageRef", testImageRef) + helpers.Ensure("tag", testutil.UbuntuImage, testImageRef) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Get("testImageRef")) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Get("testImageRef")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, } - assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available") -} - -func TestPushSoci(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresSoci(base) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - base.Cmd("pull", testutil.UbuntuImage).AssertOK() - testImageRef := fmt.Sprintf("%s:%d/%s:%s", - reg.IP.String(), reg.Port, testutil.Identifier(t), strings.Split(testutil.UbuntuImage, ":")[1]) - t.Logf("testImageRef=%q", testImageRef) - base.Cmd("tag", testutil.UbuntuImage, testImageRef).AssertOK() - - base.Cmd("--snapshotter=soci", "--insecure-registry", "push", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", testImageRef).AssertOK() + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_remove_linux_test.go b/cmd/nerdctl/image/image_remove_linux_test.go deleted file mode 100644 index 5752aa04aa4..00000000000 --- a/cmd/nerdctl/image/image_remove_linux_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "testing" - - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestRemoveImage(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - base.Cmd("image", "prune", "--force", "--all").AssertOK() - - // ignore error - base.Cmd("rmi", "-f", tID).AssertOK() - - base.Cmd("run", "--name", tID, testutil.CommonImage).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - defer base.Cmd("rmi", "-f", testutil.CommonImage).Run() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemoveRunningImage(t *testing.T) { - // If an image is associated with a running/paused containers, `docker rmi -f imageName` - // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. - // In both cases, `nerdctl rmi -f` will fail. - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail() - base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - - base.Cmd("kill", tID).AssertOK() - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemovePausedImage(t *testing.T) { - // If an image is associated with a running/paused containers, `docker rmi -f imageName` - // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. - // In both cases, `nerdctl rmi -f` will fail. - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") - } - tID := testutil.Identifier(t) - - base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK() - base.Cmd("pause", tID).AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail() - base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage)) - - base.Cmd("kill", tID).AssertOK() - base.Cmd("rmi", testutil.CommonImage).AssertFail() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage)) -} - -func TestRemoveImageWithCreatedContainer(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("pull", testutil.AlpineImage).AssertOK() - base.Cmd("pull", testutil.NginxAlpineImage).AssertOK() - - base.Cmd("create", "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK() - defer base.Cmd("rm", "-f", tID).AssertOK() - - base.Cmd("rmi", testutil.AlpineImage).AssertFail() - base.Cmd("rmi", "-f", testutil.AlpineImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.AlpineImage)) - - // a created container with removed image doesn't impact other `rmi` command - base.Cmd("rmi", "-f", testutil.NginxAlpineImage).AssertOK() - base.Cmd("images").AssertOutNotContains(testutil.ImageRepo(testutil.NginxAlpineImage)) -} diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go new file mode 100644 index 00000000000..863a34362e1 --- /dev/null +++ b/cmd/nerdctl/image/image_remove_test.go @@ -0,0 +1,312 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "errors" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestRemoveImage(t *testing.T) { + nerdtest.Setup() + + repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) + nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage) + // NOTES: + // - there MAY be circumstances where docker fails with this (as in: succeed in untagging the image) + // If that ever happens, we could add a preventative `image prune --force --all` inside Setup + // - since all of these are rmi-ing the common image, we need private mode + testGroup := test.Group{ + { + Description: "Remove image with stopped container - without -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with stopped container - with -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - without -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with running container - with -f", + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - without -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with created container - with -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.NginxAlpineImage) + helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("rmi", testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.All( + test.DoesNotContain(repoName), + // a created container with removed image doesn't impact other `rmi` command + test.DoesNotContain(nginxRepoName), + ), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - without -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + nerdtest.CGroup, + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with paused container - with -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + nerdtest.CGroup, + // FIXME: nerdctl is broken + // https://github.com/containerd/nerdctl/issues/3454 + // If an image is associated with a running/paused containers, `docker rmi -f imageName` + // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails. + // In both cases, `nerdctl rmi -f` will fail. + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("pause", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - without -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("image is being used")}, + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.Contains(repoName), + }) + }, + } + }, + }, + { + Description: "Remove image with killed container - with -f", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + helpers.Ensure("kill", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: test.RunCommand("rmi", "-f", testutil.CommonImage), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + helpers.Command("images").Run(&test.Expected{ + Output: test.DoesNotContain(repoName), + }) + }, + } + }, + }, + } + + testGroup.Run(t) +} diff --git a/cmd/nerdctl/image/image_save_linux_test.go b/cmd/nerdctl/image/image_save_linux_test.go deleted file mode 100644 index 0c7c722e97e..00000000000 --- a/cmd/nerdctl/image/image_save_linux_test.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package image - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestSave(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.AlpineImage).AssertOK() - archiveTarPath := filepath.Join(t.TempDir(), "a.tar") - base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK() - rootfsPath := filepath.Join(t.TempDir(), "rootfs") - err := helpers.ExtractDockerArchive(archiveTarPath, rootfsPath) - assert.NilError(t, err) - etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") - etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) - assert.NilError(t, err) - etcOSRelease := string(etcOSReleaseBytes) - t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage) - t.Log(etcOSRelease) - assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) -} diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index c8078967477..f262dc96972 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -17,54 +17,167 @@ package image import ( + "os" "path/filepath" "strings" "testing" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestSaveById(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] - } - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() -} +func TestSave(t *testing.T) { + nerdtest.Setup() -func TestSaveByIdWithDifferentNames(t *testing.T) { - // See detailed comment in TestRunCustomRootfs for why we need a separate namespace. - base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t)) - t.Cleanup(func() { - base.Cmd("namespace", "remove", testutil.Identifier(t)).Run() - }) - base.Cmd("pull", testutil.CommonImage).AssertOK() - inspect := base.InspectImage(testutil.CommonImage) - var id string - if testutil.GetTarget() == testutil.Docker { - id = inspect.ID - } else { - id = strings.Split(inspect.RepoDigests[0], ":")[1] + testGroup := &test.Group{ + { + Description: "Test content (linux only)", + Require: test.Not(test.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("save", "-o", filepath.Join(data.TempDir(), "out.tar"), testutil.CommonImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + rootfsPath := filepath.Join(data.TempDir(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.TempDir(), "out.tar"), rootfsPath) + assert.NilError(t, err) + etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") + etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) + assert.NilError(t, err) + etcOSRelease := string(etcOSReleaseBytes) + assert.Assert(t, strings.Contains(etcOSRelease, "Alpine")) + }, + } + }, + }, + { + Description: "Single image, by id", + // This test relies on the fact that we can remove the common image, which definitely conflicts with others, + // hence the private mode. + // Further note though, that this will hide the fact this the save command could fail if some layers are missing. + // See https://github.com/containerd/nerdctl/issues/3425 and others for details. + Require: nerdtest.Private, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if testutil.GetTarget() == testutil.Docker { + id = img.ID + } else { + id = strings.Split(img.RepoDigests[0], ":")[1] + } + tarPath := filepath.Join(data.TempDir(), "out.tar") + helpers.Ensure("save", "-o", tarPath, id) + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("load", "-i", tarPath) + data.Set("id", id) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + { + Description: "Image with different names, by id", + Require: nerdtest.Private, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + img := nerdtest.InspectImage(helpers, testutil.CommonImage) + var id string + if testutil.GetTarget() == testutil.Docker { + id = img.ID + } else { + id = strings.Split(img.RepoDigests[0], ":")[1] + } + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + tarPath := filepath.Join(data.TempDir(), "out.tar") + helpers.Ensure("save", "-o", tarPath, id) + helpers.Ensure("rmi", "-f", testutil.CommonImage) + helpers.Ensure("load", "-i", tarPath) + data.Set("id", id) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo") + }, + Expected: test.Expects(0, nil, test.Equals("foo\n")), + }, + { + Description: "Issue 3425, without tag", + // This test relies on the fact that we can remove the common image, which definitely conflicts with others, + // hence the private mode. + // Further note though, that this will hide the fact this the save command could fail if some layers are missing. + // See https://github.com/containerd/nerdctl/issues/3425 and others for details. + Require: test.Require( + nerdtest.Private, + // FIXME + nerdtest.NerdctlNeedsFixing, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("run", "--name", data.Identifier(), "-d", testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("pull", testutil.CommonImage) + // helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + tarPath := filepath.Join(data.TempDir(), "out.tar") + return helpers.Command("save", "-o", tarPath, testutil.CommonImage) // data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Issue 3425, with tag", + // This test relies on the fact that we can remove the common image, which definitely conflicts with others, + // hence the private mode. + // Further note though, that this will hide the fact this the save command could fail if some layers are missing. + // See https://github.com/containerd/nerdctl/issues/3425 and others for details. + Require: test.Require( + nerdtest.Private, + nerdtest.NerdctlNeedsFixing, + ), + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Get("id")) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("run", "--name", data.Identifier(), "-d", testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + tarPath := filepath.Join(data.TempDir(), "out.tar") + return helpers.Command("save", "-o", tarPath, data.Identifier()) + }, + Expected: test.Expects(0, nil, nil), + }, } - base.Cmd("tag", testutil.CommonImage, "foobar").AssertOK() - - archiveTarPath := filepath.Join(t.TempDir(), "id.tar") - base.Cmd("save", "-o", archiveTarPath, id).AssertOK() - base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK() - base.Cmd("load", "-i", archiveTarPath).AssertOK() - base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK() + testGroup.Run(t) }