github.com/containerd/Containerd@v1.4.13/cmd/ctr/commands/content/content.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 content 18 19 import ( 20 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "os/exec" 25 "strings" 26 "text/tabwriter" 27 "time" 28 29 "github.com/containerd/containerd/cmd/ctr/commands" 30 "github.com/containerd/containerd/content" 31 "github.com/containerd/containerd/errdefs" 32 "github.com/containerd/containerd/log" 33 units "github.com/docker/go-units" 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 ) 39 40 var ( 41 // Command is the cli command for managing content 42 Command = cli.Command{ 43 Name: "content", 44 Usage: "manage content", 45 Subcommands: cli.Commands{ 46 activeIngestCommand, 47 deleteCommand, 48 editCommand, 49 fetchCommand, 50 fetchObjectCommand, 51 getCommand, 52 ingestCommand, 53 listCommand, 54 pushObjectCommand, 55 setLabelsCommand, 56 }, 57 } 58 59 getCommand = cli.Command{ 60 Name: "get", 61 Usage: "get the data for an object", 62 ArgsUsage: "[<digest>, ...]", 63 Description: "display the image object", 64 Action: func(context *cli.Context) error { 65 dgst, err := digest.Parse(context.Args().First()) 66 if err != nil { 67 return err 68 } 69 client, ctx, cancel, err := commands.NewClient(context) 70 if err != nil { 71 return err 72 } 73 defer cancel() 74 cs := client.ContentStore() 75 ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: dgst}) 76 if err != nil { 77 return err 78 } 79 defer ra.Close() 80 81 _, err = io.Copy(os.Stdout, content.NewReader(ra)) 82 return err 83 }, 84 } 85 86 ingestCommand = cli.Command{ 87 Name: "ingest", 88 Usage: "accept content into the store", 89 ArgsUsage: "[flags] <key>", 90 Description: "ingest objects into the local content store", 91 Flags: []cli.Flag{ 92 cli.Int64Flag{ 93 Name: "expected-size", 94 Usage: "validate against provided size", 95 }, 96 cli.StringFlag{ 97 Name: "expected-digest", 98 Usage: "verify content against expected digest", 99 }, 100 }, 101 Action: func(context *cli.Context) error { 102 var ( 103 ref = context.Args().First() 104 expectedSize = context.Int64("expected-size") 105 expectedDigest = digest.Digest(context.String("expected-digest")) 106 ) 107 if err := expectedDigest.Validate(); expectedDigest != "" && err != nil { 108 return err 109 } 110 if ref == "" { 111 return errors.New("must specify a transaction reference") 112 } 113 client, ctx, cancel, err := commands.NewClient(context) 114 if err != nil { 115 return err 116 } 117 defer cancel() 118 119 cs := client.ContentStore() 120 121 // TODO(stevvooe): Allow ingest to be reentrant. Currently, we expect 122 // all data to be written in a single invocation. Allow multiple writes 123 // to the same transaction key followed by a commit. 124 return content.WriteBlob(ctx, cs, ref, os.Stdin, ocispec.Descriptor{Size: expectedSize, Digest: expectedDigest}) 125 }, 126 } 127 128 activeIngestCommand = cli.Command{ 129 Name: "active", 130 Usage: "display active transfers", 131 ArgsUsage: "[flags] [<regexp>]", 132 Description: "display the ongoing transfers", 133 Flags: []cli.Flag{ 134 cli.DurationFlag{ 135 Name: "timeout, t", 136 Usage: "total timeout for fetch", 137 EnvVar: "CONTAINERD_FETCH_TIMEOUT", 138 }, 139 cli.StringFlag{ 140 Name: "root", 141 Usage: "path to content store root", 142 Value: "/tmp/content", // TODO(stevvooe): for now, just use the PWD/.content 143 }, 144 }, 145 Action: func(context *cli.Context) error { 146 match := context.Args().First() 147 client, ctx, cancel, err := commands.NewClient(context) 148 if err != nil { 149 return err 150 } 151 defer cancel() 152 cs := client.ContentStore() 153 active, err := cs.ListStatuses(ctx, match) 154 if err != nil { 155 return err 156 } 157 tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 158 fmt.Fprintln(tw, "REF\tSIZE\tAGE\t") 159 for _, active := range active { 160 fmt.Fprintf(tw, "%s\t%s\t%s\t\n", 161 active.Ref, 162 units.HumanSize(float64(active.Offset)), 163 units.HumanDuration(time.Since(active.StartedAt))) 164 } 165 166 return tw.Flush() 167 }, 168 } 169 170 listCommand = cli.Command{ 171 Name: "list", 172 Aliases: []string{"ls"}, 173 Usage: "list all blobs in the store", 174 ArgsUsage: "[flags]", 175 Description: "list blobs in the content store", 176 Flags: []cli.Flag{ 177 cli.BoolFlag{ 178 Name: "quiet, q", 179 Usage: "print only the blob digest", 180 }, 181 }, 182 Action: func(context *cli.Context) error { 183 var ( 184 quiet = context.Bool("quiet") 185 args = []string(context.Args()) 186 ) 187 client, ctx, cancel, err := commands.NewClient(context) 188 if err != nil { 189 return err 190 } 191 defer cancel() 192 cs := client.ContentStore() 193 194 var walkFn content.WalkFunc 195 if quiet { 196 walkFn = func(info content.Info) error { 197 fmt.Println(info.Digest) 198 return nil 199 } 200 } else { 201 tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0) 202 defer tw.Flush() 203 204 fmt.Fprintln(tw, "DIGEST\tSIZE\tAGE\tLABELS") 205 walkFn = func(info content.Info) error { 206 var labelStrings []string 207 for k, v := range info.Labels { 208 labelStrings = append(labelStrings, strings.Join([]string{k, v}, "=")) 209 } 210 labels := strings.Join(labelStrings, ",") 211 if labels == "" { 212 labels = "-" 213 } 214 215 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", 216 info.Digest, 217 units.HumanSize(float64(info.Size)), 218 units.HumanDuration(time.Since(info.CreatedAt)), 219 labels) 220 return nil 221 } 222 223 } 224 225 return cs.Walk(ctx, walkFn, args...) 226 }, 227 } 228 229 setLabelsCommand = cli.Command{ 230 Name: "label", 231 Usage: "add labels to content", 232 ArgsUsage: "<digest> [<label>=<value> ...]", 233 Description: "labels blobs in the content store", 234 Action: func(context *cli.Context) error { 235 object, labels := commands.ObjectWithLabelArgs(context) 236 client, ctx, cancel, err := commands.NewClient(context) 237 if err != nil { 238 return err 239 } 240 defer cancel() 241 242 cs := client.ContentStore() 243 244 dgst, err := digest.Parse(object) 245 if err != nil { 246 return err 247 } 248 249 info := content.Info{ 250 Digest: dgst, 251 Labels: map[string]string{}, 252 } 253 254 var paths []string 255 for k, v := range labels { 256 paths = append(paths, fmt.Sprintf("labels.%s", k)) 257 if v != "" { 258 info.Labels[k] = v 259 } 260 } 261 262 // Nothing updated, do no clear 263 if len(paths) == 0 { 264 info, err = cs.Info(ctx, info.Digest) 265 } else { 266 info, err = cs.Update(ctx, info, paths...) 267 } 268 if err != nil { 269 return err 270 } 271 272 var labelStrings []string 273 for k, v := range info.Labels { 274 labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v)) 275 } 276 277 fmt.Println(strings.Join(labelStrings, ",")) 278 279 return nil 280 }, 281 } 282 283 editCommand = cli.Command{ 284 Name: "edit", 285 Usage: "edit a blob and return a new digest", 286 ArgsUsage: "[flags] <digest>", 287 Description: "edit a blob and return a new digest", 288 Flags: []cli.Flag{ 289 cli.StringFlag{ 290 Name: "validate", 291 Usage: "validate the result against a format (json, mediatype, etc.)", 292 }, 293 cli.StringFlag{ 294 Name: "editor", 295 Usage: "select editor (vim, emacs, etc.)", 296 EnvVar: "EDITOR", 297 }, 298 }, 299 Action: func(context *cli.Context) error { 300 var ( 301 validate = context.String("validate") 302 object = context.Args().First() 303 ) 304 305 if validate != "" { 306 return errors.New("validating the edit result not supported") 307 } 308 309 // TODO(stevvooe): Support looking up objects by a reference through 310 // the image metadata storage. 311 312 dgst, err := digest.Parse(object) 313 if err != nil { 314 return err 315 } 316 client, ctx, cancel, err := commands.NewClient(context) 317 if err != nil { 318 return err 319 } 320 defer cancel() 321 cs := client.ContentStore() 322 ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: dgst}) 323 if err != nil { 324 return err 325 } 326 defer ra.Close() 327 328 nrc, err := edit(context, content.NewReader(ra)) 329 if err != nil { 330 return err 331 } 332 defer nrc.Close() 333 334 wr, err := cs.Writer(ctx, content.WithRef("edit-"+object)) // TODO(stevvooe): Choose a better key? 335 if err != nil { 336 return err 337 } 338 339 if _, err := io.Copy(wr, nrc); err != nil { 340 return err 341 } 342 343 if err := wr.Commit(ctx, 0, wr.Digest()); err != nil { 344 return err 345 } 346 347 fmt.Println(wr.Digest()) 348 return nil 349 }, 350 } 351 352 deleteCommand = cli.Command{ 353 Name: "delete", 354 Aliases: []string{"del", "remove", "rm"}, 355 Usage: "permanently delete one or more blobs", 356 ArgsUsage: "[<digest>, ...]", 357 Description: `Delete one or more blobs permanently. Successfully deleted 358 blobs are printed to stdout.`, 359 Action: func(context *cli.Context) error { 360 var ( 361 args = []string(context.Args()) 362 exitError error 363 ) 364 client, ctx, cancel, err := commands.NewClient(context) 365 if err != nil { 366 return err 367 } 368 defer cancel() 369 cs := client.ContentStore() 370 371 for _, arg := range args { 372 dgst, err := digest.Parse(arg) 373 if err != nil { 374 if exitError == nil { 375 exitError = err 376 } 377 log.G(ctx).WithError(err).Errorf("could not delete %v", dgst) 378 continue 379 } 380 381 if err := cs.Delete(ctx, dgst); err != nil { 382 if !errdefs.IsNotFound(err) { 383 if exitError == nil { 384 exitError = err 385 } 386 log.G(ctx).WithError(err).Errorf("could not delete %v", dgst) 387 } 388 continue 389 } 390 391 fmt.Println(dgst) 392 } 393 394 return exitError 395 }, 396 } 397 398 // TODO(stevvooe): Create "multi-fetch" mode that just takes a remote 399 // then receives object/hint lines on stdin, returning content as 400 // needed. 401 fetchObjectCommand = cli.Command{ 402 Name: "fetch-object", 403 Usage: "retrieve objects from a remote", 404 ArgsUsage: "[flags] <remote> <object> [<hint>, ...]", 405 Description: `Fetch objects by identifier from a remote.`, 406 Flags: commands.RegistryFlags, 407 Action: func(context *cli.Context) error { 408 var ( 409 ref = context.Args().First() 410 ) 411 ctx, cancel := commands.AppContext(context) 412 defer cancel() 413 414 resolver, err := commands.GetResolver(ctx, context) 415 if err != nil { 416 return err 417 } 418 419 ctx = log.WithLogger(ctx, log.G(ctx).WithField("ref", ref)) 420 421 log.G(ctx).Debugf("resolving") 422 name, desc, err := resolver.Resolve(ctx, ref) 423 if err != nil { 424 return err 425 } 426 fetcher, err := resolver.Fetcher(ctx, name) 427 if err != nil { 428 return err 429 } 430 431 log.G(ctx).Debugf("fetching") 432 rc, err := fetcher.Fetch(ctx, desc) 433 if err != nil { 434 return err 435 } 436 defer rc.Close() 437 438 _, err = io.Copy(os.Stdout, rc) 439 return err 440 }, 441 } 442 443 pushObjectCommand = cli.Command{ 444 Name: "push-object", 445 Usage: "push an object to a remote", 446 ArgsUsage: "[flags] <remote> <object> <type>", 447 Description: `Push objects by identifier to a remote.`, 448 Flags: commands.RegistryFlags, 449 Action: func(context *cli.Context) error { 450 var ( 451 ref = context.Args().Get(0) 452 object = context.Args().Get(1) 453 media = context.Args().Get(2) 454 ) 455 dgst, err := digest.Parse(object) 456 if err != nil { 457 return err 458 } 459 client, ctx, cancel, err := commands.NewClient(context) 460 if err != nil { 461 return err 462 } 463 defer cancel() 464 465 resolver, err := commands.GetResolver(ctx, context) 466 if err != nil { 467 return err 468 } 469 470 ctx = log.WithLogger(ctx, log.G(ctx).WithField("ref", ref)) 471 472 log.G(ctx).Debugf("resolving") 473 pusher, err := resolver.Pusher(ctx, ref) 474 if err != nil { 475 return err 476 } 477 478 cs := client.ContentStore() 479 480 info, err := cs.Info(ctx, dgst) 481 if err != nil { 482 return err 483 } 484 desc := ocispec.Descriptor{ 485 MediaType: media, 486 Digest: dgst, 487 Size: info.Size, 488 } 489 490 ra, err := cs.ReaderAt(ctx, desc) 491 if err != nil { 492 return err 493 } 494 defer ra.Close() 495 496 cw, err := pusher.Push(ctx, desc) 497 if err != nil { 498 return err 499 } 500 501 // TODO: Progress reader 502 if err := content.Copy(ctx, cw, content.NewReader(ra), desc.Size, desc.Digest); err != nil { 503 return err 504 } 505 506 fmt.Printf("Pushed %s %s\n", desc.Digest, desc.MediaType) 507 508 return nil 509 }, 510 } 511 ) 512 513 func edit(context *cli.Context, rd io.Reader) (io.ReadCloser, error) { 514 editor := context.String("editor") 515 if editor == "" { 516 return nil, fmt.Errorf("editor is required") 517 } 518 519 tmp, err := ioutil.TempFile(os.Getenv("XDG_RUNTIME_DIR"), "edit-") 520 if err != nil { 521 return nil, err 522 } 523 524 if _, err := io.Copy(tmp, rd); err != nil { 525 tmp.Close() 526 return nil, err 527 } 528 529 cmd := exec.Command("sh", "-c", fmt.Sprintf("%s %s", editor, tmp.Name())) 530 531 cmd.Stdin = os.Stdin 532 cmd.Stdout = os.Stdout 533 cmd.Stderr = os.Stderr 534 cmd.Env = os.Environ() 535 536 if err := cmd.Run(); err != nil { 537 tmp.Close() 538 return nil, err 539 } 540 541 if _, err := tmp.Seek(0, io.SeekStart); err != nil { 542 tmp.Close() 543 return nil, err 544 } 545 546 return onCloser{ReadCloser: tmp, onClose: func() error { 547 return os.RemoveAll(tmp.Name()) 548 }}, nil 549 } 550 551 type onCloser struct { 552 io.ReadCloser 553 onClose func() error 554 } 555 556 func (oc onCloser) Close() error { 557 var err error 558 if err1 := oc.ReadCloser.Close(); err1 != nil { 559 err = err1 560 } 561 562 if oc.onClose != nil { 563 err1 := oc.onClose() 564 if err == nil { 565 err = err1 566 } 567 } 568 569 return err 570 }