github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/image/push.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 image
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/content"
    28  	"github.com/containerd/containerd/images"
    29  	"github.com/containerd/containerd/images/converter"
    30  	"github.com/containerd/containerd/reference"
    31  	refdocker "github.com/containerd/containerd/reference/docker"
    32  	"github.com/containerd/containerd/remotes"
    33  	"github.com/containerd/containerd/remotes/docker"
    34  	dockerconfig "github.com/containerd/containerd/remotes/docker/config"
    35  	"github.com/containerd/log"
    36  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    37  	"github.com/containerd/nerdctl/v2/pkg/errutil"
    38  	"github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
    39  	"github.com/containerd/nerdctl/v2/pkg/imgutil/push"
    40  	"github.com/containerd/nerdctl/v2/pkg/ipfs"
    41  	"github.com/containerd/nerdctl/v2/pkg/platformutil"
    42  	"github.com/containerd/nerdctl/v2/pkg/referenceutil"
    43  	"github.com/containerd/nerdctl/v2/pkg/signutil"
    44  	"github.com/containerd/nerdctl/v2/pkg/snapshotterutil"
    45  	"github.com/containerd/stargz-snapshotter/estargz"
    46  	"github.com/containerd/stargz-snapshotter/estargz/zstdchunked"
    47  	estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
    48  	"github.com/opencontainers/go-digest"
    49  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    50  )
    51  
    52  // Push pushes an image specified by `rawRef`.
    53  func Push(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePushOptions) error {
    54  	if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil {
    55  		if scheme != "ipfs" {
    56  			return fmt.Errorf("ipfs scheme is only supported but got %q", scheme)
    57  		}
    58  		log.G(ctx).Infof("pushing image %q to IPFS", ref)
    59  
    60  		var ipfsPath string
    61  		if options.IpfsAddress != "" {
    62  			dir, err := os.MkdirTemp("", "apidirtmp")
    63  			if err != nil {
    64  				return err
    65  			}
    66  			defer os.RemoveAll(dir)
    67  			if err := os.WriteFile(filepath.Join(dir, "api"), []byte(options.IpfsAddress), 0600); err != nil {
    68  				return err
    69  			}
    70  			ipfsPath = dir
    71  		}
    72  
    73  		var layerConvert converter.ConvertFunc
    74  		if options.Estargz {
    75  			layerConvert = eStargzConvertFunc()
    76  		}
    77  		c, err := ipfs.Push(ctx, client, ref, layerConvert, options.AllPlatforms, options.Platforms, options.IpfsEnsureImage, ipfsPath)
    78  		if err != nil {
    79  			log.G(ctx).WithError(err).Warnf("ipfs push failed")
    80  			return err
    81  		}
    82  		fmt.Fprintln(options.Stdout, c)
    83  		return nil
    84  	}
    85  
    86  	named, err := refdocker.ParseDockerRef(rawRef)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	ref := named.String()
    91  	refDomain := refdocker.Domain(named)
    92  
    93  	platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platforms)
    94  	if err != nil {
    95  		return err
    96  	}
    97  	pushRef := ref
    98  	if !options.AllPlatforms {
    99  		pushRef = ref + "-tmp-reduced-platform"
   100  		// Push fails with "400 Bad Request" when the manifest is multi-platform but we do not locally have multi-platform blobs.
   101  		// So we create a tmp reduced-platform image to avoid the error.
   102  		platImg, err := converter.Convert(ctx, client, pushRef, ref, converter.WithPlatform(platMC))
   103  		if err != nil {
   104  			if len(options.Platforms) == 0 {
   105  				return fmt.Errorf("failed to create a tmp single-platform image %q: %w", pushRef, err)
   106  			}
   107  			return fmt.Errorf("failed to create a tmp reduced-platform image %q (platform=%v): %w", pushRef, options.Platforms, err)
   108  		}
   109  		defer client.ImageService().Delete(ctx, platImg.Name, images.SynchronousDelete())
   110  		log.G(ctx).Infof("pushing as a reduced-platform image (%s, %s)", platImg.Target.MediaType, platImg.Target.Digest)
   111  	}
   112  
   113  	if options.Estargz {
   114  		pushRef = ref + "-tmp-esgz"
   115  		esgzImg, err := converter.Convert(ctx, client, pushRef, ref, converter.WithPlatform(platMC), converter.WithLayerConvertFunc(eStargzConvertFunc()))
   116  		if err != nil {
   117  			return fmt.Errorf("failed to convert to eStargz: %v", err)
   118  		}
   119  		defer client.ImageService().Delete(ctx, esgzImg.Name, images.SynchronousDelete())
   120  		log.G(ctx).Infof("pushing as an eStargz image (%s, %s)", esgzImg.Target.MediaType, esgzImg.Target.Digest)
   121  	}
   122  
   123  	// In order to push images where most layers are the same but the
   124  	// repository name is different, it is necessary to refresh the
   125  	// PushTracker. Otherwise, the MANIFEST_BLOB_UNKNOWN error will occur due
   126  	// to the registry not creating the corresponding layer link file,
   127  	// resulting in the failure of the entire image push.
   128  	pushTracker := docker.NewInMemoryTracker()
   129  
   130  	pushFunc := func(r remotes.Resolver) error {
   131  		return push.Push(ctx, client, r, pushTracker, options.Stdout, pushRef, ref, platMC, options.AllowNondistributableArtifacts, options.Quiet)
   132  	}
   133  
   134  	var dOpts []dockerconfigresolver.Opt
   135  	if options.GOptions.InsecureRegistry {
   136  		log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain)
   137  		dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
   138  	}
   139  	dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir))
   140  
   141  	ho, err := dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	resolverOpts := docker.ResolverOptions{
   147  		Tracker: pushTracker,
   148  		Hosts:   dockerconfig.ConfigureHosts(ctx, *ho),
   149  	}
   150  
   151  	resolver := docker.NewResolver(resolverOpts)
   152  	if err = pushFunc(resolver); err != nil {
   153  		// In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp <port>: connection refused"
   154  		if !errutil.IsErrHTTPResponseToHTTPSClient(err) && !errutil.IsErrConnectionRefused(err) {
   155  			return err
   156  		}
   157  		if options.GOptions.InsecureRegistry {
   158  			log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
   159  			dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
   160  			resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...)
   161  			if err != nil {
   162  				return err
   163  			}
   164  			return pushFunc(resolver)
   165  		}
   166  		log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
   167  		log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
   168  		return err
   169  	}
   170  
   171  	img, err := client.ImageService().Get(ctx, pushRef)
   172  	if err != nil {
   173  		return err
   174  	}
   175  	refSpec, err := reference.Parse(pushRef)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	signRef := fmt.Sprintf("%s@%s", refSpec.String(), img.Target.Digest.String())
   180  	if err = signutil.Sign(signRef,
   181  		options.GOptions.Experimental,
   182  		options.SignOptions); err != nil {
   183  		return err
   184  	}
   185  	if options.GOptions.Snapshotter == "soci" {
   186  		if err = snapshotterutil.CreateSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil {
   187  			return err
   188  		}
   189  		if err = snapshotterutil.PushSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms); err != nil {
   190  			return err
   191  		}
   192  	}
   193  	if options.Quiet {
   194  		fmt.Fprintln(options.Stdout, ref)
   195  	}
   196  	return nil
   197  }
   198  
   199  func eStargzConvertFunc() converter.ConvertFunc {
   200  	convertToESGZ := estargzconvert.LayerConvertFunc()
   201  	return func(ctx context.Context, cs content.Store, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
   202  		if isReusableESGZ(ctx, cs, desc) {
   203  			log.L.Infof("reusing estargz %s without conversion", desc.Digest)
   204  			return nil, nil
   205  		}
   206  		newDesc, err := convertToESGZ(ctx, cs, desc)
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		log.L.Infof("converted %q to %s", desc.MediaType, newDesc.Digest)
   211  		return newDesc, err
   212  	}
   213  
   214  }
   215  
   216  func isReusableESGZ(ctx context.Context, cs content.Store, desc ocispec.Descriptor) bool {
   217  	dgstStr, ok := desc.Annotations[estargz.TOCJSONDigestAnnotation]
   218  	if !ok {
   219  		return false
   220  	}
   221  	tocdgst, err := digest.Parse(dgstStr)
   222  	if err != nil {
   223  		return false
   224  	}
   225  	ra, err := cs.ReaderAt(ctx, desc)
   226  	if err != nil {
   227  		return false
   228  	}
   229  	defer ra.Close()
   230  	r, err := estargz.Open(io.NewSectionReader(ra, 0, desc.Size), estargz.WithDecompressors(new(zstdchunked.Decompressor)))
   231  	if err != nil {
   232  		return false
   233  	}
   234  	if _, err := r.VerifyTOC(tocdgst); err != nil {
   235  		return false
   236  	}
   237  	return true
   238  }