github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/cmd/ctr/commands/snapshots/snapshots.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 snapshots 18 19 import ( 20 gocontext "context" 21 "fmt" 22 "io" 23 "os" 24 "strings" 25 "text/tabwriter" 26 "time" 27 28 "github.com/containerd/containerd/cmd/ctr/commands" 29 "github.com/containerd/containerd/content" 30 "github.com/containerd/containerd/diff" 31 "github.com/containerd/containerd/log" 32 "github.com/containerd/containerd/mount" 33 "github.com/containerd/containerd/pkg/progress" 34 "github.com/containerd/containerd/rootfs" 35 "github.com/containerd/containerd/snapshots" 36 digest "github.com/opencontainers/go-digest" 37 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 38 "github.com/pkg/errors" 39 "github.com/urfave/cli" 40 ) 41 42 // Command is the cli command for managing snapshots 43 var Command = cli.Command{ 44 Name: "snapshots", 45 Aliases: []string{"snapshot"}, 46 Usage: "manage snapshots", 47 Flags: commands.SnapshotterFlags, 48 Subcommands: cli.Commands{ 49 commitCommand, 50 diffCommand, 51 infoCommand, 52 listCommand, 53 mountCommand, 54 prepareCommand, 55 removeCommand, 56 setLabelCommand, 57 treeCommand, 58 unpackCommand, 59 usageCommand, 60 viewCommand, 61 }, 62 } 63 64 var listCommand = cli.Command{ 65 Name: "list", 66 Aliases: []string{"ls"}, 67 Usage: "list snapshots", 68 Action: func(context *cli.Context) error { 69 client, ctx, cancel, err := commands.NewClient(context) 70 if err != nil { 71 return err 72 } 73 defer cancel() 74 var ( 75 snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) 76 tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) 77 ) 78 fmt.Fprintln(tw, "KEY\tPARENT\tKIND\t") 79 if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { 80 fmt.Fprintf(tw, "%v\t%v\t%v\t\n", 81 info.Name, 82 info.Parent, 83 info.Kind) 84 return nil 85 }); err != nil { 86 return err 87 } 88 89 return tw.Flush() 90 }, 91 } 92 93 var diffCommand = cli.Command{ 94 Name: "diff", 95 Usage: "get the diff of two snapshots. the default second snapshot is the first snapshot's parent.", 96 ArgsUsage: "[flags] <idA> [<idB>]", 97 Flags: append([]cli.Flag{ 98 cli.StringFlag{ 99 Name: "media-type", 100 Usage: "media type to use for creating diff", 101 Value: ocispec.MediaTypeImageLayerGzip, 102 }, 103 cli.StringFlag{ 104 Name: "ref", 105 Usage: "content upload reference to use", 106 }, 107 cli.BoolFlag{ 108 Name: "keep", 109 Usage: "keep diff content. up to creator to delete it.", 110 }, 111 }, commands.LabelFlag), 112 Action: func(context *cli.Context) error { 113 var ( 114 idA = context.Args().First() 115 idB = context.Args().Get(1) 116 ) 117 if idA == "" { 118 return errors.New("snapshot id must be provided") 119 } 120 client, ctx, cancel, err := commands.NewClient(context) 121 if err != nil { 122 return err 123 } 124 defer cancel() 125 126 ctx, done, err := client.WithLease(ctx) 127 if err != nil { 128 return err 129 } 130 defer done(ctx) 131 132 var desc ocispec.Descriptor 133 labels := commands.LabelArgs(context.StringSlice("label")) 134 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 135 136 fmt.Println(context.String("media-type")) 137 138 if context.Bool("keep") { 139 labels["containerd.io/gc.root"] = time.Now().UTC().Format(time.RFC3339) 140 } 141 opts := []diff.Opt{ 142 diff.WithMediaType(context.String("media-type")), 143 diff.WithReference(context.String("ref")), 144 diff.WithLabels(labels), 145 } 146 147 if idB == "" { 148 desc, err = rootfs.CreateDiff(ctx, idA, snapshotter, client.DiffService(), opts...) 149 if err != nil { 150 return err 151 } 152 } else { 153 desc, err = withMounts(ctx, idA, snapshotter, func(a []mount.Mount) (ocispec.Descriptor, error) { 154 return withMounts(ctx, idB, snapshotter, func(b []mount.Mount) (ocispec.Descriptor, error) { 155 return client.DiffService().Compare(ctx, a, b, opts...) 156 }) 157 }) 158 if err != nil { 159 return err 160 } 161 } 162 163 ra, err := client.ContentStore().ReaderAt(ctx, desc) 164 if err != nil { 165 return err 166 } 167 _, err = io.Copy(os.Stdout, content.NewReader(ra)) 168 169 return err 170 }, 171 } 172 173 func withMounts(ctx gocontext.Context, id string, sn snapshots.Snapshotter, f func(mounts []mount.Mount) (ocispec.Descriptor, error)) (ocispec.Descriptor, error) { 174 var mounts []mount.Mount 175 info, err := sn.Stat(ctx, id) 176 if err != nil { 177 return ocispec.Descriptor{}, err 178 } 179 if info.Kind == snapshots.KindActive { 180 mounts, err = sn.Mounts(ctx, id) 181 if err != nil { 182 return ocispec.Descriptor{}, err 183 } 184 } else { 185 key := fmt.Sprintf("%s-view-key", id) 186 mounts, err = sn.View(ctx, key, id) 187 if err != nil { 188 return ocispec.Descriptor{}, err 189 } 190 defer sn.Remove(ctx, key) 191 } 192 return f(mounts) 193 } 194 195 var usageCommand = cli.Command{ 196 Name: "usage", 197 Usage: "usage snapshots", 198 ArgsUsage: "[flags] [<key>, ...]", 199 Flags: []cli.Flag{ 200 cli.BoolFlag{ 201 Name: "b", 202 Usage: "display size in bytes", 203 }, 204 }, 205 Action: func(context *cli.Context) error { 206 var displaySize func(int64) string 207 if context.Bool("b") { 208 displaySize = func(s int64) string { 209 return fmt.Sprintf("%d", s) 210 } 211 } else { 212 displaySize = func(s int64) string { 213 return progress.Bytes(s).String() 214 } 215 } 216 client, ctx, cancel, err := commands.NewClient(context) 217 if err != nil { 218 return err 219 } 220 defer cancel() 221 var ( 222 snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) 223 tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) 224 ) 225 fmt.Fprintln(tw, "KEY\tSIZE\tINODES\t") 226 if context.NArg() == 0 { 227 if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { 228 usage, err := snapshotter.Usage(ctx, info.Name) 229 if err != nil { 230 return err 231 } 232 fmt.Fprintf(tw, "%v\t%s\t%d\t\n", info.Name, displaySize(usage.Size), usage.Inodes) 233 return nil 234 }); err != nil { 235 return err 236 } 237 } else { 238 for _, id := range context.Args() { 239 usage, err := snapshotter.Usage(ctx, id) 240 if err != nil { 241 return err 242 } 243 fmt.Fprintf(tw, "%v\t%s\t%d\t\n", id, displaySize(usage.Size), usage.Inodes) 244 } 245 } 246 247 return tw.Flush() 248 }, 249 } 250 251 var removeCommand = cli.Command{ 252 Name: "remove", 253 Aliases: []string{"rm"}, 254 ArgsUsage: "<key> [<key>, ...]", 255 Usage: "remove snapshots", 256 Action: func(context *cli.Context) error { 257 client, ctx, cancel, err := commands.NewClient(context) 258 if err != nil { 259 return err 260 } 261 defer cancel() 262 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 263 for _, key := range context.Args() { 264 err = snapshotter.Remove(ctx, key) 265 if err != nil { 266 return errors.Wrapf(err, "failed to remove %q", key) 267 } 268 } 269 270 return nil 271 }, 272 } 273 274 var prepareCommand = cli.Command{ 275 Name: "prepare", 276 Usage: "prepare a snapshot from a committed snapshot", 277 ArgsUsage: "[flags] <key> [<parent>]", 278 Flags: []cli.Flag{ 279 cli.StringFlag{ 280 Name: "target, t", 281 Usage: "mount target path, will print mount, if provided", 282 }, 283 }, 284 Action: func(context *cli.Context) error { 285 if narg := context.NArg(); narg < 1 || narg > 2 { 286 return cli.ShowSubcommandHelp(context) 287 } 288 var ( 289 target = context.String("target") 290 key = context.Args().Get(0) 291 parent = context.Args().Get(1) 292 ) 293 client, ctx, cancel, err := commands.NewClient(context) 294 if err != nil { 295 return err 296 } 297 defer cancel() 298 299 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 300 labels := map[string]string{ 301 "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), 302 } 303 304 mounts, err := snapshotter.Prepare(ctx, key, parent, snapshots.WithLabels(labels)) 305 if err != nil { 306 return err 307 } 308 309 if target != "" { 310 printMounts(target, mounts) 311 } 312 313 return nil 314 }, 315 } 316 317 var viewCommand = cli.Command{ 318 Name: "view", 319 Usage: "create a read-only snapshot from a committed snapshot", 320 ArgsUsage: "[flags] <key> [<parent>]", 321 Flags: []cli.Flag{ 322 cli.StringFlag{ 323 Name: "target, t", 324 Usage: "mount target path, will print mount, if provided", 325 }, 326 }, 327 Action: func(context *cli.Context) error { 328 if narg := context.NArg(); narg < 1 || narg > 2 { 329 return cli.ShowSubcommandHelp(context) 330 } 331 var ( 332 target = context.String("target") 333 key = context.Args().Get(0) 334 parent = context.Args().Get(1) 335 ) 336 client, ctx, cancel, err := commands.NewClient(context) 337 if err != nil { 338 return err 339 } 340 defer cancel() 341 342 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 343 mounts, err := snapshotter.View(ctx, key, parent) 344 if err != nil { 345 return err 346 } 347 348 if target != "" { 349 printMounts(target, mounts) 350 } 351 352 return nil 353 }, 354 } 355 356 var mountCommand = cli.Command{ 357 Name: "mounts", 358 Aliases: []string{"m", "mount"}, 359 Usage: "mount gets mount commands for the snapshots", 360 ArgsUsage: "<target> <key>", 361 Action: func(context *cli.Context) error { 362 if context.NArg() != 2 { 363 return cli.ShowSubcommandHelp(context) 364 } 365 var ( 366 target = context.Args().Get(0) 367 key = context.Args().Get(1) 368 ) 369 client, ctx, cancel, err := commands.NewClient(context) 370 if err != nil { 371 return err 372 } 373 defer cancel() 374 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 375 mounts, err := snapshotter.Mounts(ctx, key) 376 if err != nil { 377 return err 378 } 379 380 printMounts(target, mounts) 381 382 return nil 383 }, 384 } 385 386 var commitCommand = cli.Command{ 387 Name: "commit", 388 Usage: "commit an active snapshot into the provided name", 389 ArgsUsage: "<key> <active>", 390 Action: func(context *cli.Context) error { 391 if context.NArg() != 2 { 392 return cli.ShowSubcommandHelp(context) 393 } 394 var ( 395 key = context.Args().Get(0) 396 active = context.Args().Get(1) 397 ) 398 client, ctx, cancel, err := commands.NewClient(context) 399 if err != nil { 400 return err 401 } 402 defer cancel() 403 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 404 labels := map[string]string{ 405 "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), 406 } 407 return snapshotter.Commit(ctx, key, active, snapshots.WithLabels(labels)) 408 }, 409 } 410 411 var treeCommand = cli.Command{ 412 Name: "tree", 413 Usage: "display tree view of snapshot branches", 414 Action: func(context *cli.Context) error { 415 client, ctx, cancel, err := commands.NewClient(context) 416 if err != nil { 417 return err 418 } 419 defer cancel() 420 var ( 421 snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) 422 tree = newSnapshotTree() 423 ) 424 425 if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { 426 // Get or create node and add node details 427 tree.add(info) 428 return nil 429 }); err != nil { 430 return err 431 } 432 433 printTree(tree) 434 435 return nil 436 }, 437 } 438 439 var infoCommand = cli.Command{ 440 Name: "info", 441 Usage: "get info about a snapshot", 442 ArgsUsage: "<key>", 443 Action: func(context *cli.Context) error { 444 if context.NArg() != 1 { 445 return cli.ShowSubcommandHelp(context) 446 } 447 448 key := context.Args().Get(0) 449 client, ctx, cancel, err := commands.NewClient(context) 450 if err != nil { 451 return err 452 } 453 defer cancel() 454 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 455 info, err := snapshotter.Stat(ctx, key) 456 if err != nil { 457 return err 458 } 459 460 commands.PrintAsJSON(info) 461 462 return nil 463 }, 464 } 465 466 var setLabelCommand = cli.Command{ 467 Name: "label", 468 Usage: "add labels to content", 469 ArgsUsage: "<name> [<label>=<value> ...]", 470 Description: "labels snapshots in the snapshotter", 471 Action: func(context *cli.Context) error { 472 key, labels := commands.ObjectWithLabelArgs(context) 473 client, ctx, cancel, err := commands.NewClient(context) 474 if err != nil { 475 return err 476 } 477 defer cancel() 478 479 snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) 480 481 info := snapshots.Info{ 482 Name: key, 483 Labels: map[string]string{}, 484 } 485 486 var paths []string 487 for k, v := range labels { 488 paths = append(paths, fmt.Sprintf("labels.%s", k)) 489 if v != "" { 490 info.Labels[k] = v 491 } 492 } 493 494 // Nothing updated, do no clear 495 if len(paths) == 0 { 496 info, err = snapshotter.Stat(ctx, info.Name) 497 } else { 498 info, err = snapshotter.Update(ctx, info, paths...) 499 } 500 if err != nil { 501 return err 502 } 503 504 var labelStrings []string 505 for k, v := range info.Labels { 506 labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v)) 507 } 508 509 fmt.Println(strings.Join(labelStrings, ",")) 510 511 return nil 512 }, 513 } 514 515 var unpackCommand = cli.Command{ 516 Name: "unpack", 517 Usage: "unpack applies layers from a manifest to a snapshot", 518 ArgsUsage: "[flags] <digest>", 519 Flags: commands.SnapshotterFlags, 520 Action: func(context *cli.Context) error { 521 dgst, err := digest.Parse(context.Args().First()) 522 if err != nil { 523 return err 524 } 525 client, ctx, cancel, err := commands.NewClient(context) 526 if err != nil { 527 return err 528 } 529 defer cancel() 530 log.G(ctx).Debugf("unpacking layers from manifest %s", dgst.String()) 531 // TODO: Support unpack by name 532 images, err := client.ListImages(ctx) 533 if err != nil { 534 return err 535 } 536 var unpacked bool 537 for _, image := range images { 538 if image.Target().Digest == dgst { 539 fmt.Printf("unpacking %s (%s)...", dgst, image.Target().MediaType) 540 if err := image.Unpack(ctx, context.String("snapshotter")); err != nil { 541 fmt.Println() 542 return err 543 } 544 fmt.Println("done") 545 unpacked = true 546 break 547 } 548 } 549 if !unpacked { 550 return errors.New("manifest not found") 551 } 552 // TODO: Get rootfs from Image 553 //log.G(ctx).Infof("chain ID: %s", chainID.String()) 554 return nil 555 }, 556 } 557 558 type snapshotTree struct { 559 nodes []*snapshotTreeNode 560 index map[string]*snapshotTreeNode 561 } 562 563 func newSnapshotTree() *snapshotTree { 564 return &snapshotTree{ 565 index: make(map[string]*snapshotTreeNode), 566 } 567 } 568 569 type snapshotTreeNode struct { 570 info snapshots.Info 571 children []string 572 } 573 574 func (st *snapshotTree) add(info snapshots.Info) *snapshotTreeNode { 575 entry, ok := st.index[info.Name] 576 if !ok { 577 entry = &snapshotTreeNode{info: info} 578 st.nodes = append(st.nodes, entry) 579 st.index[info.Name] = entry 580 } else { 581 entry.info = info // update info if we created placeholder 582 } 583 584 if info.Parent != "" { 585 pn := st.get(info.Parent) 586 if pn == nil { 587 // create a placeholder 588 pn = st.add(snapshots.Info{Name: info.Parent}) 589 } 590 591 pn.children = append(pn.children, info.Name) 592 } 593 return entry 594 } 595 596 func (st *snapshotTree) get(name string) *snapshotTreeNode { 597 return st.index[name] 598 } 599 600 func printTree(st *snapshotTree) { 601 for _, node := range st.nodes { 602 // Print for root(parent-less) nodes only 603 if node.info.Parent == "" { 604 printNode(node.info.Name, st, 0) 605 } 606 } 607 } 608 609 func printNode(name string, tree *snapshotTree, level int) { 610 node := tree.index[name] 611 prefix := strings.Repeat(" ", level) 612 613 if level > 0 { 614 prefix += "\\_" 615 } 616 617 fmt.Printf(prefix+" %s\n", node.info.Name) 618 level++ 619 for _, child := range node.children { 620 printNode(child, tree, level) 621 } 622 } 623 624 func printMounts(target string, mounts []mount.Mount) { 625 // FIXME: This is specific to Unix 626 for _, m := range mounts { 627 fmt.Printf("mount -t %s %s %s -o %s\n", m.Type, m.Source, target, strings.Join(m.Options, ",")) 628 } 629 }