github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/cmd/ctr/commands/images/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 images 18 19 import ( 20 gocontext "context" 21 "os" 22 "sync" 23 "text/tabwriter" 24 "time" 25 26 "github.com/containerd/containerd" 27 "github.com/containerd/containerd/cmd/ctr/commands" 28 "github.com/containerd/containerd/cmd/ctr/commands/content" 29 "github.com/containerd/containerd/images" 30 "github.com/containerd/containerd/log" 31 "github.com/containerd/containerd/pkg/progress" 32 "github.com/containerd/containerd/platforms" 33 "github.com/containerd/containerd/remotes" 34 "github.com/containerd/containerd/remotes/docker" 35 digest "github.com/opencontainers/go-digest" 36 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 37 "github.com/pkg/errors" 38 "github.com/urfave/cli" 39 "golang.org/x/sync/errgroup" 40 ) 41 42 var pushCommand = cli.Command{ 43 Name: "push", 44 Usage: "push an image to a remote", 45 ArgsUsage: "[flags] <remote> [<local>]", 46 Description: `Pushes an image reference from containerd. 47 48 All resources associated with the manifest reference will be pushed. 49 The ref is used to resolve to a locally existing image manifest. 50 The image manifest must exist before push. Creating a new image 51 manifest can be done through calculating the diff for layers, 52 creating the associated configuration, and creating the manifest 53 which references those resources. 54 `, 55 Flags: append(commands.RegistryFlags, cli.StringFlag{ 56 Name: "manifest", 57 Usage: "digest of manifest", 58 }, cli.StringFlag{ 59 Name: "manifest-type", 60 Usage: "media type of manifest digest", 61 Value: ocispec.MediaTypeImageManifest, 62 }, cli.StringSliceFlag{ 63 Name: "platform", 64 Usage: "push content from a specific platform", 65 Value: &cli.StringSlice{}, 66 }), 67 Action: func(context *cli.Context) error { 68 var ( 69 ref = context.Args().First() 70 local = context.Args().Get(1) 71 debug = context.GlobalBool("debug") 72 desc ocispec.Descriptor 73 ) 74 if ref == "" { 75 return errors.New("please provide a remote image reference to push") 76 } 77 78 client, ctx, cancel, err := commands.NewClient(context) 79 if err != nil { 80 return err 81 } 82 defer cancel() 83 84 if manifest := context.String("manifest"); manifest != "" { 85 desc.Digest, err = digest.Parse(manifest) 86 if err != nil { 87 return errors.Wrap(err, "invalid manifest digest") 88 } 89 desc.MediaType = context.String("manifest-type") 90 } else { 91 if local == "" { 92 local = ref 93 } 94 img, err := client.ImageService().Get(ctx, local) 95 if err != nil { 96 return errors.Wrap(err, "unable to resolve image to manifest") 97 } 98 desc = img.Target 99 100 if pss := context.StringSlice("platform"); len(pss) == 1 { 101 p, err := platforms.Parse(pss[0]) 102 if err != nil { 103 return errors.Wrapf(err, "invalid platform %q", pss[0]) 104 } 105 106 cs := client.ContentStore() 107 if manifests, err := images.Children(ctx, cs, desc); err == nil && len(manifests) > 0 { 108 matcher := platforms.NewMatcher(p) 109 for _, manifest := range manifests { 110 if manifest.Platform != nil && matcher.Match(*manifest.Platform) { 111 if _, err := images.Children(ctx, cs, manifest); err != nil { 112 return errors.Wrap(err, "no matching manifest") 113 } 114 desc = manifest 115 break 116 } 117 } 118 } 119 } 120 } 121 122 resolver, err := commands.GetResolver(ctx, context) 123 if err != nil { 124 return err 125 } 126 ongoing := newPushJobs(commands.PushTracker) 127 128 eg, ctx := errgroup.WithContext(ctx) 129 130 // used to notify the progress writer 131 doneCh := make(chan struct{}) 132 133 eg.Go(func() error { 134 defer close(doneCh) 135 136 log.G(ctx).WithField("image", ref).WithField("digest", desc.Digest).Debug("pushing") 137 138 jobHandler := images.HandlerFunc(func(ctx gocontext.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 139 ongoing.add(remotes.MakeRefKey(ctx, desc)) 140 return nil, nil 141 }) 142 143 return client.Push(ctx, ref, desc, 144 containerd.WithResolver(resolver), 145 containerd.WithImageHandler(jobHandler), 146 ) 147 }) 148 149 // don't show progress if debug mode is set 150 if !debug { 151 eg.Go(func() error { 152 var ( 153 ticker = time.NewTicker(100 * time.Millisecond) 154 fw = progress.NewWriter(os.Stdout) 155 start = time.Now() 156 done bool 157 ) 158 159 defer ticker.Stop() 160 161 for { 162 select { 163 case <-ticker.C: 164 fw.Flush() 165 166 tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0) 167 168 content.Display(tw, ongoing.status(), start) 169 tw.Flush() 170 171 if done { 172 fw.Flush() 173 return nil 174 } 175 case <-doneCh: 176 done = true 177 case <-ctx.Done(): 178 done = true // allow ui to update once more 179 } 180 } 181 }) 182 } 183 return eg.Wait() 184 }, 185 } 186 187 type pushjobs struct { 188 jobs map[string]struct{} 189 ordered []string 190 tracker docker.StatusTracker 191 mu sync.Mutex 192 } 193 194 func newPushJobs(tracker docker.StatusTracker) *pushjobs { 195 return &pushjobs{ 196 jobs: make(map[string]struct{}), 197 tracker: tracker, 198 } 199 } 200 201 func (j *pushjobs) add(ref string) { 202 j.mu.Lock() 203 defer j.mu.Unlock() 204 205 if _, ok := j.jobs[ref]; ok { 206 return 207 } 208 j.ordered = append(j.ordered, ref) 209 j.jobs[ref] = struct{}{} 210 } 211 212 func (j *pushjobs) status() []content.StatusInfo { 213 j.mu.Lock() 214 defer j.mu.Unlock() 215 216 statuses := make([]content.StatusInfo, 0, len(j.jobs)) 217 for _, name := range j.ordered { 218 si := content.StatusInfo{ 219 Ref: name, 220 } 221 222 status, err := j.tracker.GetStatus(name) 223 if err != nil { 224 si.Status = "waiting" 225 } else { 226 si.Offset = status.Offset 227 si.Total = status.Total 228 si.StartedAt = status.StartedAt 229 si.UpdatedAt = status.UpdatedAt 230 if status.Offset >= status.Total { 231 if status.UploadUUID == "" { 232 si.Status = "done" 233 } else { 234 si.Status = "committing" 235 } 236 } else { 237 si.Status = "uploading" 238 } 239 } 240 statuses = append(statuses, si) 241 } 242 243 return statuses 244 }