From 9539f6b0267febe2d2ce7853a6a400f8b9791996 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:17:28 -0700 Subject: [PATCH 1/6] Add fetch and EnsureAllContent methods Signed-off-by: apostasie --- pkg/cmd/image/ensure.go | 127 +++++++++++++++++++++++++++++++++++++ pkg/imgutil/fetch/fetch.go | 92 +++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 pkg/cmd/image/ensure.go create mode 100644 pkg/imgutil/fetch/fetch.go diff --git a/pkg/cmd/image/ensure.go b/pkg/cmd/image/ensure.go new file mode 100644 index 00000000000..7d5a2eec440 --- /dev/null +++ b/pkg/cmd/image/ensure.go @@ -0,0 +1,127 @@ +/* + 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 ( + "context" + "errors" + "fmt" + "net/http" + "os" + + distributionref "github.com/distribution/reference" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" + "github.com/containerd/nerdctl/v2/pkg/errutil" + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" + "github.com/containerd/nerdctl/v2/pkg/imgutil/fetch" + "github.com/containerd/nerdctl/v2/pkg/platformutil" +) + +func EnsureAllContent(ctx context.Context, client *containerd.Client, srcName string, options types.GlobalCommandOptions) error { + // Get the image from the srcName + imageService := client.ImageService() + img, err := imageService.Get(ctx, srcName) + if err != nil { + fmt.Println("Failed getting imageservice") + return err + } + + provider := containerdutil.NewProvider(client) + snapshotter := containerdutil.SnapshotService(client, options.Snapshotter) + // Read the image + imagesList, _ := read(ctx, provider, snapshotter, img.Target) + // Iterate through the list + for _, i := range imagesList { + err = ensureOne(ctx, client, srcName, img.Target, i.platform, options) + if err != nil { + return err + } + } + + return nil +} + +func ensureOne(ctx context.Context, client *containerd.Client, rawRef string, target ocispec.Descriptor, platform ocispec.Platform, options types.GlobalCommandOptions) error { + + named, err := distributionref.ParseDockerRef(rawRef) + if err != nil { + return err + } + refDomain := distributionref.Domain(named) + // if platform == nil { + // platform = platforms.DefaultSpec() + //} + pltf := []ocispec.Platform{platform} + platformComparer := platformutil.NewMatchComparerFromOCISpecPlatformSlice(pltf) + + _, _, _, missing, err := images.Check(ctx, client.ContentStore(), target, platformComparer) + if err != nil { + return err + } + + if len(missing) > 0 { + // Get a resolver + var dOpts []dockerconfigresolver.Opt + if options.InsecureRegistry { + log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true)) + } + dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.HostsDir)) + resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...) + if err != nil { + return err + } + config := &fetch.Config{ + Resolver: resolver, + RemoteOpts: []containerd.RemoteOpt{}, + Platforms: pltf, + ProgressOutput: os.Stderr, + } + + err = fetch.Fetch(ctx, client, rawRef, config) + + if err != nil { + // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused". + if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) { + return err + } + if options.InsecureRegistry { + log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain) + dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true)) + resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...) + if err != nil { + return err + } + config.Resolver = resolver + return fetch.Fetch(ctx, client, rawRef, config) + } + log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain) + log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)") + } + + return err + } + + return nil +} diff --git a/pkg/imgutil/fetch/fetch.go b/pkg/imgutil/fetch/fetch.go new file mode 100644 index 00000000000..c9ab04f5533 --- /dev/null +++ b/pkg/imgutil/fetch/fetch.go @@ -0,0 +1,92 @@ +/* + 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 fetch + +import ( + "context" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/core/remotes" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs" + "github.com/containerd/nerdctl/v2/pkg/platformutil" +) + +// Config for content fetch +type Config struct { + // Resolver + Resolver remotes.Resolver + // ProgressOutput to display progress + ProgressOutput io.Writer + // RemoteOpts, e.g. containerd.WithPullUnpack. + // + // Regardless to RemoteOpts, the following opts are always set: + // WithResolver, WithImageHandler, WithSchema1Conversion + // + // RemoteOpts related to unpacking can be set only when len(Platforms) is 1. + RemoteOpts []containerd.RemoteOpt + Platforms []ocispec.Platform // empty for all-platforms +} + +func Fetch(ctx context.Context, client *containerd.Client, ref string, config *Config) error { + ongoing := jobs.New(ref) + + pctx, stopProgress := context.WithCancel(ctx) + progress := make(chan struct{}) + + go func() { + if config.ProgressOutput != nil { + // no progress bar, because it hides some debug logs + jobs.ShowProgress(pctx, ongoing, client.ContentStore(), config.ProgressOutput) + } + close(progress) + }() + + h := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if desc.MediaType != images.MediaTypeDockerSchema1Manifest { + ongoing.Add(desc) + } + return nil, nil + }) + + log.G(pctx).WithField("image", ref).Debug("fetching") + platformMC := platformutil.NewMatchComparerFromOCISpecPlatformSlice(config.Platforms) + opts := []containerd.RemoteOpt{ + containerd.WithResolver(config.Resolver), + containerd.WithImageHandler(h), + //nolint:staticcheck + containerd.WithSchema1Conversion, //lint:ignore SA1019 nerdctl should support schema1 as well. + containerd.WithPlatformMatcher(platformMC), + } + opts = append(opts, config.RemoteOpts...) + + // Note that client.Fetch does not unpack + _, err := client.Fetch(pctx, ref, opts...) + + stopProgress() + if err != nil { + return err + } + + <-progress + return nil +} From 8ba7ce7879cf0f32a6cc9828add91cb1defdfba9 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:19:28 -0700 Subject: [PATCH 2/6] EnsureAllContent on commit Signed-off-by: apostasie --- pkg/imgutil/commit/commit.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index 70a48f81586..101cf7987f5 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -46,6 +46,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/image" "github.com/containerd/nerdctl/v2/pkg/containerutil" imgutil "github.com/containerd/nerdctl/v2/pkg/imgutil" "github.com/containerd/nerdctl/v2/pkg/labels" @@ -126,6 +127,13 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd return emptyDigest, err } + // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 + err = image.EnsureAllContent(ctx, client, baseImg.Name(), globalOptions) + if err != nil { + log.G(ctx).Warn("Unable to fetch missing layers before committing. " + + "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.") + } + if opts.Pause { task, err := container.Task(ctx, cio.Load) if err != nil { From ef09d191c9acb8fc8de8941468e8b3289b7ce2d2 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:19:47 -0700 Subject: [PATCH 3/6] EnsureAllContent on convert Signed-off-by: apostasie --- pkg/cmd/image/convert.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index d628c43158a..e320a421be2 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -75,6 +75,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa } convertOpts = append(convertOpts, converter.WithPlatform(platMC)) + // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 + err = EnsureAllContent(ctx, client, srcRawRef, options.GOptions) + if err != nil { + return err + } + estargz := options.Estargz zstd := options.Zstd zstdchunked := options.ZstdChunked From 8e10d879970ac7327c11b3522e36d86ab3d85298 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:20:05 -0700 Subject: [PATCH 4/6] EnsureAllContent on save Signed-off-by: apostasie --- pkg/cmd/image/save.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index fc92e0b9eb9..815305ee80c 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -48,6 +48,13 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio if found.UniqueImages > 1 { return fmt.Errorf("ambiguous digest ID: multiple IDs found with provided prefix %s", found.Req) } + + // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 + err = EnsureAllContent(ctx, client, found.Image.Name, options.GOptions) + if err != nil { + return err + } + imgName := found.Image.Name imgDigest := found.Image.Target.Digest.String() if _, ok := savedImages[imgDigest]; !ok { From 1e52bf526f6ea8797e6a323aee1e557cad346dbf Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:21:53 -0700 Subject: [PATCH 5/6] EnsureAllContent on tag Signed-off-by: apostasie --- pkg/cmd/image/tag.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/image/tag.go b/pkg/cmd/image/tag.go index 48929d3694e..70f68acae84 100644 --- a/pkg/cmd/image/tag.go +++ b/pkg/cmd/image/tag.go @@ -22,6 +22,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/errdefs" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" @@ -31,7 +32,7 @@ import ( func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagOptions) error { imageService := client.ImageService() var srcName string - imagewalker := &imagewalker.ImageWalker{ + walker := &imagewalker.ImageWalker{ Client: client, OnFound: func(ctx context.Context, found imagewalker.Found) error { if srcName == "" { @@ -40,7 +41,7 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO return nil }, } - matchCount, err := imagewalker.Walk(ctx, options.Source) + matchCount, err := walker.Walk(ctx, options.Source) if err != nil { return err } @@ -59,17 +60,25 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO } defer done(ctx) - image, err := imageService.Get(ctx, srcName) + // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 + err = EnsureAllContent(ctx, client, srcName, options.GOptions) + if err != nil { + log.G(ctx).Warn("Unable to fetch missing layers before committing. " + + "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.") + } + + img, err := imageService.Get(ctx, srcName) if err != nil { return err } - image.Name = target.String() - if _, err = imageService.Create(ctx, image); err != nil { + + img.Name = target.String() + if _, err = imageService.Create(ctx, img); err != nil { if errdefs.IsAlreadyExists(err) { - if err = imageService.Delete(ctx, image.Name); err != nil { + if err = imageService.Delete(ctx, img.Name); err != nil { return err } - if _, err = imageService.Create(ctx, image); err != nil { + if _, err = imageService.Create(ctx, img); err != nil { return err } } else { From 764a2aa51f17461515fdb1fe0ead5a031df0b5e1 Mon Sep 17 00:00:00 2001 From: apostasie Date: Tue, 24 Sep 2024 21:23:04 -0700 Subject: [PATCH 6/6] Fix tests and add regression tests for #3425 Signed-off-by: apostasie --- .../container/container_commit_linux_test.go | 46 ++- cmd/nerdctl/image/image_inspect_test.go | 300 ++++++++++-------- cmd/nerdctl/image/image_list_test.go | 3 +- cmd/nerdctl/issues/issues_linux_test.go | 133 ++++++++ hack/build-integration-kubernetes.sh | 19 +- hack/kind.yaml | 2 + pkg/testutil/nerdtest/test.go | 14 + 7 files changed, 374 insertions(+), 143 deletions(-) create mode 100644 cmd/nerdctl/issues/issues_linux_test.go diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index 24c791d4f60..8a4af41fdcd 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -20,8 +20,6 @@ import ( "strings" "testing" - "gotest.tools/v3/icmd" - "github.com/containerd/nerdctl/v2/pkg/testutil" ) @@ -35,12 +33,11 @@ It will regularly succeed or fail, making random PR fail the Kube check. func TestKubeCommitPush(t *testing.T) { t.Parallel() - t.Skip("Test that confirm that #827 is still broken is too flaky") - base := testutil.NewBaseForKubernetes(t) tID := testutil.Identifier(t) var containerID string + // var registryIP string setup := func() { testutil.KubectlHelper(base, "run", "--image", testutil.CommonImage, tID, "--", "sleep", "Inf"). @@ -55,10 +52,37 @@ func TestKubeCommitPush(t *testing.T) { cmd := testutil.KubectlHelper(base, "get", "pods", tID, "-o", "jsonpath={ .status.containerStatuses[0].containerID }") cmd.Run() containerID = strings.TrimPrefix(cmd.Out(), "containerd://") + + // This below is missing configuration to allow for plain http communication + // This is left here for future work to successfully start a registry usable in the cluster + /* + // Start a registry + testutil.KubectlHelper(base, "run", "--port", "5000", "--image", testutil.RegistryImageStable, "testregistry"). + AssertOK() + + testutil.KubectlHelper(base, "wait", "pod", "testregistry", "--for=condition=ready", "--timeout=1m"). + AssertOK() + + cmd = testutil.KubectlHelper(base, "get", "pods", tID, "-o", "jsonpath={ .status.hostIPs[0].ip }") + cmd.Run() + registryIP = cmd.Out() + + cmd = testutil.KubectlHelper(base, "apply", "-f", "-", fmt.Sprintf(`apiVersion: v1 + kind: ConfigMap + metadata: + name: local-registry + namespace: nerdctl-test + data: + localRegistryHosting.v1: | + host: "%s:5000" + help: "https://kind.sigs.k8s.io/docs/user/local-registry/" + `, registryIP)) + */ + } tearDown := func() { - testutil.KubectlHelper(base, "delete", "pod", "-f", tID).Run() + testutil.KubectlHelper(base, "delete", "pod", "--all").Run() } tearDown() @@ -66,15 +90,7 @@ func TestKubeCommitPush(t *testing.T) { setup() t.Run("test commit / push on Kube (https://github.com/containerd/nerdctl/issues/827)", func(t *testing.T) { - t.Log("This test is meant to verify that we can commit / push an image from a pod." + - "Currently, this is broken, hence the test assumes it will fail. Once the problem is fixed, we should just" + - "change the expectation to 'success'.") - - base.Cmd("commit", containerID, "registry.example.com/my-app:v1").AssertOK() - // See note above. - base.Cmd("push", "registry.example.com/my-app:v1").Assert(icmd.Expected{ - ExitCode: 1, - Err: "failed to create a tmp single-platform image", - }) + base.Cmd("commit", containerID, "testcommitsave").AssertOK() + base.Cmd("save", "testcommitsave").AssertOK() }) } diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go index 14fcb90caf8..95154304d1c 100644 --- a/cmd/nerdctl/image/image_inspect_test.go +++ b/cmd/nerdctl/image/image_inspect_test.go @@ -26,69 +26,52 @@ 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) + nerdtest.Setup() - 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() + platform := runtime.GOOS + "/" + runtime.GOARCH tags := []string{ "", @@ -102,76 +85,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) + 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", platform) + helpers.Ensure("pull", "busybox", "--platform", platform) + helpers.Ensure("pull", "busybox:stable", "--platform", platform) + helpers.Ensure("pull", "registry-1.docker.io/library/busybox", "--platform", platform) + helpers.Ensure("pull", "registry-1.docker.io/library/busybox:stable", "--platform", platform) + }, + 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, alpine.ID+" vs "+it.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) + }, + } + }, + }, + }, } - // 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(), "") - } - - // 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/issues/issues_linux_test.go b/cmd/nerdctl/issues/issues_linux_test.go new file mode 100644 index 00000000000..72f68b707b5 --- /dev/null +++ b/cmd/nerdctl/issues/issues_linux_test.go @@ -0,0 +1,133 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package issues is meant to document testing for complex scenarios type of issues that cannot simply be ascribed +// to a specific package. +package issues + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" + "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" +) + +func TestMain(m *testing.M) { + testutil.M(m) +} + +func TestIssue3425(t *testing.T) { + nerdtest.Setup() + + var registry *testregistry.RegistryServer + + testCase := &test.Case{ + Description: "TestIssue3425", + Setup: func(data test.Data, helpers test.Helpers) { + base := testutil.NewBase(t) + registry = testregistry.NewWithNoAuth(base, 0, false) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if registry != nil { + registry.Cleanup(nil) + } + }, + SubTests: []*test.Case{ + { + Description: "with tag", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("tag", testutil.CommonImage, fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with commit", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "touch", "/something") + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("commit", data.Identifier(), fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", registry.Port, data.Identifier())) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with save", + Require: nerdtest.Private, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("image", "pull", testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.Command { + return helpers.Command("save", testutil.CommonImage) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "with convert", + Require: test.Require( + nerdtest.Private, + test.Not(test.Windows), + test.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("image", "pull", testutil.CommonImage) + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage) + helpers.Ensure("image", "rm", "-f", testutil.CommonImage) + helpers.Ensure("image", "pull", testutil.CommonImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", 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), + }, + }, + } + + testCase.Run(t) +} diff --git a/hack/build-integration-kubernetes.sh b/hack/build-integration-kubernetes.sh index 0fa32457b21..e41f13fcf7f 100755 --- a/hack/build-integration-kubernetes.sh +++ b/hack/build-integration-kubernetes.sh @@ -23,6 +23,7 @@ readonly root GO_VERSION=1.23 KIND_VERSION=v0.24.0 +CNI_PLUGINS_VERSION=v1.5.1 [ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64 @@ -53,6 +54,19 @@ install::kubectl(){ host::install "$temp"/kubectl } +install::cni(){ + local version="$1" + local temp + temp="$(fs::mktemp "install")" + + http::get "$temp"/cni.tgz "https://github.com/containernetworking/plugins/releases/download/$version/cni-plugins-${GOOS:-linux}-${GOARCH:-amd64}-$version.tgz" + sudo mkdir -p /opt/cni/bin + sudo tar xzf "$temp"/cni.tgz -C /opt/cni/bin + mkdir -p ~/opt/cni/bin + tar xzf "$temp"/cni.tgz -C ~/opt/cni/bin + rm "$temp"/cni.tgz +} + exec::kind(){ local args=() [ ! "$_rootful" ] || args=(sudo env PATH="$PATH" KIND_EXPERIMENTAL_PROVIDER="$KIND_EXPERIMENTAL_PROVIDER") @@ -85,6 +99,9 @@ main(){ PATH=$(pwd)/_output:"$PATH" export PATH + # Add CNI plugins + install::cni "$CNI_PLUGINS_VERSION" + # Hack to get go into kind control plane exec::nerdctl rm -f go-kind 2>/dev/null || true exec::nerdctl run -d --name go-kind golang:"$GO_VERSION" sleep Inf @@ -97,4 +114,4 @@ main(){ exec::kind create cluster --name nerdctl-test --config=./hack/kind.yaml } -main "$@" \ No newline at end of file +main "$@" diff --git a/hack/kind.yaml b/hack/kind.yaml index 1695fafdb88..c6439c02458 100644 --- a/hack/kind.yaml +++ b/hack/kind.yaml @@ -10,3 +10,5 @@ nodes: containerPath: /usr/local/go - hostPath: . containerPath: /nerdctl-source + - hostPath: /opt/cni + containerPath: /opt/cni diff --git a/pkg/testutil/nerdtest/test.go b/pkg/testutil/nerdtest/test.go index a2f7a5bd3c2..812ff08591d 100644 --- a/pkg/testutil/nerdtest/test.go +++ b/pkg/testutil/nerdtest/test.go @@ -170,6 +170,20 @@ func InspectNetwork(helpers test.Helpers, name string, args ...string) dockercom return dc[0] } +func InspectImage(helpers test.Helpers, name string) dockercompat.Image { + var dc []dockercompat.Image + cmd := helpers.Command("image", "inspect", name) + cmd.Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info) + }, + }) + return dc[0] +} + func nerdctlSetup(testCase *test.Case, t *testing.T) test.Command { t.Helper()