Enable to distribute images on IPFS

Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>
This commit is contained in:
Kohei Tokunaga 2021-11-09 16:28:55 +09:00
parent f316402913
commit 2ae16dea47
29 changed files with 1676 additions and 62 deletions

View File

@ -52,7 +52,7 @@ jobs:
test-integration:
runs-on: ubuntu-20.04
timeout-minutes: 20
timeout-minutes: 30
strategy:
matrix:
containerd: [1.4.5, 1.5.7, 1.6.0-beta.2]
@ -77,7 +77,7 @@ jobs:
test-integration-rootless:
runs-on: ubuntu-20.04
timeout-minutes: 20
timeout-minutes: 30
strategy:
matrix:
containerd: [1.4.5, 1.5.7, 1.6.0-beta.2]
@ -100,7 +100,7 @@ jobs:
cross:
runs-on: ubuntu-20.04
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/setup-go@v2
with:
@ -129,7 +129,7 @@ jobs:
test-integration-cgroup2:
# nested virtualization is only available on macOS hosts
runs-on: macos-10.15
timeout-minutes: 40
timeout-minutes: 50
env:
VAGRANT_VAGRANTFILE: hack/Vagrantfile.fedora
steps:

View File

@ -34,6 +34,8 @@ ARG SLIRP4NETNS_VERSION=1.1.12
# Extra deps: FUSE-OverlayFS
ARG FUSE_OVERLAYFS_VERSION=1.7.1
ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=1.0.3
# Extra deps: IPFS
ARG IPFS_VERSION=0.10.0
# Test deps
ARG GO_VERSION=1.17
@ -161,6 +163,15 @@ RUN fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION}-${TARG
tar xzf "${fname}" -C /out/bin && \
rm -f "${fname}" && \
echo "- containerd-fuse-overlayfs: v${CONTAINERD_FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG IPFS_VERSION
RUN fname="go-ipfs_v${IPFS_VERSION}_${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
curl -o "${fname}" -fSL "https://github.com/ipfs/go-ipfs/releases/download/v${IPFS_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/go-ipfs-${IPFS_VERSION}" | sha512sum -c && \
tmpout=$(mktemp -d) && \
tar -C ${tmpout} -xzf "${fname}" go-ipfs/ipfs && \
mv ${tmpout}/go-ipfs/ipfs /out/bin/ && \
echo "- IPFS: v${IPFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \
echo "## License" >> /out/share/doc/nerdctl-full/README.md && \
echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/v${SLIRP4NETNS_VERSION}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \
@ -213,6 +224,14 @@ COPY . /go/src/github.com/containerd/nerdctl
WORKDIR /go/src/github.com/containerd/nerdctl
VOLUME /tmp
ENV CGO_ENABLED=0
# enable offline ipfs for integration test
COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml
COPY ./Dockerfile.d/test-integration-ipfs-offline.service /usr/local/lib/systemd/system/
# install ipfs service. avoid using 5001(api)/8080(gateway) which are reserved by tests.
RUN systemctl enable test-integration-ipfs-offline && \
ipfs init && \
ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888" && \
ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889"
CMD ["go", "test", "-v", "./cmd/nerdctl/..."]
FROM test-integration AS test-integration-rootless
@ -231,6 +250,8 @@ RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N '' && \
cp -a /root/.ssh/id_rsa.pub /home/rootless/.ssh/authorized_keys && \
mkdir -p /home/rootless/.local/share && \
chown -R rootless:rootless /home/rootless
# ipfs daemon for rootless containerd will be enabled in /test-integration-rootless.sh
RUN systemctl disable test-integration-ipfs-offline
VOLUME /home/rootless/.local/share
RUN go test -o /usr/local/bin/nerdctl.test -c ./cmd/nerdctl
COPY ./Dockerfile.d/test-integration-rootless.sh /

View File

@ -0,0 +1,3 @@
# From https://github.com/ipfs/go-ipfs/releases
519d912be9367b8d5de75866aab1989954018ee1d414733ce0283735f806decb4fd11cffcb42f1beb638348a05cae95e3271c5121132e9e8e6b91ad0bfb1fda4 go-ipfs_v0.10.0_linux-amd64.tar.gz
82a9b7a3e8701b982daa9ffbb9565ee725b50dc367a4358ab2a67e97a3b8b29b48d6c607e7edb54608181171dd9b4eded5b88bc4018b4dc348bac64de0edd26a go-ipfs_v0.10.0_linux-arm64.tar.gz

View File

@ -0,0 +1,2 @@
# Enable IPFS
ipfs = true

View File

@ -0,0 +1,8 @@
[Unit]
Description=ipfs daemon for integration test (offline)
[Service]
ExecStart=ipfs daemon --init --offline
[Install]
WantedBy=docker-entrypoint.target

View File

@ -21,5 +21,17 @@ if [[ "$(id -u)" = "0" ]]; then
else
containerd-rootless-setuptool.sh install
containerd-rootless-setuptool.sh install-buildkit
containerd-rootless-setuptool.sh install-stargz
cat <<EOF >> /home/rootless/.config/containerd/config.toml
[proxy_plugins]
[proxy_plugins."stargz"]
type = "snapshot"
address = "/run/user/1000/containerd-stargz-grpc/containerd-stargz-grpc.sock"
EOF
systemctl --user restart containerd.service
containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing
echo "ipfs = true" >> /home/rootless/.config/containerd-stargz-grpc/config.toml
systemctl --user restart stargz-snapshotter.service
export IPFS_PATH="/home/rootless/.local/share/ipfs"
exec "$@"
fi

View File

@ -281,6 +281,8 @@ Run a command in a new container.
Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IFPS. See [`/docs/ipfs.md`](./docs/ipfs.md) for details.
Basic flags:
- :whale: :window: `-i, --interactive`: Keep STDIN open even if not attached"
- :whale: :window: `-t, --tty`: Allocate a pseudo-TTY
@ -667,6 +669,8 @@ Pull an image from a registry.
Usage: `nerdctl pull [OPTIONS] NAME[:TAG|@DIGEST]`
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IFPS. See [`/docs/ipfs.md`](./docs/ipfs.md) for details.
Flags:
- :whale: `--platform=(amd64|arm64|...)`: Pull content for a specific platform
- :nerd_face: Unlike Docker, this flag can be specified multiple times (`--platform=amd64 --platform=arm64`)
@ -680,6 +684,8 @@ Push an image to a registry.
Usage: `nerdctl push [OPTIONS] NAME[:TAG]`
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to push it to IFPS. See [`/docs/ipfs.md`](./docs/ipfs.md) for details.
Flags:
- :nerd_face: `--platform=(amd64|arm64|...)`: Push content for a specific platform
- :nerd_face: `--all-platforms`: Push content for all platforms
@ -1110,3 +1116,4 @@ Others:
- [`./docs/freebsd.md`](./docs/freebsd.md): Running FreeBSD jails
- [`./docs/multi-platform.md`](./docs/multi-platform.md): Multi-platform mode
- [`./docs/experimental.md`](./docs/experimental.md): Experimental features
- [`./docs/ipfs.md`](./docs/ipfs.md): Distributing images on IPFS

View File

@ -21,9 +21,9 @@ import (
"errors"
"fmt"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/idutil/containerwalker"
"github.com/containerd/nerdctl/pkg/imgutil/commit"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/spf13/cobra"
)
@ -85,7 +85,7 @@ func commitAction(cmd *cobra.Command, args []string) error {
func newCommitOpts(cmd *cobra.Command, args []string) (*commit.Opts, error) {
rawRef := args[1]
named, err := refdocker.ParseDockerRef(rawRef)
named, err := referenceutil.ParseDockerRef(rawRef)
if err != nil {
return nil, err
}

View File

@ -24,10 +24,12 @@ import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/platforms"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/composer"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/ipfs"
"github.com/containerd/nerdctl/pkg/netutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
httpapi "github.com/ipfs/go-ipfs-http-client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
@ -145,11 +147,11 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo
}
o.ImageExists = func(ctx context.Context, rawRef string) (bool, error) {
named, err := refdocker.ParseDockerRef(rawRef)
refNamed, err := referenceutil.ParseAny(rawRef)
if err != nil {
return false, err
}
ref := named.String()
ref := refNamed.String()
if _, err := client.ImageService().Get(ctx, ref); err != nil {
if errors.Is(err, errdefs.ErrNotFound) {
return false, nil
@ -168,8 +170,18 @@ func getComposer(cmd *cobra.Command, client *containerd.Client) (*composer.Compo
}
ocispecPlatforms = []ocispec.Platform{parsed} // no append
}
_, imgErr := imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), snapshotter, imageName,
pullMode, insecure, ocispecPlatforms, nil)
var imgErr error
if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(imageName); err == nil {
ipfsClient, err := httpapi.NewLocalApi()
if err != nil {
return err
}
_, imgErr = ipfs.EnsureImage(ctx, client, ipfsClient, cmd.OutOrStdout(), snapshotter, scheme, ref,
pullMode, ocispecPlatforms, nil)
} else {
_, imgErr = imgutil.EnsureImage(ctx, client, cmd.OutOrStdout(), snapshotter, imageName,
pullMode, insecure, ocispecPlatforms, nil)
}
return imgErr
}

View File

@ -19,6 +19,7 @@ package main
import (
"fmt"
"io"
"os"
"strings"
"testing"
"time"
@ -30,8 +31,7 @@ import (
func TestComposeUp(t *testing.T) {
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
testComposeUp(t, base, fmt.Sprintf(`
version: '3.1'
services:
@ -63,7 +63,82 @@ services:
volumes:
wordpress:
db:
`, testutil.WordpressImage, testutil.MariaDBImage)
`, testutil.WordpressImage, testutil.MariaDBImage))
}
func TestIPFSComposeUp(t *testing.T) {
requiresIPFS(t)
testutil.DockerIncompatible(t)
tests := []struct {
name string
snapshotter string
pushOptions []string
requiresStargz bool
}{
{
name: "overlayfs",
snapshotter: "overlayfs",
},
{
name: "stargz",
snapshotter: "stargz",
pushOptions: []string{"--estargz"},
requiresStargz: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
base := testutil.NewBase(t)
if tt.requiresStargz {
requiresStargz(base)
}
ipfsImgs := make([]string, 2)
for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} {
ipfsImgs[i] = pushImageToIPFS(t, base, img, tt.pushOptions...)
}
base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER="+tt.snapshotter)
testComposeUp(t, base, fmt.Sprintf(`
version: '3.1'
services:
wordpress:
image: %s
restart: always
ports:
- 8080: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
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
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]))
})
}
}
func testComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string) {
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()

View File

@ -25,8 +25,8 @@ import (
"github.com/containerd/containerd/images/converter"
"github.com/containerd/containerd/images/converter/uncompress"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/stargz-snapshotter/estargz"
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
"github.com/containerd/stargz-snapshotter/recorder"
@ -90,13 +90,13 @@ func imageConvertAction(cmd *cobra.Command, args []string) error {
return errors.New("src and target image need to be specified")
}
srcNamed, err := refdocker.ParseDockerRef(srcRawRef)
srcNamed, err := referenceutil.ParseAny(srcRawRef)
if err != nil {
return err
}
srcRef := srcNamed.String()
targetNamed, err := refdocker.ParseDockerRef(targetRawRef)
targetNamed, err := referenceutil.ParseDockerRef(targetRawRef)
if err != nil {
return err
}

View File

@ -23,10 +23,10 @@ import (
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images/converter"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/imgcrypt/images/encryption"
"github.com/containerd/imgcrypt/images/encryption/parsehelpers"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
)
@ -105,13 +105,13 @@ func getImgcryptAction(encrypt bool) func(cmd *cobra.Command, args []string) err
return errors.New("src and target image need to be specified")
}
srcNamed, err := refdocker.ParseDockerRef(srcRawRef)
srcNamed, err := referenceutil.ParseAny(srcRawRef)
if err != nil {
return err
}
srcRef := srcNamed.String()
targetNamed, err := refdocker.ParseDockerRef(targetRawRef)
targetNamed, err := referenceutil.ParseDockerRef(targetRawRef)
if err != nil {
return err
}

View File

@ -34,10 +34,10 @@ import (
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/pkg/progress"
"github.com/containerd/containerd/platforms"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/snapshots"
"github.com/containerd/nerdctl/pkg/formatter"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/opencontainers/image-spec/identity"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
@ -79,7 +79,7 @@ func imagesAction(cmd *cobra.Command, args []string) error {
}
if len(args) > 0 {
canonicalRef, err := refdocker.ParseDockerRef(args[0])
canonicalRef, err := referenceutil.ParseAny(args[0])
if err != nil {
return err
}

View File

@ -21,8 +21,11 @@ import (
"os"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/ipfs"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/nerdctl/pkg/strutil"
httpapi "github.com/ipfs/go-ipfs-http-client"
"github.com/spf13/cobra"
)
@ -30,7 +33,7 @@ import (
func newPullCommand() *cobra.Command {
var pullCommand = &cobra.Command{
Use: "pull",
Short: "Pull an image from a registry",
Short: "Pull an image from a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS.",
RunE: pullAction,
SilenceUsage: true,
SilenceErrors: true,
@ -88,6 +91,17 @@ func pullAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(args[0]); err == nil {
ipfsClient, err := httpapi.NewLocalApi()
if err != nil {
return err
}
_, err = ipfs.EnsureImage(ctx, client, ipfsClient, os.Stdout, snapshotter, scheme, ref,
"always", ocispecPlatforms, unpack)
return err
}
_, err = imgutil.EnsureImage(ctx, client, os.Stdout, snapshotter, args[0],
"always", insecure, ocispecPlatforms, unpack)
return err

View File

@ -17,16 +17,27 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images/converter"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
"github.com/containerd/nerdctl/pkg/imgutil/push"
"github.com/containerd/nerdctl/pkg/ipfs"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/containerd/stargz-snapshotter/estargz/zstdchunked"
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
httpapi "github.com/ipfs/go-ipfs-http-client"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -34,7 +45,7 @@ import (
func newPushCommand() *cobra.Command {
var pushCommand = &cobra.Command{
Use: "push NAME[:TAG]",
Short: "Push an image or a repository to a registry",
Short: "Push an image or a repository to a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to push image to IPFS.",
RunE: pushAction,
ValidArgsFunction: pushShellComplete,
SilenceUsage: true,
@ -47,6 +58,9 @@ func newPushCommand() *cobra.Command {
pushCommand.Flags().Bool("all-platforms", false, "Push content for all platforms")
// #endregion
pushCommand.PersistentFlags().Bool("estargz", false, "Convert the image into eStargz")
pushCommand.PersistentFlags().Bool("ipfs-ensure-image", true, "Ensure the entire contents of the image is locally available before push")
return pushCommand
}
@ -55,17 +69,7 @@ func pushAction(cmd *cobra.Command, args []string) error {
return errors.New("image name needs to be specified")
}
rawRef := args[0]
named, err := refdocker.ParseDockerRef(rawRef)
if err != nil {
return err
}
ref := named.String()
refDomain := refdocker.Domain(named)
insecure, err := cmd.Flags().GetBool("insecure-registry")
if err != nil {
return err
}
client, ctx, cancel, err := newClient(cmd)
if err != nil {
return err
@ -80,28 +84,80 @@ func pushAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
convertEStargz, err := cmd.Flags().GetBool("estargz")
if err != nil {
return err
}
if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil {
if scheme != "ipfs" {
return fmt.Errorf("ipfs scheme is only supported but got %q", scheme)
}
ensureImage, err := cmd.Flags().GetBool("ipfs-ensure-image")
if err != nil {
return err
}
logrus.Infof("pushing image %q to IPFS", ref)
ipfsClient, err := httpapi.NewLocalApi()
if err != nil {
return err
}
var layerConvert converter.ConvertFunc
if convertEStargz {
layerConvert = eStargzConvertFunc()
}
c, err := ipfs.Push(ctx, client, ipfsClient, ref, layerConvert, allPlatforms, platform, ensureImage)
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), c.String())
return err
}
named, err := refdocker.ParseDockerRef(rawRef)
if err != nil {
return err
}
ref := named.String()
refDomain := refdocker.Domain(named)
insecure, err := cmd.Flags().GetBool("insecure-registry")
if err != nil {
return err
}
platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
if err != nil {
return err
}
platRef := ref
pushRef := ref
if !allPlatforms {
platRef = ref + "-tmp-reduced-platform"
pushRef = ref + "-tmp-reduced-platform"
// Push fails with "400 Bad Request" when the manifest is multi-platform but we do not locally have multi-platform blobs.
// So we create a tmp reduced-platform image to avoid the error.
platImg, err := converter.Convert(ctx, client, platRef, ref, converter.WithPlatform(platMC))
platImg, err := converter.Convert(ctx, client, pushRef, ref, converter.WithPlatform(platMC))
if err != nil {
if len(platform) == 0 {
return fmt.Errorf("failed to create a tmp single-platform image %q: %w", platRef, err)
return fmt.Errorf("failed to create a tmp single-platform image %q: %w", pushRef, err)
}
return fmt.Errorf("failed to create a tmp reduced-platform image %q (platform=%v): %w", platRef, platform, err)
return fmt.Errorf("failed to create a tmp reduced-platform image %q (platform=%v): %w", pushRef, platform, err)
}
defer client.ImageService().Delete(ctx, platImg.Name)
logrus.Infof("pushing as a reduced-platform image (%s, %s)", platImg.Target.MediaType, platImg.Target.Digest)
}
if convertEStargz {
pushRef = ref + "-tmp-esgz"
esgzImg, err := converter.Convert(ctx, client, pushRef, ref, converter.WithPlatform(platMC), converter.WithLayerConvertFunc(eStargzConvertFunc()))
if err != nil {
return fmt.Errorf("failed to convert to eStargz: %v", err)
}
defer client.ImageService().Delete(ctx, esgzImg.Name)
logrus.Infof("pushing as an eStargz image (%s, %s)", esgzImg.Target.MediaType, esgzImg.Target.Digest)
}
pushFunc := func(r remotes.Resolver) error {
return push.Push(ctx, client, r, cmd.OutOrStdout(), platRef, ref, platMC)
return push.Push(ctx, client, r, cmd.OutOrStdout(), pushRef, ref, platMC)
}
var dOpts []dockerconfigresolver.Opt
@ -138,3 +194,44 @@ func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]
// show image names
return shellCompleteImageNames(cmd)
}
func eStargzConvertFunc() converter.ConvertFunc {
convertToESGZ := estargzconvert.LayerConvertFunc()
return func(ctx context.Context, cs content.Store, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
if isReusableESGZ(ctx, cs, desc) {
logrus.Infof("reusing estargz %s without conversion", desc.Digest)
return nil, nil
}
newDesc, err := convertToESGZ(ctx, cs, desc)
if err != nil {
return nil, err
}
logrus.Infof("converted %q to %s", desc.MediaType, newDesc.Digest)
return newDesc, err
}
}
func isReusableESGZ(ctx context.Context, cs content.Store, desc ocispec.Descriptor) bool {
dgstStr, ok := desc.Annotations[estargz.TOCJSONDigestAnnotation]
if !ok {
return false
}
tocdgst, err := digest.Parse(dgstStr)
if err != nil {
return false
}
ra, err := cs.ReaderAt(ctx, desc)
if err != nil {
return false
}
defer ra.Close()
r, err := estargz.Open(io.NewSectionReader(ra, 0, desc.Size), estargz.WithDecompressors(new(zstdchunked.Decompressor)))
if err != nil {
return false
}
if _, err := r.VerifyTOC(tocdgst); err != nil {
return false
}
return true
}

View File

@ -101,7 +101,7 @@ func newTestInsecureRegistry(base *testutil.Base, name, user, pass string) *test
// listen on 0.0.0.0 to enable 127.0.0.1
listenIP := net.ParseIP("0.0.0.0")
const listenPort = 5000 // TODO: choose random empty port
const authPort = 5001 // TODO: choose random empty port
const authPort = 5100 // TODO: choose random empty port
base.T.Logf("hostIP=%q, listenIP=%q, listenPort=%d, authPort=%d", hostIP, listenIP, listenPort, authPort)
registryCert, registryKey, registryClose := generateTestCert(base, hostIP.String())
@ -116,7 +116,7 @@ func newTestInsecureRegistry(base *testutil.Base, name, user, pass string) *test
authConfigFileName := authConfigFile.Name()
_, err = authConfigFile.Write([]byte(fmt.Sprintf(`
server:
addr: ":5001"
addr: ":5100"
certificate: "/auth/domain.crt"
key: "/auth/domain.key"
token:
@ -135,7 +135,7 @@ acl:
authContainerName := "auth-" + name
cmd := base.Cmd("run",
"-d",
"-p", fmt.Sprintf("%s:%d:5001", listenIP, authPort),
"-p", fmt.Sprintf("%s:%d:5100", listenIP, authPort),
"--name", authContainerName,
"-v", authCert+":/auth/domain.crt",
"-v", authKey+":/auth/domain.key",

View File

@ -44,6 +44,7 @@ import (
"github.com/containerd/nerdctl/pkg/dnsutil/hostsstore"
"github.com/containerd/nerdctl/pkg/idgen"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/ipfs"
"github.com/containerd/nerdctl/pkg/labels"
"github.com/containerd/nerdctl/pkg/logging"
"github.com/containerd/nerdctl/pkg/mountutil"
@ -52,11 +53,13 @@ import (
"github.com/containerd/nerdctl/pkg/netutil/nettype"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/portutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/nerdctl/pkg/resolvconf"
"github.com/containerd/nerdctl/pkg/rootlessutil"
"github.com/containerd/nerdctl/pkg/strutil"
"github.com/containerd/nerdctl/pkg/taskutil"
"github.com/docker/cli/opts"
httpapi "github.com/ipfs/go-ipfs-http-client"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
@ -64,7 +67,7 @@ import (
)
func newRunCommand() *cobra.Command {
shortHelp := "Run a command in a new container"
shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS."
longHelp := shortHelp
switch runtime.GOOS {
case "windows":
@ -711,10 +714,21 @@ func generateRootfsOpts(ctx context.Context, client *containerd.Client, platform
if err != nil {
return nil, nil, nil, err
}
ensured, err = imgutil.EnsureImage(ctx, client, os.Stdout, snapshotter, args[0],
pull, insecureRegistry, ocispecPlatforms, nil)
if err != nil {
return nil, nil, nil, err
if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(args[0]); err == nil {
ipfsClient, err := httpapi.NewLocalApi()
if err != nil {
return nil, nil, nil, err
}
ensured, err = ipfs.EnsureImage(ctx, client, ipfsClient, os.Stdout, snapshotter, scheme, ref, pull, ocispecPlatforms, nil)
if err != nil {
return nil, nil, nil, err
}
} else {
ensured, err = imgutil.EnsureImage(ctx, client, os.Stdout, snapshotter, args[0],
pull, insecureRegistry, ocispecPlatforms, nil)
if err != nil {
return nil, nil, nil, err
}
}
}
var (

View File

@ -28,10 +28,126 @@ import (
"strings"
"testing"
"github.com/containerd/nerdctl/pkg/infoutil"
"github.com/containerd/nerdctl/pkg/rootlessutil"
"github.com/containerd/nerdctl/pkg/testutil"
"github.com/ipfs/go-cid"
httpapi "github.com/ipfs/go-ipfs-http-client"
"gotest.tools/v3/assert"
)
func TestIPFS(t *testing.T) {
requiresIPFS(t)
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage)
base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
base.Cmd("pull", ipfsCID).AssertOK()
base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK()
// encryption
keyPair := newJWEKeyPair(t)
defer keyPair.cleanup()
encryptImageRef := "newimg: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())
rmiAll(base)
decryptImageRef := "newimg: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 TestIPFSCommit(t *testing.T) {
requiresIPFS(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(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
base.Cmd("pull", ipfsCID).AssertOK()
base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK()
newContainer, newImg := "hello", "helloimg:v1"
base.Cmd("run", "--name", "hello", "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK()
base.Cmd("commit", newContainer, newImg).AssertOK()
base.Cmd("stop", newContainer).AssertOK()
base.Cmd("rm", newContainer).AssertOK()
ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines())
rmiAll(base)
base.Cmd("pull", ipfsCID2).AssertOK()
base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK()
}
func TestIPFSWithLazyPulling(t *testing.T) {
requiresIPFS(t)
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz")
base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz")
base.Cmd("pull", ipfsCID).AssertOK()
base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK()
}
func TestIPFSWithLazyPullingCommit(t *testing.T) {
requiresIPFS(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, "--estargz")
base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz")
base.Cmd("pull", ipfsCID).AssertOK()
base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK()
newContainer, newImg := "hello", "helloimg:v1"
base.Cmd("run", "--name", "hello", "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK()
base.Cmd("commit", newContainer, newImg).AssertOK()
base.Cmd("stop", newContainer).AssertOK()
base.Cmd("rm", newContainer).AssertOK()
ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines())
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).AssertOK()
return ipfsCID
}
func cidOf(t *testing.T, lines []string) string {
assert.Equal(t, len(lines) >= 2, true)
c, err := cid.Decode(lines[len(lines)-2])
assert.NilError(t, err)
return "ipfs://" + c.String()
}
func requiresIPFS(t *testing.T) {
if _, err := httpapi.NewLocalApi(); err != nil {
t.Skipf("test requires ipfs daemon, but got: %v", err)
}
return
}
func TestRunEntrypointWithBuild(t *testing.T) {
testutil.RequiresBuild(t)
base := testutil.NewBase(t)

View File

@ -22,8 +22,8 @@ import (
"os"
"github.com/containerd/containerd/images/archive"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
@ -110,7 +110,7 @@ func saveImage(images []string, out io.Writer, saveOpts []archive.ExportOpt, cmd
imageStore := client.ImageService()
for _, img := range images {
named, err := refdocker.ParseDockerRef(img)
named, err := referenceutil.ParseAny(img)
if err != nil {
return err
}

View File

@ -21,8 +21,8 @@ import (
"fmt"
"github.com/containerd/containerd/errdefs"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/spf13/cobra"
)
@ -70,7 +70,7 @@ func tagAction(cmd *cobra.Command, args []string) error {
return fmt.Errorf("%s: not found", args[0])
}
target, err := refdocker.ParseDockerRef(args[1])
target, err := referenceutil.ParseDockerRef(args[1])
if err != nil {
return err
}

View File

@ -6,3 +6,4 @@ The following features are experimental and subject to change:
- [FreeBSD containers](./freebsd.md)
- Importing an external eStargz record JSON file with `nerdctl image convert --estargz-record-in=FILE` .
eStargz itself is out of experimental.
- [Image Distribution on IPFS](./ipfs.md)

170
docs/ipfs.md Normal file
View File

@ -0,0 +1,170 @@
# Distribute Container Images on IPFS (Experimental)
You can distribute container images without registries, using IPFS.
## Prerequisites
To use this feature, make sure `ipfs daemon` is running on your host.
For example, you can run an IPFS daemon using the following command.
```
ipfs daemon
```
In rootless mode, you need to install ipfs daemon using `containerd-rootless-setuptool.sh`.
```
containerd-rootless-setuptool.sh -- install-ipfs --init
```
:information_source: If you don't want IPFS to communicate with nodes on the internet, you can run IPFS daemon in offline mode using `--offline` flag or you can create a private IPFS network as described [here](https://github.com/containerd/stargz-snapshotter/blob/main/docs/ipfs.md#appendix-1-creating-ipfs-private-network).
## IPFS-enabled image and OCI Compatibility
Image distribution on IPFS is achieved by OCI-compatible *IPFS-enabled image format*.
nerdctl automatically converts an image to IPFS-enabled when necessary.
For example, when nerdctl pushes an image to IPFS, if that image isn't an IPFS-enabled one, it converts that image to the IPFS-enabled one.
Please see [the doc in stargz-snapshotter project](https://github.com/containerd/stargz-snapshotter/blob/v0.10.0/docs/ipfs.md) for details about IPFS-enabled image format.
## Using nerdctl with IPFS
nerdctl supports an image name prefix `ipfs://` to handle images on IPFS.
### `nerdctl push ipfs://<image-name>`
For `nerdctl push`, you can specify `ipfs://` prefix for arbitrary image names stored in containerd.
When this prefix is specified, nerdctl pushes that image to IPFS.
```console
> nerdctl push ipfs://ubuntu:20.04
INFO[0000] pushing image "ubuntu:20.04" to IPFS
INFO[0000] ensuring image contents
bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze
```
At last line of the output, the IPFS CID of the pushed image is printed.
You can use this CID to pull this image from IPFS.
You can also specify `--estargz` option to enable [eStargz-based lazy pulling](https://github.com/containerd/stargz-snapshotter/blob/v0.10.0/docs/ipfs.md) on IPFS.
Please see the later section for details.
```console
> nerdctl push --estargz ipfs://fedora:36
INFO[0000] pushing image "fedora:36" to IPFS
INFO[0000] ensuring image contents
INFO[0011] converted "application/vnd.docker.image.rootfs.diff.tar.gzip" to sha256:cd4be969f12ef45dee7270f3643f796364045edf94cfa9ef6744d91d5cdf2208
bafkreibp2ncujcia663uum25ustwvmyoguxqyzjnxnlhebhsgk2zowscye
```
### `nerdctl pull ipfs://<CID>` and `nerdctl run ipfs://<CID>`
You can pull an image from IPFS by specifying `ipfs://<CID>` where `CID` is the CID of the image.
```console
> nerdctl pull ipfs://bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze
bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:28bfa1fc6d491d3bee91bab451cab29c747e72917efacb0adc4e73faffe1f51c: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:f6eed19a2880f1000be1d46fb5d114d094a59e350f9d025580f7297c8d9527d5: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:7b1a6ab2e44dbac178598dabe7cff59bd67233dba0b27e4fbd1f9d4b3c877a54: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 1.2 s total: 27.2 M (22.7 MiB/s)
```
`nerdctl run` also supports the same image name syntax.
When specified, this command pulls the image from IPFS.
```console
> nerdctl run --rm -it ipfs://bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze echo hello
hello
```
You can also push that image to the container registry.
```
nerdctl tag ipfs://bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze ghcr.io/ktock/ubuntu:20.04-ipfs
nerdctl push ghcr.io/ktock/ubuntu:20.04-ipfs
```
The pushed image can run on other (IPFS-agnostic) runtimes.
```console
> docker run --rm -it ghcr.io/ktock/ubuntu:20.04-ipfs echo hello
hello
```
:information_source: Note that though the IPFS-enabled image is OCI compatible, some runtimes including [containerd](https://github.com/containerd/containerd/pull/6221) and [podman](https://github.com/containers/image/pull/1403) had bugs and failed to pull that image. Containerd has already fixed this.
### compose on IPFS
`nerdctl compose` supports same image name syntax to pull images from IPFS.
```yaml
version: "3.8"
services:
ubuntu:
image: ipfs://bafkreicq4dg6nkef5ju422ptedcwfz6kcvpvvhuqeykfrwq5krazf3muze
command: echo hello
```
### encryption
You can distribute [encrypted images](./ocicrypt.md) on IPFS using OCIcrypt.
Please see [`/docs/ocycrypt.md`](./ocicrypt.md) for details about how to ecrypt and decrypt an image.
Same as normal images, the encrypted image can be pushed to IPFS using `ipfs://` prefix.
```console
> nerdctl image encrypt --recipient=jwe:mypubkey.pem ubuntu:20.04 ubuntu:20.04-encrypted
sha256:a5c57411f3d11bb058b584934def0710c6c5b5a4a2d7e9b78f5480ecfc450740
> nerdctl push ipfs://ubuntu:20.04-encrypted
INFO[0000] pushing image "ubuntu:20.04-encrypted" to IPFS
INFO[0000] ensuring image contents
bafkreifajsysbvhtgd7fdgrfesszexdq6v5zbj5y2jnjfwxdjyqws2s3s4
```
You can pull the encrypted image from IPFS using `ipfs://` prefix and can decrypt it in the same way as described in [`/docs/ocycrypt.md`](./ocicrypt.md).
```console
> nerdctl pull --unpack=false ipfs://bafkreifajsysbvhtgd7fdgrfesszexdq6v5zbj5y2jnjfwxdjyqws2s3s4
bafkreifajsysbvhtgd7fdgrfesszexdq6v5zbj5y2jnjfwxdjyqws2s3s4: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:73334fee83139d1d8dbf488b28ad100767c38428b2a62504c758905c475c1d6c: done |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8855ae825902045ea2b27940634673ba410b61885f91b9f038f6b3303f48727c: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:ba6acccedd2923aee4c2acc6a23780b14ed4b8a5fa4e14e252a23b846df9b6c1: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:e74a9a7749e808e4ad1e90d5a81ce3146ce270de0fbdf22429cd465df8f10a13: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.3 s total: 22.0 M (73.2 MiB/s)
> nerdctl image decrypt --key=mykey.pem ipfs://bafkreifajsysbvhtgd7fdgrfesszexdq6v5zbj5y2jnjfwxdjyqws2s3s4 ubuntu:20.04-decrypted
sha256:b0ccaddb7e7e4e702420de126468eab263eb0f3c25abf0b957ce8adcd1e82105
> nerdctl run --rm -it ubuntu:20.04-decrypted echo hello
hello
```
## Running containers on IPFS with eStargz-based lazy pulling
nerdctl supports running eStargz images on IPFS with lazy pulling using Stargz Snapshotter.
In this configuration, Stargz Snapshotter mounts the eStargz image from IPFS to the container's rootfs using FUSE with lazy pulling support.
Thus the container can startup without waiting for the entire image contents to be locally available.
You can see faster container cold-start.
To use this feature, you need to enable Stargz Snapshotter following [`/docs/stargz.md`](./stargz.md).
You also need to add the following configuration to `config.toml` of Stargz Snapsohtter (typically located at `/etc/containerd-stargz-grpc/config.toml`).
```toml
ipfs = true
```
You can push an arbitrary image to IPFS with converting it to eStargz using `--estargz` option.
```
nerdctl push --estargz ipfs://fedora:36
```
You can pull and run that eStargz image with lazy pulling.
```
nerdctl run --rm -it ipfs://bafkreibp2ncujcia663uum25ustwvmyoguxqyzjnxnlhebhsgk2zowscye echo hello
```
- See [the doc in stargz-snapshotter project](https://github.com/containerd/stargz-snapshotter/blob/v0.10.0/docs/ipfs.md) for details about lazy pulling on IPFS.
- See [`/docs/stargz.md`](./stargz.md) for details about the configuration of nerdctl for Stargz Snapshotter.

View File

@ -49,6 +49,7 @@ SYSTEMD_CONTAINERD_UNIT="containerd.service"
SYSTEMD_BUILDKIT_UNIT="buildkit.service"
SYSTEMD_FUSE_OVERLAYFS_UNIT="containerd-fuse-overlayfs.service"
SYSTEMD_STARGZ_UNIT="stargz-snapshotter.service"
SYSTEMD_IPFS_UNIT="ipfs-daemon.service"
# global vars
ARG0="$0"
@ -336,6 +337,7 @@ cmd_entrypoint_install_stargz() {
[Service]
Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
Environment=IPFS_PATH=${XDG_DATA_HOME}/ipfs
ExecStart="$REALPATH0" nsenter -- containerd-stargz-grpc -address "${XDG_RUNTIME_DIR}/containerd-stargz-grpc/containerd-stargz-grpc.sock" -root "${XDG_DATA_HOME}/containerd-stargz-grpc" -config "${XDG_CONFIG_HOME}/containerd-stargz-grpc/config.toml"
ExecReload=/bin/kill -s HUP \$MAINPID
RestartSec=2
@ -358,6 +360,56 @@ cmd_entrypoint_install_stargz() {
INFO "Set \`export CONTAINERD_SNAPSHOTTER=\"stargz\"\` to use the stargz snapshotter."
}
# CLI subcommand: "install-ipfs"
cmd_entrypoint_install_ipfs() {
init
if ! command -v "ipfs" >/dev/null 2>&1; then
ERROR "ipfs needs to be present under \$PATH"
exit 1
fi
if ! systemctl --user --no-pager status "${SYSTEMD_CONTAINERD_UNIT}" >/dev/null 2>&1; then
ERROR "Install containerd first (\`$ARG0 install\`)"
exit 1
fi
IPFS_PATH="${XDG_DATA_HOME}/ipfs"
mkdir -p "${IPFS_PATH}"
cat <<-EOT | install_systemd_unit "${SYSTEMD_IPFS_UNIT}"
[Unit]
Description=ipfs daemon for rootless nerdctl
PartOf=${SYSTEMD_CONTAINERD_UNIT}
[Service]
Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
Environment=IPFS_PATH=${IPFS_PATH}
ExecStart="$REALPATH0" nsenter -- ipfs daemon $@
ExecReload=/bin/kill -s HUP \$MAINPID
RestartSec=2
Restart=always
Type=simple
KillMode=mixed
[Install]
WantedBy=default.target
EOT
# Aavoid using 5001(api)/8080(gateway) which are reserved by tests.
# TODO: support unix socket
systemctl --user stop "${SYSTEMD_IPFS_UNIT}"
sleep 3
IPFS_PATH=${IPFS_PATH} ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888"
IPFS_PATH=${IPFS_PATH} ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889"
systemctl --user restart "${SYSTEMD_IPFS_UNIT}"
sleep 3
INFO "If you use stargz-snapshotter, add the following line to \"${XDG_CONFIG_HOME}/containerd-stargz-grpc/config.toml\" manually, and then run \`systemctl --user restart ${SYSTEMD_STARGZ_UNIT}\`:"
cat <<-EOT
### BEGIN ###
ipfs = true
### END ###
EOT
INFO "Set \`export IPFS_PATH=\"${IPFS_PATH}\"\` to use ipfs."
}
# CLI subcommand: "uninstall"
cmd_entrypoint_uninstall() {
init
@ -393,6 +445,14 @@ cmd_entrypoint_uninstall_stargz() {
INFO "To remove data, run: \`$BIN/rootlesskit rm -rf ${XDG_DATA_HOME}/containerd-stargz-grpc"
}
# CLI subcommand: "uninstall-ipfs"
cmd_entrypoint_uninstall_ipfs() {
init
uninstall_systemd_unit "${SYSTEMD_IPFS_UNIT}"
INFO "This uninstallation tool does NOT remove data."
INFO "To remove data, run: \`$BIN/rootlesskit rm -rf ${XDG_DATA_HOME}/ipfs"
}
# text for --help
usage() {
echo "Usage: ${ARG0} [OPTIONS] COMMAND"
@ -416,6 +476,10 @@ usage() {
echo "Add-on commands (stargz):"
echo " install-stargz Install the systemd unit for stargz snapshotter"
echo " uninstall-stargz Uninstall the systemd unit for stargz snapshotter"
echo
echo "Add-on commands (ipfs):"
echo " install-ipfs [ipfs-daemon-flags...] Install the systemd unit for ipfs daemon. Specify \"--offline\" if run the daemon in offline mode"
echo " uninstall-ipfs Uninstall the systemd unit for ipfs daemon"
}
# parse CLI args

7
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/containerd/imgcrypt v1.1.2
github.com/containerd/stargz-snapshotter v0.10.0
github.com/containerd/stargz-snapshotter/estargz v0.10.0
github.com/containerd/stargz-snapshotter/ipfs v0.10.0
github.com/containerd/typeurl v1.0.2
github.com/containernetworking/cni v1.0.1
github.com/containernetworking/plugins v1.0.1
@ -25,6 +26,10 @@ require (
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/fatih/color v1.13.0
github.com/gogo/protobuf v1.3.2
github.com/ipfs/go-cid v0.1.0
github.com/ipfs/go-ipfs-files v0.0.9
github.com/ipfs/go-ipfs-http-client v0.1.0
github.com/ipfs/interface-go-ipfs-core v0.5.2
github.com/mattn/go-isatty v0.0.14
github.com/moby/sys/mount v0.2.0
github.com/opencontainers/go-digest v1.0.0
@ -43,6 +48,8 @@ require (
)
replace (
// Temporary fork for avoiding importing patent-protected code: https://github.com/hashicorp/golang-lru/issues/73
github.com/hashicorp/golang-lru => github.com/ktock/golang-lru v0.5.5-0.20211029085301-ec551be6f75c
github.com/spf13/cobra => github.com/robberphex/cobra v1.2.2-0.20211012081327-8e3ac9400ac4 // https://github.com/spf13/cobra/pull/1503
github.com/spf13/pflag => github.com/robberphex/pflag v1.0.6-0.20211014094653-9df3e45100fd // https://github.com/spf13/pflag/pull/333
)

777
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,7 @@ import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/images"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/nerdctl/pkg/referenceutil"
)
type Found struct {
@ -45,7 +45,7 @@ type ImageWalker struct {
// Returns the number of the found entries.
func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
var filters []string
if canonicalRef, err := refdocker.ParseDockerRef(req); err == nil {
if canonicalRef, err := referenceutil.ParseAny(req); err == nil {
filters = append(filters, fmt.Sprintf("name==%s", canonicalRef.String()))
}
filters = append(filters,

View File

@ -53,8 +53,8 @@ type EnsuredImage struct {
// PullMode is either one of "always", "missing", "never"
type PullMode = string
// getExistingImage may return errdefs.NotFound()
func getExistingImage(ctx context.Context, client *containerd.Client, snapshotter, rawRef string, platform ocispec.Platform) (*EnsuredImage, error) {
// GetExistingImage returns the specified image if exists in containerd. May return errdefs.NotFound() if not exists.
func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotter, rawRef string, platform ocispec.Platform) (*EnsuredImage, error) {
var res *EnsuredImage
imagewalker := &imagewalker.ImageWalker{
Client: client,
@ -109,7 +109,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write
}
if mode != "always" && len(ocispecPlatforms) == 1 {
res, err := getExistingImage(ctx, client, snapshotter, rawRef, ocispecPlatforms[0])
res, err := GetExistingImage(ctx, client, snapshotter, rawRef, ocispecPlatforms[0])
if err == nil {
return res, nil
}
@ -139,7 +139,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write
return nil, err
}
img, err := pullImage(ctx, client, stdout, snapshotter, resolver, ref, ocispecPlatforms, unpack)
img, err := PullImage(ctx, client, stdout, snapshotter, resolver, ref, ocispecPlatforms, unpack)
if err != nil {
if !IsErrHTTPResponseToHTTPSClient(err) {
return nil, err
@ -151,7 +151,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write
if err != nil {
return nil, err
}
return pullImage(ctx, client, stdout, snapshotter, resolver, ref, ocispecPlatforms, unpack)
return PullImage(ctx, client, stdout, snapshotter, resolver, ref, ocispecPlatforms, unpack)
} else {
logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
@ -170,7 +170,8 @@ func IsErrHTTPResponseToHTTPSClient(err error) bool {
return strings.Contains(err.Error(), unexposed)
}
func pullImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter string, resolver remotes.Resolver, ref string, ocispecPlatforms []ocispec.Platform, unpack *bool) (*EnsuredImage, error) {
// PullImage pulls an image using the specified resolver.
func PullImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter string, resolver remotes.Resolver, ref string, ocispecPlatforms []ocispec.Platform, unpack *bool) (*EnsuredImage, error) {
ctx, done, err := client.WithLease(ctx)
if err != nil {
return nil, err

151
pkg/ipfs/image.go Normal file
View File

@ -0,0 +1,151 @@
/*
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 (
"context"
"fmt"
"io"
"os"
"github.com/containerd/containerd"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/images/converter"
"github.com/containerd/containerd/remotes"
"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
"github.com/containerd/nerdctl/pkg/imgutil"
"github.com/containerd/nerdctl/pkg/platformutil"
"github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/containerd/stargz-snapshotter/ipfs"
"github.com/docker/docker/errdefs"
"github.com/ipfs/go-cid"
files "github.com/ipfs/go-ipfs-files"
iface "github.com/ipfs/interface-go-ipfs-core"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
)
// EnsureImage pull the specified image from IPFS.
func EnsureImage(ctx context.Context, client *containerd.Client, ipfsClient iface.CoreAPI, stdout io.Writer, snapshotter string, scheme string, ref string, mode imgutil.PullMode, ocispecPlatforms []ocispec.Platform, unpack *bool) (*imgutil.EnsuredImage, error) {
switch mode {
case "always", "missing", "never":
// NOP
default:
return nil, fmt.Errorf("unexpected pull mode: %q", mode)
}
switch scheme {
case "ipfs", "ipns":
// NOP
default:
return nil, fmt.Errorf("unexpected scheme: %q", scheme)
}
if mode != "always" && len(ocispecPlatforms) == 1 {
res, err := imgutil.GetExistingImage(ctx, client, snapshotter, ref, ocispecPlatforms[0])
if err == nil {
return res, nil
}
if !errdefs.IsNotFound(err) {
return nil, err
}
}
if mode == "never" {
return nil, fmt.Errorf("image %q is not available", ref)
}
r, err := ipfs.NewResolver(ipfsClient, ipfs.ResolverOptions{
Scheme: scheme,
})
if err != nil {
return nil, err
}
return imgutil.PullImage(ctx, client, os.Stdout, snapshotter, r, ref, ocispecPlatforms, unpack)
}
// Push pushes the specified image to IPFS.
func Push(ctx context.Context, client *containerd.Client, ipfsClient iface.CoreAPI, rawRef string, layerConvert converter.ConvertFunc, allPlatforms bool, platform []string, ensureImage bool) (cid.Cid, error) {
platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
if err != nil {
return cid.Cid{}, err
}
if ensureImage {
// Ensure image contents are fully downloaded
logrus.Infof("ensuring image contents")
if err := ensureContentsOfIPFSImage(ctx, client, ipfsClient, rawRef, allPlatforms, platform); err != nil {
logrus.WithError(err).Warnf("failed to ensure the existence of image %q", rawRef)
}
}
ref, err := referenceutil.ParseAny(rawRef)
if err != nil {
return cid.Cid{}, err
}
p, err := ipfs.Push(ctx, client, ipfsClient, ref.String(), layerConvert, platMC)
if err != nil {
return cid.Cid{}, err
}
return p.Cid(), nil
}
// ensureContentsOfIPFSImage ensures that the entire contents of an exisiting IPFS image are fully downloaded to containerd.
func ensureContentsOfIPFSImage(ctx context.Context, client *containerd.Client, ipfsClient iface.CoreAPI, ref string, allPlatforms bool, platform []string) error {
platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
if err != nil {
return err
}
var img images.Image
walker := &imagewalker.ImageWalker{
Client: client,
OnFound: func(ctx context.Context, found imagewalker.Found) error {
img = found.Image
return nil
},
}
n, err := walker.Walk(ctx, ref)
if err != nil {
return err
} else if n == 0 {
return fmt.Errorf("image does not exist: %q", ref)
} else if n > 1 {
return fmt.Errorf("ambigious reference %q matched %d objects", ref, n)
}
cs := client.ContentStore()
childrenHandler := images.ChildrenHandler(cs)
childrenHandler = images.SetChildrenLabels(cs, childrenHandler)
childrenHandler = images.FilterPlatforms(childrenHandler, platMC)
return images.Dispatch(ctx, images.Handlers(
remotes.FetchHandler(cs, &fetcher{ipfsClient}),
childrenHandler,
), nil, img.Target)
}
// fetcher fetches a file from IPFS
// TODO: fix github.com/containerd/stargz-snapshotter/ipfs to export this and we should import that
type fetcher struct {
api iface.CoreAPI
}
func (f *fetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
p, err := ipfs.GetPath(desc)
if err != nil {
return nil, err
}
n, err := f.api.Unixfs().Get(ctx, p)
if err != nil {
return nil, fmt.Errorf("failed to get file %q: %v", p.String(), err)
}
return files.ToFile(n), nil
}

View File

@ -0,0 +1,66 @@
/*
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 referenceutil
import (
"fmt"
"strings"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/ipfs/go-cid"
)
// Reference is a reference to an image.
type Reference interface {
// String returns the full reference which can be understood by containerd.
String() string
}
// ParseAny parses the passed reference with allowing it to be non-docker reference.
// If the ref has IPFS scheme or can be parsed as CID, it's parsed as an IPFS reference.
// Otherwise it's parsed as a docker reference.
func ParseAny(rawRef string) (Reference, error) {
if _, ref, err := ParseIPFSRefWithScheme(rawRef); err == nil {
return stringRef{ref}, nil
}
if c, err := cid.Decode(rawRef); err == nil {
return c, nil
}
return ParseDockerRef(rawRef)
}
// ParseDockerRef parses the passed reference with assuming it's a docker reference.
func ParseDockerRef(rawRef string) (refdocker.Named, error) {
return refdocker.ParseDockerRef(rawRef)
}
// ParseIPFSSRefWithScheme parses the passed reference with assuming it's an IPFS reference with scheme prefix.
func ParseIPFSRefWithScheme(name string) (scheme, ref string, err error) {
if strings.HasPrefix(name, "ipfs://") || strings.HasPrefix(name, "ipns://") {
return name[:4], name[7:], nil
}
return "", "", fmt.Errorf("reference is not an IPFS reference")
}
type stringRef struct {
s string
}
func (s stringRef) String() string {
return s.s
}