diff --git a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go b/cmd/nerdctl/ipfs/ipfs_build_linux_test.go deleted file mode 100644 index 7ca00803bdb..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_build_linux_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package ipfs - -import ( - "fmt" - "strings" - "testing" - "time" - - "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/testutil" -) - -func TestIPFSBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") - - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s -CMD ["echo", "nerdctl-build-test-string"] - `, ipfsCIDBase) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - done := ipfsRegistryUp(t, base) - defer done() - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("build", buildCtx, "-t", imageName).AssertOK() - - base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-build-test-string") -} - -func ipfsRegistryUp(t *testing.T, base *testutil.Base, args ...string) (done func() error) { - res := icmd.StartCmd(base.Cmd(append([]string{"ipfs", "registry", "serve"}, args...)...).Cmd) - time.Sleep(time.Second) - assert.Assert(t, res.Cmd.Process != nil) - assert.NilError(t, res.Error) - return func() error { - res.Cmd.Process.Kill() - icmd.WaitOnCmd(3*time.Second, res) - return nil - } -} diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index 1ac2b682482..7619e0b0591 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -19,84 +19,144 @@ package ipfs import ( "fmt" "io" + "strconv" "strings" "testing" + "time" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSComposeUp(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - tests := []struct { - name string - snapshotter string - pushOptions []string - composeOptions []string - requiresStargz bool - }{ - { - name: "overlayfs", - snapshotter: "overlayfs", - }, - { - name: "stargz", - snapshotter: "stargz", - pushOptions: []string{"--estargz"}, - requiresStargz: true, - }, - { - name: "ipfs-address", - snapshotter: "overlayfs", - pushOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - composeOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - if tt.requiresStargz { - helpers.RequiresStargz(base) - } - ipfsImgs := make([]string, 2) - for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} { - ipfsImgs[i] = pushImageToIPFS(t, base, img, tt.pushOptions...) +func TestIPFSCompNoBuild(t *testing.T) { + testCase := nerdtest.Setup() + + const ipfsAddrKey = "ipfsAddrKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Registry, + nerdtest.IPFS, + // See note below + // nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Start Kubo + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + data.Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)) + + // Ensure we have the images + helpers.Ensure("pull", "--quiet", testutil.WordpressImage) + helpers.Ensure("pull", "--quiet", testutil.MariaDBImage) + } + + testCase.SubTests = []*test.Case{ + subtestTestIPFSCompNoB(t, false, false), + subtestTestIPFSCompNoB(t, true, false), + subtestTestIPFSCompNoB(t, false, true), + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + } + // Speeding up repeat tests... + helpers.Anyhow("rmi", testutil.WordpressImage) + helpers.Anyhow("rmi", testutil.MariaDBImage) + } + + testCase.Run(t) +} + +func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { + t.Helper() + + const ipfsAddrKey = "ipfsAddrKey" + const mariaImageCIDKey = "mariaImageCIDKey" + const wordpressImageCIDKey = "wordpressImageCIDKey" + const composeExtraKey = "composeExtraKey" + + testCase := &test.Case{} + + testCase.Description += "with" + + if stargz { + testCase.Description += "-stargz" + } + + if byAddr { + testCase.Description += "-byAddr" + } + + if stargz { + testCase.Require = nerdtest.Stargz + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var ipfsCIDWP, ipfsCIDMD string + if stargz { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--estargz") + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--estargz") + } else if byAddr { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Get(ipfsAddrKey)) + data.Set(composeExtraKey, "--ipfs-address="+data.Get(ipfsAddrKey)) + } else { + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage) + } + data.Set(wordpressImageCIDKey, ipfsCIDWP) + data.Set(mariaImageCIDKey, ipfsCIDMD) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // NOTE: + // Removing these images locally forces tests to be sequentials (as IPFS being content addressable, + // they have the same cid - except for the estargz version obviously) + // Deliberately electing to not remove them here so that we can parallelize and cut down the running time + /* + if data.Get(mariaImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mariaImageCIDKey)) + helpers.Anyhow("rmi", data.Get(wordpressImageCIDKey)) } - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER="+tt.snapshotter) - helpers.ComposeUp(t, base, fmt.Sprintf(` + */ + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + safePort, err := portlock.Acquire(0) + assert.NilError(helpers.T(), err) + data.Set("wordpressPort", strconv.Itoa(safePort)) + composeUP(data, helpers, fmt.Sprintf(` version: '3.1' services: wordpress: - image: %s + image: ipfs://%s restart: always ports: - - 8080:80 + - %d:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser WORDPRESS_DB_PASSWORD: examplepass WORDPRESS_DB_NAME: exampledb volumes: - # workaround for https://github.com/containerd/stargz-snapshotter/issues/444 - - "/run" - wordpress:/var/www/html db: - image: %s + image: ipfs://%s restart: always environment: MYSQL_DATABASE: exampledb @@ -104,53 +164,160 @@ services: MYSQL_PASSWORD: examplepass MYSQL_RANDOM_ROOT_PASSWORD: '1' volumes: - # workaround for https://github.com/containerd/stargz-snapshotter/issues/444 - - "/run" - db:/var/lib/mysql volumes: wordpress: db: -`, ipfsImgs[0], ipfsImgs[1]), tt.composeOptions...) - }) +`, data.Get(wordpressImageCIDKey), safePort, data.Get(mariaImageCIDKey)), data.Get(composeExtraKey)) + // FIXME: need to break down composeUP into testable commands instead + // Right now, this is just a dummy placeholder + return helpers.Command("info") } + + testCase.Expected = test.Expects(0, nil, nil) + + return testCase } -func TestIPFSComposeUpBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.NginxAlpineImage) - ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://") +func TestIPFSCompBuild(t *testing.T) { + testCase := nerdtest.Setup() + + var ipfsServer test.TestableCommand + var comp *testutil.ComposeDir + + const mainImageCIDKey = "mainImageCIDKey" + safePort, err := portlock.Acquire(0) + assert.NilError(t, err) + var listenAddr = "localhost:" + strconv.Itoa(safePort) + + testCase.Require = test.Require( + // Linux only + test.Linux, + // Obviously not docker supported + test.Not(nerdtest.Docker), + nerdtest.Build, + nerdtest.IPFS, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Get alpine + helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage) + // Start a local ipfs backed registry + // FIXME: this is bad and likely to collide with other tests + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) - const dockerComposeYAML = ` + // Save nginx to ipfs + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) + + const dockerComposeYAML = ` services: web: build: . ports: - - 8080:80 + - 8081:80 ` - dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s + dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s COPY index.html /usr/share/nginx/html/index.html -`, ipfsCIDBase) - indexHTML := t.Name() +`, listenAddr, data.Get(mainImageCIDKey)) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + comp = testutil.NewComposeDir(t, dockerComposeYAML) + comp.WriteFile("Dockerfile", dockerfile) + comp.WriteFile("index.html", data.Identifier("indexhtml")) + } - comp.WriteFile("Dockerfile", dockerfile) - comp.WriteFile("index.html", indexHTML) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + ipfsServer.Run(nil) + } + if comp != nil { + helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + comp.CleanUp() + } + } - done := ipfsRegistryUp(t, base) - defer done() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", comp.YAMLFullPath(), "up", "-d", "--build") + } - resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - t.Logf("respBody=%q", respBody) - assert.Assert(t, strings.Contains(string(respBody), indexHTML)) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false) + assert.NilError(t, err) + respBody, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + t.Logf("respBody=%q", respBody) + assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml"))) + }, + } + } + + testCase.Run(t) +} + +func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, opts string) { + comp := testutil.NewComposeDir(helpers.T(), dockerComposeYAML) + // defer comp.CleanUp() + + // Because it might or might not happen, and + helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + defer helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") + + projectName := comp.ProjectName() + + args := []string{"compose", "-f", comp.YAMLFullPath()} + if opts != "" { + args = append(args, opts) + } + + helpers.Ensure(append(args, "up", "--quiet-pull", "-d")...) + + helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName)) + + checkWordpress := func() error { + // FIXME: see other notes on using the same port repeatedly + resp, err := nettestutil.HTTPGet("http://127.0.0.1:"+data.Get("wordpressPort"), 5, false) + if err != nil { + return err + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { + return fmt.Errorf("respBody does not contain %q (%s)", testutil.WordpressIndexHTMLSnippet, string(respBody)) + } + return nil + } + + var wordpressWorking bool + var err error + // 15 seconds is long enough + for i := 0; i < 5; i++ { + err = checkWordpress() + if err == nil { + wordpressWorking = true + break + } + time.Sleep(3 * time.Second) + } + + if !wordpressWorking { + ccc := helpers.Capture("ps", "-a") + helpers.T().Log(ccc) + helpers.T().Error(helpers.Err("logs", projectName+"-wordpress-1")) + helpers.T().Fatalf("wordpress is not working %v", err) + } + + helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v") + helpers.Fail("volume", "inspect", fmt.Sprintf("%s_db", projectName)) + helpers.Fail("network", "inspect", fmt.Sprintf("%s_default", projectName)) } diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go new file mode 100644 index 00000000000..3738ceb4514 --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -0,0 +1,105 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "fmt" + "regexp" + "testing" + + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSAddrWithKubo(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImagemainImageCIDKey" + const ipfsAddrKey = "ipfsAddrKey" + + var ipfsRegistry *registry.Server + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.Registry, + nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil) + ipfsRegistry.Setup(data, helpers) + ipfsAddr := fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port) + data.Set(ipfsAddrKey, ipfsAddr) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsRegistry != nil { + ipfsRegistry.Cleanup(data, helpers) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey))) + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.Private, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + ipfsCID := pushToIPFS(helpers, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey)), "--estargz") + helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Set(mainImageCIDKey, ipfsCID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/ipfs/ipfs_linux_test.go b/cmd/nerdctl/ipfs/ipfs_linux_test.go deleted file mode 100644 index a50e426ae0f..00000000000 --- a/cmd/nerdctl/ipfs/ipfs_linux_test.go +++ /dev/null @@ -1,148 +0,0 @@ -/* - Copyright The containerd Authors. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package ipfs - -import ( - "fmt" - "testing" - - "gotest.tools/v3/assert" - - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" - "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" -) - -func TestIPFS(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - - // encryption - keyPair := helpers.NewJWEKeyPair(t) - defer keyPair.Cleanup() - tID := testutil.Identifier(t) - encryptImageRef := tID + ":enc" - layersNum := 1 - base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, ipfsCID, encryptImageRef).AssertOK() - base.Cmd("image", "inspect", "--mode=native", "--format={{len .Manifest.Layers}}", encryptImageRef).AssertOutExactly(fmt.Sprintf("%d\n", layersNum)) - for i := 0; i < layersNum; i++ { - base.Cmd("image", "inspect", "--mode=native", fmt.Sprintf("--format={{json (index .Manifest.Layers %d) }}", i), encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe") - } - ipfsCIDEnc := cidOf(t, base.Cmd("push", "ipfs://"+encryptImageRef).OutLines()) - helpers.RmiAll(base) - - decryptImageRef := tID + ":dec" - base.Cmd("pull", "--unpack=false", ipfsCIDEnc).AssertOK() - base.Cmd("image", "decrypt", "--key="+keyPair.Pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key - base.Cmd("image", "decrypt", "--key="+keyPair.Prv, ipfsCIDEnc, decryptImageRef).AssertOK() - base.Cmd("run", "--rm", decryptImageRef, "/bin/sh", "-c", "echo hello").AssertOK() -} - -func TestIPFSAddress(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - iReg := testregistry.NewIPFSRegistry(base, nil, 0, nil, nil) - t.Cleanup(func() { - iReg.Cleanup(nil) - }) - ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/%d", iReg.IP, iReg.Port) - - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", ipfsaddr)) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", "--ipfs-address", ipfsaddr, ipfsCID).AssertOK() - base.Cmd("run", "--ipfs-address", ipfsaddr, "--rm", ipfsCID, "echo", "hello").AssertOK() -} - -func TestIPFSCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK() -} - -func TestIPFSWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() -} - -func TestIPFSWithLazyPullingCommit(t *testing.T) { - // cgroup is required for nerdctl commit - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - base.Cmd("pull", ipfsCID).AssertOK() - base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK() - tID := testutil.Identifier(t) - newContainer, newImg := tID, tID+":v1" - base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK() - base.Cmd("commit", newContainer, newImg).AssertOK() - base.Cmd("kill", newContainer).AssertOK() - base.Cmd("rm", newContainer).AssertOK() - ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines()) - helpers.RmiAll(base) - - base.Cmd("pull", ipfsCID2).AssertOK() - base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "ls /.stargz-snapshotter && cat /hello").AssertOK() - base.Cmd("image", "rm", ipfsCID2).AssertOK() -} - -func pushImageToIPFS(t *testing.T, base *testutil.Base, name string, opts ...string) string { - base.Cmd("pull", name).AssertOK() - ipfsCID := cidOf(t, base.Cmd(append([]string{"push"}, append(opts, "ipfs://"+name)...)...).OutLines()) - base.Cmd("rmi", name).Run() - return ipfsCID -} - -func cidOf(t *testing.T, lines []string) string { - assert.Equal(t, len(lines) >= 2, true) - return "ipfs://" + lines[len(lines)-2] -} diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index d367d0d9f86..63a461361cc 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -17,44 +17,131 @@ package ipfs import ( + "fmt" + "regexp" "strings" "testing" + "time" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" ) -func TestIPFSRegistry(t *testing.T) { - testutil.DockerIncompatible(t) +func pushToIPFS(helpers test.Helpers, name string, opts ...string) string { + var ipfsCID string + cmd := helpers.Command("push", "ipfs://"+name) + cmd.WithArgs(opts...) + cmd.Run(&test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + lines := strings.Split(stdout, "\n") + assert.Equal(t, len(lines) >= 2, true) + ipfsCID = lines[len(lines)-2] + }, + }) + return ipfsCID +} - base := testutil.NewBase(t) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=overlayfs") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage) - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) +func TestIPFSNerdctlRegistry(t *testing.T) { + testCase := nerdtest.Setup() - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "echo", "hello").AssertOK() -} + // FIXME: this is bad and likely to collide with other tests + const listenAddr = "localhost:5555" -func TestIPFSRegistryWithLazyPulling(t *testing.T) { - testutil.DockerIncompatible(t) + const ipfsImageURLKey = "ipfsImageURLKey" - base := testutil.NewBase(t) - helpers.RequiresStargz(base) - base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=stargz") - ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz") - ipfsRegistryAddr := "localhost:5555" - ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID) + var ipfsServer test.TestableCommand - done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr) - defer done() - base.Cmd("pull", ipfsRegistryRef).AssertOK() - base.Cmd("run", "--rm", ipfsRegistryRef, "ls", "/.stargz-snapshotter").AssertOK() -} + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.IPFS, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + + // Start a local ipfs backed registry + ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr) + // Once foregrounded, do not wait for it more than a second + ipfsServer.Background(1 * time.Second) + // Apparently necessary to let it start... + time.Sleep(time.Second) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if ipfsServer != nil { + // Close the server once done + ipfsServer.Run(nil) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotterr", + NoParallel: true, + Require: nerdtest.Stargz, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", data.Get(ipfsImageURLKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), + }, + { + Description: "with build", + NoParallel: true, + Require: nerdtest.Build, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", data.Identifier("built-image")) + if data.Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", data.Get(ipfsImageURLKey)) + } + }, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.AlpineImage)) + + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, data.Get(ipfsImageURLKey)) + + buildCtx := testhelpers.CreateBuildContext(t, dockerfile) + + helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Identifier("built-image")) + }, + Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")), + }, + } -func ipfsRegistryReference(addr string, c string) string { - return addr + "/ipfs/" + strings.TrimPrefix(c, "ipfs://") + testCase.Run(t) } diff --git a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go new file mode 100644 index 00000000000..e18acc332ee --- /dev/null +++ b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go @@ -0,0 +1,227 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ipfs + +import ( + "regexp" + "testing" + + testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" +) + +func TestIPFSSimple(t *testing.T) { + testCase := nerdtest.Setup() + + const mainImageCIDKey = "mainImageCIDKey" + const transformedImageCIDKey = "transformedImageCIDKey" + + testCase.Require = test.Require( + test.Linux, + test.Not(nerdtest.Docker), + nerdtest.IPFS, + // We constantly rmi the image by its CID which is shared across tests, so, we make this group private + // and every subtest NoParallel + nerdtest.Private, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.AlpineImage) + } + + testCase.SubTests = []*test.Case{ + { + Description: "with default snapshotter", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + }, + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with stargz snapshotter", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + } + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + }, + Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), + }, + { + Description: "with commit and push", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the committed image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "cat", "/hello") + }, + + Expected: test.Expects(0, nil, test.Equals("hello\n")), + }, + { + Description: "with commit and push, stargz lazy pulling", + NoParallel: true, + Require: test.Require( + nerdtest.Stargz, + nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"), + ), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage, "--estargz")) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Run a container that does modify something, then commit and push it + helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello") + helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image")) + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image"))) + + // Clean-up + helpers.Ensure("rm", data.Identifier("commit-container")) + helpers.Ensure("rmi", data.Identifier("commit-image")) + + // Pull back the image + helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", data.Identifier("commit-container")) + helpers.Anyhow("rmi", data.Identifier("commit-image")) + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", data.Get(transformedImageCIDKey)) + } + }, + + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter") + }, + + Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("hello[\n]sha256:.*[.]json[\n]"))), + }, + { + Description: "with encryption", + NoParallel: true, + Require: test.Binary("openssl"), + Setup: func(data test.Data, helpers test.Helpers) { + data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.AlpineImage)) + helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey)) + + // Prep a key pair + keyPair := testhelpers.NewJWEKeyPair(t) + // FIXME: this will only cleanup when the group is done, not right, but it works + t.Cleanup(keyPair.Cleanup) + data.Set("pub", keyPair.Pub) + data.Set("prv", keyPair.Prv) + + // Encrypt the image, and verify it is encrypted + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, data.Get(mainImageCIDKey), data.Identifier("encrypted")) + cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Equals("1\n"), + }) + cmd = helpers.Command("image", "inspect", "--mode=native", "--format={{json (index .Manifest.Layers 0) }}", data.Identifier("encrypted")) + cmd.Run(&test.Expected{ + Output: test.Contains("org.opencontainers.image.enc.keys.jwe"), + }) + + // Push the encrypted image and save the CID + data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted"))) + + // Remove both images locally + helpers.Ensure("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Ensure("rmi", "-f", data.Get(transformedImageCIDKey)) + + // Pull back without unpacking + helpers.Ensure("pull", "--unpack=false", "ipfs://"+data.Get(transformedImageCIDKey)) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if data.Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey)) + } + }, + SubTests: []*test.Case{ + { + Description: "decrypt with pub key does not work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "decrypt", "--key="+data.Get("pub"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "decrypt with priv key does work", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("decrypted")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("image", "decrypt", "--key="+data.Get("prv"), data.Get(transformedImageCIDKey), data.Identifier("decrypted")) + }, + Expected: test.Expects(0, nil, nil), + }, + }, + }, + } + + testCase.Run(t) +}