diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed5380..74029a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: matrix: test: - snapshotter + - push-n-pull - kubernetes - k3s - k3s-external diff --git a/modules/flake/overlays.nix b/modules/flake/overlays.nix index 385b5b2..7e52305 100644 --- a/modules/flake/overlays.nix +++ b/modules/flake/overlays.nix @@ -8,12 +8,12 @@ # Depends on PR merged into main, but not yet in a release tag. # See: https://github.com/containerd/containerd/pull/9028 containerd = super.containerd.overrideAttrs(o: { - src = self.fetchFromGitHub { - owner = "containerd"; - repo = "containerd"; - rev = "779875a057ff98e9b754371c193fe3b0c23ae7a2"; - hash = "sha256-sXMDMX0QPbnFvRYrAP+sVFjTI9IqzOmLnmqAo8lE9pg="; - }; + patches = (o.patches or []) ++ [ + # See: https://github.com/containerd/containerd/pull/9028 + ./patches/containerd-unpacker-wait.patch + # See: https://github.com/containerd/containerd/pull/9864 + ./patches/containerd-import-compressed.patch + ]; }); in { diff --git a/modules/flake/patches/containerd-import-compressed.patch b/modules/flake/patches/containerd-import-compressed.patch new file mode 100644 index 0000000..0a42829 --- /dev/null +++ b/modules/flake/patches/containerd-import-compressed.patch @@ -0,0 +1,36 @@ +commit 786b10f46aa4c10adf6f2c34f1f83d93d84af57f +Author: Edgar Lee +Date: Fri Feb 23 23:11:48 2024 +0800 + + Automatically decompress archives for transfer service import + + Signed-off-by: Edgar Lee + +diff --git a/pkg/transfer/archive/importer.go b/pkg/transfer/archive/importer.go +index a9c4cea93..b20055a0b 100644 +--- a/pkg/transfer/archive/importer.go ++++ b/pkg/transfer/archive/importer.go +@@ -24,6 +24,7 @@ import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + transferapi "github.com/containerd/containerd/api/types/transfer" ++ "github.com/containerd/containerd/archive/compression" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images/archive" + "github.com/containerd/containerd/log" +@@ -64,7 +65,14 @@ func (iis *ImageImportStream) Import(ctx context.Context, store content.Store) ( + if iis.forceCompress { + opts = append(opts, archive.WithImportCompression()) + } +- return archive.ImportIndex(ctx, store, iis.stream, opts...) ++ ++ r, err := compression.DecompressStream(iis.stream) ++ if err != nil { ++ return ocispec.Descriptor{}, err ++ } ++ defer r.Close() ++ ++ return archive.ImportIndex(ctx, store, r, opts...) + } + + func (iis *ImageImportStream) MarshalAny(ctx context.Context, sm streaming.StreamCreator) (typeurl.Any, error) { diff --git a/modules/flake/patches/containerd-unpacker-wait.patch b/modules/flake/patches/containerd-unpacker-wait.patch new file mode 100644 index 0000000..944ae9a --- /dev/null +++ b/modules/flake/patches/containerd-unpacker-wait.patch @@ -0,0 +1,39 @@ +commit 779875a057ff98e9b754371c193fe3b0c23ae7a2 +Author: Edgar Lee +Date: Tue Aug 29 15:34:19 2023 -0700 + + Add missing unpacker.Wait for image import + + - For remote snapshotters, the unpack phase serves as an important step for + preparing the remote snapshot. With the missing unpacker.Wait, the + snapshotter `Prepare` context is always canceled. + - This patch allows remote snapshotter based archives to be imported via + the transfer service or `ctr image import` + + Signed-off-by: Edgar Lee + +diff --git a/pkg/transfer/local/import.go b/pkg/transfer/local/import.go +index cf9944c72..6ccc4f1f9 100644 +--- a/pkg/transfer/local/import.go ++++ b/pkg/transfer/local/import.go +@@ -113,10 +113,20 @@ func (ts *localTransferService) importStream(ctx context.Context, i transfer.Ima + } + + if err := images.WalkNotEmpty(ctx, handler, index); err != nil { ++ if unpacker != nil { ++ // wait for unpacker to cleanup ++ unpacker.Wait() ++ } + // TODO: Handle Not Empty as a special case on the input + return err + } + ++ if unpacker != nil { ++ if _, err = unpacker.Wait(); err != nil { ++ return err ++ } ++ } ++ + for _, desc := range descriptors { + imgs, err := is.Store(ctx, desc, ts.images) + if err != nil { diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index 7a15dec..dbb571c 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -83,6 +83,7 @@ in { # NixOS tests for nix-snapshotter. nixosTests.snapshotter = import ./tests/snapshotter.nix; + nixosTests.push-n-pull = import ./tests/push-n-pull.nix; nixosTests.kubernetes = import ./tests/kubernetes.nix; nixosTests.k3s = import ./tests/k3s.nix; nixosTests.k3s-external = import ./tests/k3s-external.nix; diff --git a/modules/nixos/tests/gvisor.nix b/modules/nixos/tests/gvisor.nix index 935ae2c..fac93aa 100644 --- a/modules/nixos/tests/gvisor.nix +++ b/modules/nixos/tests/gvisor.nix @@ -90,16 +90,16 @@ in { ]; in '' - def test(machine, sudo_su = ""): - if sudo_su == "": - machine.wait_for_unit("nix-snapshotter.service") - machine.wait_for_unit("containerd.service") - machine.wait_for_unit("preload-containerd.service") + def wait_for_unit(machine, service, user = "alice"): + if "rootless" in machine.name: + machine.wait_until_succeeds(f"systemctl --user --machine={user}@ is-active {service}") else: - machine.succeed("loginctl enable-linger alice") - wait_for_user_unit(machine, "nix-snapshotter.service") - wait_for_user_unit(machine, "containerd.service") - wait_for_user_unit(machine, "preload-containerd.service") + machine.wait_for_unit(service) + + def test(machine, sudo_su = ""): + wait_for_unit(machine, "nix-snapshotter.service") + wait_for_unit(machine, "containerd.service") + wait_for_unit(machine, "preload-containerd.service") with subtest(f"{machine.name}: Run redis using runtime runsc"): machine.succeed(f"{sudo_su} nerdctl run -d --name redis --runtime runsc -p 30000:6379 --cap-add syslog ghcr.io/pdtpartners/redis") diff --git a/modules/nixos/tests/push-n-pull.nix b/modules/nixos/tests/push-n-pull.nix new file mode 100644 index 0000000..404e154 --- /dev/null +++ b/modules/nixos/tests/push-n-pull.nix @@ -0,0 +1,160 @@ +{ config, pkgs, lib, ... }: + +let + registryConfig = { + version = "0.1"; + storage = { + cache.blobdescriptor = "inmemory"; + filesystem.rootdirectory = "/var/lib/docker-registry"; + }; + http.addr = "0.0.0.0:5000"; + }; + + configFile = + pkgs.writeText + "docker-registry-config.yml" + (builtins.toJSON registryConfig); + + registry = pkgs.nix-snapshotter.buildImage { + name = "ghcr.io/pdtpartners/registry"; + tag = "latest"; + config = { + entrypoint = [ "${pkgs.docker-distribution}/bin/registry" ]; + cmd = [ "serve" configFile ]; + }; + }; + + helloDockerTools = pkgs.dockerTools.buildImage { + name = "localhost:5000/docker-tools/hello"; + tag = "latest"; + config.entrypoint = ["${pkgs.hello}/bin/hello"]; + }; + + helloNixSnapshotter = pkgs.nix-snapshotter.buildImage { + name = "localhost:5000/nix-snapshotter/hello"; + tag = "latest"; + config.entrypoint = ["${pkgs.hello}/bin/hello"]; + }; + +in { + nodes = rec { + rootful = { + virtualisation.containerd = { + enable = true; + nixSnapshotterIntegration = true; + }; + + services.nix-snapshotter = { + enable = true; + }; + + services.preload-containerd = { + enable = true; + targets = [{ + archives = [ + registry + helloDockerTools + helloNixSnapshotter + ]; + }]; + }; + + environment.systemPackages = [ + pkgs.nerdctl + ]; + }; + + rootless = { + virtualisation.containerd.rootless = { + enable = true; + nixSnapshotterIntegration = true; + }; + + services.nix-snapshotter.rootless = { + enable = true; + }; + + services.preload-containerd.rootless = { + enable = true; + targets = [{ + archives = [ + registry + helloDockerTools + helloNixSnapshotter + ]; + address = "$XDG_RUNTIME_DIR/containerd/containerd.sock"; + }]; + }; + + environment.systemPackages = [ + pkgs.nerdctl + ]; + + users.users.alice = { + uid = 1000; + isNormalUser = true; + }; + + environment.variables = { + XDG_RUNTIME_DIR = "/run/user/1000"; + }; + }; + }; + + testScript = + let + sudo_su = lib.concatStringsSep " " [ + "sudo" + "--preserve-env=XDG_RUNTIME_DIR,CONTAINERD_ADDRESS,CONTAINERD_SNAPSHOTTER" + "-u" + "alice" + ]; + + in '' + def collect_coverage(machine): + coverfiles = machine.succeed("ls /tmp/go-cover").split() + for coverfile in coverfiles: + machine.copy_from_vm(f"/tmp/go-cover/{coverfile}", f"build/go-cover/${config.name}-{machine.name}") + + def wait_for_unit(machine, service, user = "alice"): + if "rootless" in machine.name: + machine.wait_until_succeeds(f"systemctl --user --machine={user}@ is-active {service}") + else: + machine.wait_for_unit(service) + + def stop_unit(machine, service, user = "alice"): + if "rootless" in machine.name: + machine.succeed(f"systemctl --user --machine={user}@ stop {service}") + else: + machine.succeed(f"systemctl stop {service}") + + def test(machine, sudo_su = ""): + wait_for_unit(machine, "nix-snapshotter.service") + wait_for_unit(machine, "containerd.service") + wait_for_unit(machine, "preload-containerd.service") + + machine.succeed(f"{sudo_su} nerdctl run -d -p 5000:5000 --name registry ghcr.io/pdtpartners/registry") + + with subtest(f"{machine.name}: Push container built with pkgs.dockerTools.buildImage"): + machine.succeed(f"{sudo_su} nerdctl push localhost:5000/docker-tools/hello") + machine.succeed(f"{sudo_su} nerdctl rmi localhost:5000/docker-tools/hello") + + with subtest(f"{machine.name}: Push container built with pkgs.nix-snapshotter.buildImage"): + machine.succeed(f"{sudo_su} nerdctl push localhost:5000/nix-snapshotter/hello") + machine.succeed(f"{sudo_su} nerdctl rmi localhost:5000/nix-snapshotter/hello") + + with subtest(f"{machine.name}: Pull container built with pkgs.dockerTools.buildImage"): + machine.succeed(f"{sudo_su} nerdctl pull localhost:5000/docker-tools/hello") + + with subtest(f"{machine.name}: Pull container built with pkgs.nix-snapshotter.buildImage"): + machine.succeed(f"{sudo_su} nerdctl pull localhost:5000/nix-snapshotter/hello") + + stop_unit(machine, "nix-snapshotter") + collect_coverage(machine) + + start_all() + + test(rootful) + test(rootless, "${sudo_su}") + ''; +} diff --git a/modules/nixos/tests/snapshotter.nix b/modules/nixos/tests/snapshotter.nix index 14374fe..013dc88 100644 --- a/modules/nixos/tests/snapshotter.nix +++ b/modules/nixos/tests/snapshotter.nix @@ -1,195 +1,163 @@ { config, pkgs, lib, ... }: let - registryHost = "127.0.0.1"; - - registryPort = 5000; - - imageName = "${registryHost}:${toString registryPort}/hello"; - - regularTag = "latest"; - regularImage = "${imageName}:${regularTag}"; + helloDrvFile = pkgs.nix-snapshotter.buildImage { + name = "ghcr.io/pdtpartners/hello-world"; + tag = "latest"; + config.entrypoint = [ + (pkgs.writeShellScript "hello-world" '' + #!${pkgs.runtimeShell} + echo "Hello, world!" + '') + ]; + }; - nixTag = "nix"; - nixImage = "${imageName}:${nixTag}"; + redisDockerTools = pkgs.dockerTools.buildImage { + name = "ghcr.io/docker-tools/redis"; + tag = "latest"; + config = { + Entrypoint = [ "${pkgs.redis}/bin/redis-server" ]; + Cmd = [ "--protected-mode" "no" ]; + }; + }; - base = { pkgs, ... }: - let - helloTarball = pkgs.dockerTools.buildImage { - name = imageName; - tag = regularTag; - config.entrypoint = ["${pkgs.hello}/bin/hello"]; - }; + redisNixSnapshotter = pkgs.nix-snapshotter.buildImage { + name = "ghcr.io/nix-snapshotter/redis"; + tag = "latest"; + config = { + Entrypoint = [ "${pkgs.redis}/bin/redis-server" ]; + Cmd = [ "--protected-mode" "no" ]; + }; + }; - hello-nix = pkgs.nix-snapshotter.buildImage { - name = imageName; - tag = nixTag; - config.entrypoint = ["${pkgs.hello}/bin/hello"]; +in { + nodes = rec { + rootful = { + virtualisation.containerd = { + enable = true; + nixSnapshotterIntegration = true; }; - in { - # Setup local registry for testing `buildImage` and `copyToRegistry`. - services.dockerRegistry = { + services.nix-snapshotter = { enable = true; - listenAddress = registryHost; - port = registryPort; }; - environment.variables = { - HELLO_TARBALL = helloTarball; + services.preload-containerd = { + enable = true; + targets = [{ + archives = [ + helloDrvFile + redisDockerTools + redisNixSnapshotter + ]; + }]; }; - environment.systemPackages = [ - (hello-nix.copyToRegistry { plainHTTP = true; }) - pkgs.nerdctl + environment.systemPackages = with pkgs; [ + nerdctl + redis ]; }; - rootful = { - imports = [ - base - ]; - - virtualisation.containerd = { - enable = true; - nixSnapshotterIntegration = true; - }; + rootless = { + virtualisation.containerd.rootless = { + enable = true; + nixSnapshotterIntegration = true; + }; - services.nix-snapshotter = { - enable = true; - }; - }; + services.nix-snapshotter.rootless = { + enable = true; + }; - rootless = { - imports = [ - base - ]; + services.preload-containerd.rootless = { + enable = true; + targets = [{ + archives = [ + helloDrvFile + redisDockerTools + redisNixSnapshotter + ]; + address = "$XDG_RUNTIME_DIR/containerd/containerd.sock"; + }]; + }; - virtualisation.containerd.rootless = { - enable = true; - nixSnapshotterIntegration = true; - }; + environment.systemPackages = with pkgs; [ + nerdctl + redis + ]; - services.nix-snapshotter.rootless = { - enable = true; - }; + users.users.alice = { + uid = 1000; + isNormalUser = true; + }; - users.users.alice = { - uid = 1000; - isNormalUser = true; + environment.variables = { + XDG_RUNTIME_DIR = "/run/user/1000"; + }; }; - }; - external = { - imports = [ - rootful - ]; + external = { + imports = [ + rootful + ]; - virtualisation.containerd = { - settings.external_builder = pkgs.writeScript "external-builder.sh" '' - ${pkgs.nix}/bin/nix build --out-link $1 $2 - ''; + virtualisation.containerd = { + settings.external_builder = pkgs.writeScript "external-builder.sh" '' + ${pkgs.nix}/bin/nix build --out-link $1 $2 + ''; + }; }; }; -in { - nodes = { - inherit - rootful - rootless - external - ; - }; - - testScript = { nodes, ... }: + testScript = let - user = nodes.rootless.users.users.alice; - sudo_su = lib.concatStringsSep " " [ - "XDG_RUNTIME_DIR=/run/user/${toString user.uid}" - "sudo" "--preserve-env=XDG_RUNTIME_DIR" "-u" "alice" + "sudo" + "--preserve-env=XDG_RUNTIME_DIR,CONTAINERD_ADDRESS,CONTAINERD_SNAPSHOTTER" + "-u" + "alice" ]; in '' - def setup(machine): - machine.wait_for_unit("docker-registry.service") - machine.wait_for_open_port(${toString registryPort}) - machine.succeed("copy-to-registry") - def collect_coverage(machine): coverfiles = machine.succeed("ls /tmp/go-cover").split() for coverfile in coverfiles: machine.copy_from_vm(f"/tmp/go-cover/{coverfile}", f"build/go-cover/${config.name}-{machine.name}") - def teardown_rootful(machine): - machine.succeed("systemctl stop nix-snapshotter.service") - collect_coverage(machine) - - def teardown_rootless(machine, user = "alice"): - machine.succeed(f"systemctl --user --machine={user}@ stop nix-snapshotter.service") - collect_coverage(machine) - - def wait_for_user_unit(machine, service, user = "alice"): - machine.wait_until_succeeds(f"systemctl --user --machine={user}@ is-active {service}") - - def test_rootful(machine, name = "rootful"): - machine.wait_for_unit("nix-snapshotter.service") - machine.wait_for_unit("containerd.service") - - with subtest(f"{name}: Run regular container as root"): - machine.succeed("nerdctl load < $HELLO_TARBALL") - out = machine.succeed("nerdctl run --name hello ${regularImage}") - assert "Hello, world!" in out - machine.succeed("nerdctl ps -a | grep hello") - machine.succeed("nerdctl rm hello") - - with subtest(f"{name}: Run nix container as root"): - out = machine.succeed("nerdctl run --name hello ${nixImage}") + def wait_for_unit(machine, service, user = "alice"): + if "rootless" in machine.name: + machine.wait_until_succeeds(f"systemctl --user --machine={user}@ is-active {service}") + else: + machine.wait_for_unit(service) + + def stop_unit(machine, service, user = "alice"): + if "rootless" in machine.name: + machine.succeed(f"systemctl --user --machine={user}@ stop {service}") + else: + machine.succeed(f"systemctl stop {service}") + + def test(machine, sudo_su = ""): + wait_for_unit(machine, "nix-snapshotter.service") + wait_for_unit(machine, "containerd.service") + wait_for_unit(machine, "preload-containerd.service") + + with subtest(f"{machine.name}: Run container with an executable outPath"): + out = machine.succeed(f"{sudo_su} nerdctl run --rm ghcr.io/pdtpartners/hello-world") assert "Hello, world!" in out - machine.succeed("nerdctl ps -a | grep hello") - machine.succeed("nerdctl rm hello") - def test_rootless(machine, name = "rootless"): - machine.succeed("loginctl enable-linger alice") - wait_for_user_unit(machine, "nix-snapshotter.service") - wait_for_user_unit(machine, "containerd.service") + with subtest(f"{machine.name}: Run container with CNI built with pkgs.dockerTools.buildImage"): + machine.succeed(f"{sudo_su} nerdctl run -d -p 30000:6379 ghcr.io/docker-tools/redis") + out = machine.wait_until_succeeds(f"{sudo_su} redis-cli -p 30000 ping") + assert "PONG" in out - with subtest(f"{name}: Run regular container as user"): - machine.succeed("${sudo_su} nerdctl load < $HELLO_TARBALL") - out = machine.succeed("${sudo_su} nerdctl run --name hello ${regularImage}") - assert "Hello, world!" in out - machine.succeed("${sudo_su} nerdctl ps -a | grep hello") - machine.succeed("${sudo_su} nerdctl rm hello") - - # TODO: Currently rootless nerdctl cannot pull images from 127.0.0.1, - # because the pull operation occurs in rootlesskit's network namespace. - # - # With upcoming rootlesskit v2.0.0, there is a new flag `--detach-netns` - # that when enabled, mounts the new netns to $ROOTLESSKIT_STATE_DIR/netns - # and launches slirp4netns for that netns, but leaves containerd in host netns - # with unshared userns and mountns. - # - # See: - # - https://github.com/containerd/nerdctl/blob/main/docs/registry.md#accessing-127001-from-rootless-nerdctl - # - https://github.com/containerd/nerdctl/issues/814 - # - https://github.com/rootless-containers/rootlesskit/pull/379 - # with subtest(f"{name}: Run nix container as user"): - # out = machine.succeed("${sudo_su} nerdctl run --name hello ${nixImage}") - # assert "Hello, world!" in out - # machine.succeed("${sudo_su} nerdctl ps -a | grep hello") - # machine.succeed("${sudo_su} nerdctl rm hello") + with subtest(f"{machine.name}: Run container with CNI built with pkgs.nix-snapshotter.buildImage"): + machine.succeed(f"{sudo_su} nerdctl run -d -p 30001:6379 ghcr.io/nix-snapshotter/redis") + out = machine.wait_until_succeeds(f"{sudo_su} redis-cli -p 30001 ping") + assert "PONG" in out start_all() - setup(rootful) - test_rootful(rootful) - teardown_rootful(rootful) - - setup(rootless) - test_rootless(rootless) - teardown_rootless(rootless) - - setup(external) - test_rootful(external) - teardown_rootful(external) + test(rootful) + test(rootless, "${sudo_su}") + test(external) ''; } diff --git a/pkg/command/load.go b/pkg/command/load.go index 540b21f..0fd85e9 100644 --- a/pkg/command/load.go +++ b/pkg/command/load.go @@ -2,10 +2,8 @@ package command import ( "fmt" - "os" "github.com/containerd/containerd" - "github.com/containerd/containerd/content/local" "github.com/containerd/containerd/namespaces" "github.com/pdtpartners/nix-snapshotter/pkg/nix2container" cli "github.com/urfave/cli/v2" @@ -20,16 +18,6 @@ var loadCommand = &cli.Command{ return fmt.Errorf("must provide exactly 1 args") } - root, err := os.MkdirTemp(nix2container.TempDir(), "nix2container-load") - if err != nil { - return err - } - - store, err := local.NewStore(root) - if err != nil { - return err - } - client, err := containerd.New(c.String("address")) if err != nil { return err @@ -38,7 +26,7 @@ var loadCommand = &cli.Command{ archivePath := c.Args().Get(0) ctx := namespaces.WithNamespace(c.Context, c.String("namespace")) - _, err = nix2container.Load(ctx, client, store, archivePath) + _, err = nix2container.Load(ctx, client, archivePath) return err }, } diff --git a/pkg/nix/image_service.go b/pkg/nix/image_service.go index 3c6e2e3..4017c17 100644 --- a/pkg/nix/image_service.go +++ b/pkg/nix/image_service.go @@ -9,7 +9,6 @@ import ( "time" "github.com/containerd/containerd" - "github.com/containerd/containerd/content/local" "github.com/containerd/containerd/log" "github.com/containerd/containerd/namespaces" "github.com/pdtpartners/nix-snapshotter/pkg/nix2container" @@ -128,19 +127,9 @@ func (is *imageService) PullImage(ctx context.Context, req *runtime.PullImageReq return nil, err } - root, err := os.MkdirTemp(nix2container.TempDir(), "nix-snapshotter-pull") - if err != nil { - return nil, err - } - - store, err := local.NewStore(root) - if err != nil { - return nil, err - } - log.G(ctx).Info("[image-service] Loading nix image archive") ctx = namespaces.WithNamespace(ctx, "k8s.io") - img, err := nix2container.Load(ctx, is.client, store, archivePath) + img, err := nix2container.Load(ctx, is.client, archivePath) if err != nil { return nil, err } diff --git a/pkg/nix2container/generate.go b/pkg/nix2container/generate.go index 3e5bc94..ef1ceac 100644 --- a/pkg/nix2container/generate.go +++ b/pkg/nix2container/generate.go @@ -252,14 +252,20 @@ func writeNixClosureLayer(ctx context.Context, w io.Writer, nixStorePaths, copyT } relStorePath := filepath.Join(root, nixStorePath) - if !fi.IsDir() { - relStorePath = filepath.Dir(relStorePath) - } + if fi.IsDir() { + err = os.MkdirAll(relStorePath, 0o755) + } else { + err = os.MkdirAll(filepath.Dir(relStorePath), 0o755) + if err != nil { + return "", err + } - err = os.MkdirAll(relStorePath, 0o755) + err = os.WriteFile(relStorePath, nil, 0o555) + } if err != nil { return "", err } + } // For each copyToRoot, walk the store path locally and create a symlink for @@ -280,6 +286,10 @@ func writeNixClosureLayer(ctx context.Context, w io.Writer, nixStorePaths, copyT return os.MkdirAll(rootPath, 0o755) } + if rootPath == root { + return fmt.Errorf("copyToRoot expected to be directory but got %s", copyToRoot) + } + return os.Symlink(path, rootPath) }) if err != nil { diff --git a/pkg/nix2container/generate_test.go b/pkg/nix2container/generate_test.go index 4a96fcf..25d1bf6 100644 --- a/pkg/nix2container/generate_test.go +++ b/pkg/nix2container/generate_test.go @@ -124,91 +124,95 @@ func TestWriteNixClosureLayer(t *testing.T) { }, { "file", - []string{"$TEST_DIR/test.file"}, - []string{"$TEST_DIR/"}, - []string{"$TEST_DIR_EXPAND", "/test.file"}, + []string{"$TEST_DIR/test-file"}, + []string{}, + []string{"$TEST_DIR_EXPAND", "$TEST_DIR/test-file"}, }, { - "file_with_dir", - []string{"$TEST_DIR/dir/test.file"}, - []string{"$TEST_DIR/"}, + "file_with_dirs", + []string{"$TEST_DIR/test-dir/bin/test-file"}, + []string{}, []string{ "$TEST_DIR_EXPAND", - "$TEST_DIR/dir/", - "/dir/", - "/dir/test.file"}, + "$TEST_DIR/test-dir/", + "$TEST_DIR/test-dir/bin/", + "$TEST_DIR/test-dir/bin/test-file", + }, }, { - "file_with_long_dir", - []string{"$TEST_DIR/dir/that/is/long/test.file"}, - []string{"$TEST_DIR/"}, + "file_with_copy_to_root", + []string{"$TEST_DIR/test-dir1/bin/test-file1", "$TEST_DIR/test-dir2/bin/test-file2"}, + []string{"$TEST_DIR/test-dir1/"}, []string{ "$TEST_DIR_EXPAND", - "$TEST_DIR/dir/", - "$TEST_DIR/dir/that/", - "$TEST_DIR/dir/that/is/", - "$TEST_DIR/dir/that/is/long/", - "/dir/", - "/dir/that/", - "/dir/that/is/", - "/dir/that/is/long/", - "/dir/that/is/long/test.file"}, - }, - { - "file_with_copy_to_root", - []string{"$TEST_DIR/dir/test.file"}, - []string{"$TEST_DIR/dir/"}, - []string{"$TEST_DIR_EXPAND", "$TEST_DIR/dir/", "/test.file"}, + "$TEST_DIR/test-dir1/", + "$TEST_DIR/test-dir1/bin/", + "$TEST_DIR/test-dir1/bin/test-file1", + "$TEST_DIR/test-dir2/", + "$TEST_DIR/test-dir2/bin/", + "$TEST_DIR/test-dir2/bin/test-file2", + "/bin/", + "/bin/test-file1", + }, }, { "file_with_copy_to_root_and_no_trailing_slash", - []string{"$TEST_DIR/dir/test.file"}, - []string{"$TEST_DIR/dir"}, - []string{"$TEST_DIR_EXPAND", "$TEST_DIR/dir/", "/test.file"}, + []string{"$TEST_DIR/test-dir/bin/test-file"}, + []string{"$TEST_DIR/test-dir"}, + []string{ + "$TEST_DIR_EXPAND", + "$TEST_DIR/test-dir/", + "$TEST_DIR/test-dir/bin/", + "$TEST_DIR/test-dir/bin/test-file", + "/bin/", + "/bin/test-file", + }, }, { "multiple_files", - []string{"$TEST_DIR/dir/test_1.file", "$TEST_DIR/dir/test_2.file"}, - []string{"$TEST_DIR/dir/"}, + []string{"$TEST_DIR/test-dir/bin/test-file1", "$TEST_DIR/test-dir/bin/test-file2"}, + []string{"$TEST_DIR/test-dir/"}, []string{ "$TEST_DIR_EXPAND", - "$TEST_DIR/dir/", - "/test_1.file", - "/test_2.file"}, + "$TEST_DIR/test-dir/", + "$TEST_DIR/test-dir/bin/", + "$TEST_DIR/test-dir/bin/test-file1", + "$TEST_DIR/test-dir/bin/test-file2", + "/bin/", + "/bin/test-file1", + "/bin/test-file2"}, }, { "multiple_files_on_different_levels", - []string{"$TEST_DIR/dir/test_1.file", "$TEST_DIR/test_2.file"}, - []string{"$TEST_DIR/"}, + []string{"$TEST_DIR/test-dir/bin/test-file", "$TEST_DIR/test-file"}, + []string{"$TEST_DIR/test-dir/"}, []string{ "$TEST_DIR_EXPAND", - "$TEST_DIR/dir/", - "/dir/", - "/dir/test_1.file", - "/test_2.file"}, + "$TEST_DIR/test-dir/", + "$TEST_DIR/test-dir/bin/", + "$TEST_DIR/test-dir/bin/test-file", + "$TEST_DIR/test-file", + "/bin/", + "/bin/test-file", + }, }, { "multiple_copy_to_roots", - []string{"$TEST_DIR/dir/test.file"}, - []string{"$TEST_DIR/", "$TEST_DIR/dir/"}, + []string{"$TEST_DIR/test-dir1/bin/test-file", "$TEST_DIR/test-dir2/share/test-file"}, + []string{"$TEST_DIR/test-dir1", "$TEST_DIR/test-dir2/"}, []string{ "$TEST_DIR_EXPAND", - "$TEST_DIR/dir/", - "/dir/", - "/dir/test.file", - "/test.file"}, - }, - { - "ignore_file_below_copy_to_root", - []string{"$TEST_DIR/dir/test_1.file", "$TEST_DIR/test_2.file"}, - []string{"$TEST_DIR/dir/"}, - []string{"$TEST_DIR_EXPAND", "$TEST_DIR/dir/", "/test_1.file"}, - }, - { - "no_copy_to_roots", - []string{"$TEST_DIR/dir/test.file"}, - []string{}, - []string{"$TEST_DIR_EXPAND", "$TEST_DIR/dir/"}, + "$TEST_DIR/test-dir1/", + "$TEST_DIR/test-dir1/bin/", + "$TEST_DIR/test-dir1/bin/test-file", + "$TEST_DIR/test-dir2/", + "$TEST_DIR/test-dir2/share/", + "$TEST_DIR/test-dir2/share/test-file", + "/bin/", + "/bin/test-file", + "/share/", + "/share/test-file", + }, }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/nix2container/load.go b/pkg/nix2container/load.go index ab5adcc..332d337 100644 --- a/pkg/nix2container/load.go +++ b/pkg/nix2container/load.go @@ -1,61 +1,49 @@ package nix2container import ( - "bytes" "context" - "encoding/json" "fmt" "os" + "path/filepath" "github.com/containerd/containerd" - "github.com/containerd/containerd/content" - "github.com/containerd/containerd/errdefs" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/images/archive" "github.com/containerd/containerd/log" + "github.com/containerd/containerd/pkg/transfer" tarchive "github.com/containerd/containerd/pkg/transfer/archive" "github.com/containerd/containerd/pkg/transfer/image" "github.com/containerd/containerd/platforms" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -func Load(ctx context.Context, client *containerd.Client, store content.Store, archivePath string) (containerd.Image, error) { - dt, err := os.ReadFile(archivePath) +func Load(ctx context.Context, client *containerd.Client, archivePath string) (containerd.Image, error) { + log.G(ctx).WithField("archive", archivePath).Info("Loading archive") + f, err := os.Open(archivePath) if err != nil { return nil, err } - src := tarchive.NewImageImportStream(bytes.NewReader(dt), ocispec.MediaTypeImageIndex) + defer f.Close() + + src := tarchive.NewImageImportStream(f, "") + prefix := fmt.Sprintf("import-%s", filepath.Base(archivePath)) platSpec := platforms.DefaultSpec() + storeOpts := []image.StoreOpt{ - image.WithUnpack(platSpec, "nix"), + image.WithNamedPrefix(prefix, true), image.WithPlatforms(platSpec), + image.WithUnpack(platSpec, "nix"), } - target, err := archive.ImportIndex(ctx, store, bytes.NewReader(dt)) - if err != nil { - return nil, err - } - - ref, err := refFromArchive(ctx, store, target) - if err != nil { - return nil, err - } - - dest := image.NewStore(ref, storeOpts...) + dest := image.NewStore("", storeOpts...) - // pf, done := images.ProgressHandler(ctx, os.Stderr) - // defer done() - - log.G(ctx).WithField("ref", ref).Info("Importing image") - err = client.Transfer(ctx, src, dest) - if err != nil { - return nil, err + var ref string + progressFunc := func(p transfer.Progress) { + if p.Event == "saved" { + ref = p.Name + } } - log.G(ctx).WithField("ref", ref).Info("Creating image") - img := images.Image{Name: ref, Target: target} - _, err = createImage(ctx, client.ImageService(), img) + log.G(ctx).Info("Importing image") + err = client.Transfer(ctx, src, dest, transfer.WithProgress(progressFunc)) if err != nil { return nil, err } @@ -63,47 +51,3 @@ func Load(ctx context.Context, client *containerd.Client, store content.Store, a log.G(ctx).WithField("ref", ref).Info("Created image") return client.GetImage(ctx, ref) } - -func refFromArchive(ctx context.Context, store content.Store, target ocispec.Descriptor) (ref string, err error) { - blob, err := content.ReadBlob(ctx, store, target) - if err != nil { - return - } - - var idx ocispec.Index - if err = json.Unmarshal(blob, &idx); err != nil { - return - } - - if len(idx.Manifests) != 1 { - return "", fmt.Errorf("OCI index had %d manifests", len(idx.Manifests)) - } - - mfst := idx.Manifests[0] - return mfst.Annotations[images.AnnotationImageName], nil -} - -func createImage(ctx context.Context, store images.Store, img images.Image) (images.Image, error) { - for { - if created, err := store.Create(ctx, img); err != nil { - if !errdefs.IsAlreadyExists(err) { - return images.Image{}, err - } - - updated, err := store.Update(ctx, img) - if err != nil { - // if image was removed, try create again - if errdefs.IsNotFound(err) { - continue - } - return images.Image{}, err - } - - img = updated - } else { - img = created - } - - return img, nil - } -}