github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/imgutil/push/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 push derived from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/images/push.go 18 package push 19 20 import ( 21 "context" 22 "fmt" 23 "io" 24 "sync" 25 "text/tabwriter" 26 "time" 27 28 "github.com/containerd/containerd" 29 "github.com/containerd/containerd/images" 30 "github.com/containerd/containerd/pkg/progress" 31 "github.com/containerd/containerd/remotes" 32 "github.com/containerd/containerd/remotes/docker" 33 "github.com/containerd/log" 34 "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs" 35 "github.com/containerd/platforms" 36 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 37 38 "golang.org/x/sync/errgroup" 39 ) 40 41 // Push pushes an image to a remote registry. 42 func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, pushTracker docker.StatusTracker, stdout io.Writer, 43 localRef, remoteRef string, platform platforms.MatchComparer, allowNonDist, quiet bool) error { 44 img, err := client.ImageService().Get(ctx, localRef) 45 if err != nil { 46 return fmt.Errorf("unable to resolve image to manifest: %w", err) 47 } 48 desc := img.Target 49 50 ongoing := newPushJobs(pushTracker) 51 52 eg, ctx := errgroup.WithContext(ctx) 53 54 // used to notify the progress writer 55 doneCh := make(chan struct{}) 56 57 eg.Go(func() error { 58 defer close(doneCh) 59 60 log.G(ctx).WithField("image", remoteRef).WithField("digest", desc.Digest).Debug("pushing") 61 62 jobHandler := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 63 if allowNonDist || !images.IsNonDistributable(desc.MediaType) { 64 ongoing.add(remotes.MakeRefKey(ctx, desc)) 65 } 66 return nil, nil 67 }) 68 69 if !allowNonDist { 70 jobHandler = remotes.SkipNonDistributableBlobs(jobHandler) 71 } 72 73 return client.Push(ctx, remoteRef, desc, 74 containerd.WithResolver(resolver), 75 containerd.WithImageHandler(jobHandler), 76 containerd.WithPlatformMatcher(platform), 77 ) 78 }) 79 80 if !quiet { 81 eg.Go(func() error { 82 var ( 83 ticker = time.NewTicker(100 * time.Millisecond) 84 fw = progress.NewWriter(stdout) 85 start = time.Now() 86 done bool 87 ) 88 89 defer ticker.Stop() 90 91 for { 92 select { 93 case <-ticker.C: 94 fw.Flush() 95 96 tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0) 97 98 jobs.Display(tw, ongoing.status(), start) 99 tw.Flush() 100 101 if done { 102 fw.Flush() 103 return nil 104 } 105 case <-doneCh: 106 done = true 107 case <-ctx.Done(): 108 done = true // allow ui to update once more 109 } 110 } 111 }) 112 } 113 return eg.Wait() 114 } 115 116 type pushjobs struct { 117 jobs map[string]struct{} 118 ordered []string 119 tracker docker.StatusTracker 120 mu sync.Mutex 121 } 122 123 func newPushJobs(tracker docker.StatusTracker) *pushjobs { 124 return &pushjobs{ 125 jobs: make(map[string]struct{}), 126 tracker: tracker, 127 } 128 } 129 130 func (j *pushjobs) add(ref string) { 131 j.mu.Lock() 132 defer j.mu.Unlock() 133 134 if _, ok := j.jobs[ref]; ok { 135 return 136 } 137 j.ordered = append(j.ordered, ref) 138 j.jobs[ref] = struct{}{} 139 } 140 141 func (j *pushjobs) status() []jobs.StatusInfo { 142 j.mu.Lock() 143 defer j.mu.Unlock() 144 145 statuses := make([]jobs.StatusInfo, 0, len(j.jobs)) 146 for _, name := range j.ordered { 147 si := jobs.StatusInfo{ 148 Ref: name, 149 } 150 151 status, err := j.tracker.GetStatus(name) 152 if err != nil { 153 si.Status = "waiting" 154 } else { 155 si.Offset = status.Offset 156 si.Total = status.Total 157 si.StartedAt = status.StartedAt 158 si.UpdatedAt = status.UpdatedAt 159 if status.Offset >= status.Total { 160 if status.UploadUUID == "" { 161 si.Status = "done" 162 } else { 163 si.Status = "committing" 164 } 165 } else { 166 si.Status = "uploading" 167 } 168 } 169 statuses = append(statuses, si) 170 } 171 172 return statuses 173 }