github.com/containerd/nerdctl@v1.7.7/pkg/ipfs/image.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package ipfs
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  
    25  	"github.com/containerd/containerd"
    26  	"github.com/containerd/containerd/images"
    27  	"github.com/containerd/containerd/images/converter"
    28  	"github.com/containerd/containerd/remotes"
    29  	"github.com/containerd/log"
    30  	"github.com/containerd/nerdctl/pkg/idutil/imagewalker"
    31  	"github.com/containerd/nerdctl/pkg/imgutil"
    32  	"github.com/containerd/nerdctl/pkg/platformutil"
    33  	"github.com/containerd/nerdctl/pkg/referenceutil"
    34  	"github.com/containerd/stargz-snapshotter/ipfs"
    35  	ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client"
    36  	"github.com/docker/docker/errdefs"
    37  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    38  )
    39  
    40  const ipfsPathEnv = "IPFS_PATH"
    41  
    42  // EnsureImage pull the specified image from IPFS.
    43  func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter string, scheme string, ref string, mode imgutil.PullMode, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, ipfsPath string, rFlags imgutil.RemoteSnapshotterFlags) (*imgutil.EnsuredImage, error) {
    44  	switch mode {
    45  	case "always", "missing", "never":
    46  		// NOP
    47  	default:
    48  		return nil, fmt.Errorf("unexpected pull mode: %q", mode)
    49  	}
    50  	switch scheme {
    51  	case "ipfs", "ipns":
    52  		// NOP
    53  	default:
    54  		return nil, fmt.Errorf("unexpected scheme: %q", scheme)
    55  	}
    56  
    57  	// if not `always` pull and given one platform and image found locally, return existing image directly.
    58  	if mode != "always" && len(ocispecPlatforms) == 1 {
    59  		if res, err := imgutil.GetExistingImage(ctx, client, snapshotter, ref, ocispecPlatforms[0]); err == nil {
    60  			return res, nil
    61  		} else if !errdefs.IsNotFound(err) {
    62  			return nil, err
    63  		}
    64  	}
    65  
    66  	if mode == "never" {
    67  		return nil, fmt.Errorf("image %q is not available", ref)
    68  	}
    69  	r, err := ipfs.NewResolver(ipfs.ResolverOptions{
    70  		Scheme:   scheme,
    71  		IPFSPath: lookupIPFSPath(ipfsPath),
    72  	})
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	return imgutil.PullImage(ctx, client, stdout, stderr, snapshotter, r, ref, ocispecPlatforms, unpack, quiet, rFlags)
    77  }
    78  
    79  // Push pushes the specified image to IPFS.
    80  func Push(ctx context.Context, client *containerd.Client, rawRef string, layerConvert converter.ConvertFunc, allPlatforms bool, platform []string, ensureImage bool, ipfsPath string) (string, error) {
    81  	platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
    82  	if err != nil {
    83  		return "", err
    84  	}
    85  	ipath := lookupIPFSPath(ipfsPath)
    86  	if ensureImage {
    87  		// Ensure image contents are fully downloaded
    88  		log.G(ctx).Infof("ensuring image contents")
    89  		if err := ensureContentsOfIPFSImage(ctx, client, rawRef, allPlatforms, platform, ipath); err != nil {
    90  			log.G(ctx).WithError(err).Warnf("failed to ensure the existence of image %q", rawRef)
    91  		}
    92  	}
    93  	ref, err := referenceutil.ParseAny(rawRef)
    94  	if err != nil {
    95  		return "", err
    96  	}
    97  	return ipfs.PushWithIPFSPath(ctx, client, ref.String(), layerConvert, platMC, &ipath)
    98  }
    99  
   100  // ensureContentsOfIPFSImage ensures that the entire contents of an existing IPFS image are fully downloaded to containerd.
   101  func ensureContentsOfIPFSImage(ctx context.Context, client *containerd.Client, ref string, allPlatforms bool, platform []string, ipfsPath string) error {
   102  	iurl, err := ipfsclient.GetIPFSAPIAddress(ipfsPath, "http")
   103  	if err != nil {
   104  		return err
   105  	}
   106  	platMC, err := platformutil.NewMatchComparer(allPlatforms, platform)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	var img images.Image
   111  	walker := &imagewalker.ImageWalker{
   112  		Client: client,
   113  		OnFound: func(ctx context.Context, found imagewalker.Found) error {
   114  			img = found.Image
   115  			return nil
   116  		},
   117  	}
   118  	n, err := walker.Walk(ctx, ref)
   119  	if err != nil {
   120  		return err
   121  	} else if n == 0 {
   122  		return fmt.Errorf("image does not exist: %q", ref)
   123  	} else if n > 1 {
   124  		return fmt.Errorf("ambiguous reference %q matched %d objects", ref, n)
   125  	}
   126  	cs := client.ContentStore()
   127  	childrenHandler := images.ChildrenHandler(cs)
   128  	childrenHandler = images.SetChildrenLabels(cs, childrenHandler)
   129  	childrenHandler = images.FilterPlatforms(childrenHandler, platMC)
   130  	return images.Dispatch(ctx, images.Handlers(
   131  		remotes.FetchHandler(cs, &fetcher{ipfsclient.New(iurl)}),
   132  		childrenHandler,
   133  	), nil, img.Target)
   134  }
   135  
   136  // fetcher fetches a file from IPFS
   137  // TODO: fix github.com/containerd/stargz-snapshotter/ipfs to export this and we should import that
   138  type fetcher struct {
   139  	ipfsclient *ipfsclient.Client
   140  }
   141  
   142  func (f *fetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
   143  	cid, err := ipfs.GetCID(desc)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	off, size := 0, int(desc.Size)
   148  	return f.ipfsclient.Get("/ipfs/"+cid, &off, &size)
   149  }
   150  
   151  // If IPFS_PATH is specified, this will be used.
   152  // If not, "~/.ipfs" will be used.
   153  // The behaviour is compatible to kubo: https://github.com/ipfs/go-ipfs-http-client/blob/171fcd55e3b743c38fb9d78a34a3a703ee0b5e89/api.go#L43-L44
   154  // Optionally takes ipfsPath string having the highest priority.
   155  func lookupIPFSPath(ipfsPath string) string {
   156  	var ipath string
   157  	if idir := os.Getenv(ipfsPathEnv); idir != "" {
   158  		ipath = idir
   159  	}
   160  	if ipath == "" {
   161  		ipath = "~/.ipfs"
   162  	}
   163  	if ipfsPath != "" {
   164  		ipath = ipfsPath
   165  	}
   166  	return ipath
   167  }