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