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 }