/* Copyright The containerd Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package snapshots import ( gocontext "context" "fmt" "io" "os" "strings" "text/tabwriter" "time" "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/content" "github.com/containerd/containerd/diff" "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/pkg/progress" "github.com/containerd/containerd/rootfs" "github.com/containerd/containerd/snapshots" digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/urfave/cli" ) // Command is the cli command for managing snapshots var Command = cli.Command{ Name: "snapshots", Aliases: []string{"snapshot"}, Usage: "manage snapshots", Flags: commands.SnapshotterFlags, Subcommands: cli.Commands{ commitCommand, diffCommand, infoCommand, listCommand, mountCommand, prepareCommand, removeCommand, setLabelCommand, treeCommand, unpackCommand, usageCommand, viewCommand, }, } var listCommand = cli.Command{ Name: "list", Aliases: []string{"ls"}, Usage: "list snapshots", Action: func(context *cli.Context) error { client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() var ( snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) ) fmt.Fprintln(tw, "KEY\tPARENT\tKIND\t") if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { fmt.Fprintf(tw, "%v\t%v\t%v\t\n", info.Name, info.Parent, info.Kind) return nil }); err != nil { return err } return tw.Flush() }, } var diffCommand = cli.Command{ Name: "diff", Usage: "get the diff of two snapshots. the default second snapshot is the first snapshot's parent.", ArgsUsage: "[flags] []", Flags: append([]cli.Flag{ cli.StringFlag{ Name: "media-type", Usage: "media type to use for creating diff", Value: ocispec.MediaTypeImageLayerGzip, }, cli.StringFlag{ Name: "ref", Usage: "content upload reference to use", }, cli.BoolFlag{ Name: "keep", Usage: "keep diff content. up to creator to delete it.", }, }, commands.LabelFlag), Action: func(context *cli.Context) error { var ( idA = context.Args().First() idB = context.Args().Get(1) ) if idA == "" { return errors.New("snapshot id must be provided") } client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() ctx, done, err := client.WithLease(ctx) if err != nil { return err } defer done(ctx) var desc ocispec.Descriptor labels := commands.LabelArgs(context.StringSlice("label")) snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) fmt.Println(context.String("media-type")) if context.Bool("keep") { labels["containerd.io/gc.root"] = time.Now().UTC().Format(time.RFC3339) } opts := []diff.Opt{ diff.WithMediaType(context.String("media-type")), diff.WithReference(context.String("ref")), diff.WithLabels(labels), } if idB == "" { desc, err = rootfs.CreateDiff(ctx, idA, snapshotter, client.DiffService(), opts...) if err != nil { return err } } else { desc, err = withMounts(ctx, idA, snapshotter, func(a []mount.Mount) (ocispec.Descriptor, error) { return withMounts(ctx, idB, snapshotter, func(b []mount.Mount) (ocispec.Descriptor, error) { return client.DiffService().Compare(ctx, a, b, opts...) }) }) if err != nil { return err } } ra, err := client.ContentStore().ReaderAt(ctx, desc) if err != nil { return err } _, err = io.Copy(os.Stdout, content.NewReader(ra)) return err }, } func withMounts(ctx gocontext.Context, id string, sn snapshots.Snapshotter, f func(mounts []mount.Mount) (ocispec.Descriptor, error)) (ocispec.Descriptor, error) { var mounts []mount.Mount info, err := sn.Stat(ctx, id) if err != nil { return ocispec.Descriptor{}, err } if info.Kind == snapshots.KindActive { mounts, err = sn.Mounts(ctx, id) if err != nil { return ocispec.Descriptor{}, err } } else { key := fmt.Sprintf("%s-view-key", id) mounts, err = sn.View(ctx, key, id) if err != nil { return ocispec.Descriptor{}, err } defer sn.Remove(ctx, key) } return f(mounts) } var usageCommand = cli.Command{ Name: "usage", Usage: "usage snapshots", ArgsUsage: "[flags] [, ...]", Flags: []cli.Flag{ cli.BoolFlag{ Name: "b", Usage: "display size in bytes", }, }, Action: func(context *cli.Context) error { var displaySize func(int64) string if context.Bool("b") { displaySize = func(s int64) string { return fmt.Sprintf("%d", s) } } else { displaySize = func(s int64) string { return progress.Bytes(s).String() } } client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() var ( snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) ) fmt.Fprintln(tw, "KEY\tSIZE\tINODES\t") if context.NArg() == 0 { if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { usage, err := snapshotter.Usage(ctx, info.Name) if err != nil { return err } fmt.Fprintf(tw, "%v\t%s\t%d\t\n", info.Name, displaySize(usage.Size), usage.Inodes) return nil }); err != nil { return err } } else { for _, id := range context.Args() { usage, err := snapshotter.Usage(ctx, id) if err != nil { return err } fmt.Fprintf(tw, "%v\t%s\t%d\t\n", id, displaySize(usage.Size), usage.Inodes) } } return tw.Flush() }, } var removeCommand = cli.Command{ Name: "remove", Aliases: []string{"rm"}, ArgsUsage: " [, ...]", Usage: "remove snapshots", Action: func(context *cli.Context) error { client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) for _, key := range context.Args() { err = snapshotter.Remove(ctx, key) if err != nil { return errors.Wrapf(err, "failed to remove %q", key) } } return nil }, } var prepareCommand = cli.Command{ Name: "prepare", Usage: "prepare a snapshot from a committed snapshot", ArgsUsage: "[flags] []", Flags: []cli.Flag{ cli.StringFlag{ Name: "target, t", Usage: "mount target path, will print mount, if provided", }, }, Action: func(context *cli.Context) error { if narg := context.NArg(); narg < 1 || narg > 2 { return cli.ShowSubcommandHelp(context) } var ( target = context.String("target") key = context.Args().Get(0) parent = context.Args().Get(1) ) client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) labels := map[string]string{ "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), } mounts, err := snapshotter.Prepare(ctx, key, parent, snapshots.WithLabels(labels)) if err != nil { return err } if target != "" { printMounts(target, mounts) } return nil }, } var viewCommand = cli.Command{ Name: "view", Usage: "create a read-only snapshot from a committed snapshot", ArgsUsage: "[flags] []", Flags: []cli.Flag{ cli.StringFlag{ Name: "target, t", Usage: "mount target path, will print mount, if provided", }, }, Action: func(context *cli.Context) error { if narg := context.NArg(); narg < 1 || narg > 2 { return cli.ShowSubcommandHelp(context) } var ( target = context.String("target") key = context.Args().Get(0) parent = context.Args().Get(1) ) client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) mounts, err := snapshotter.View(ctx, key, parent) if err != nil { return err } if target != "" { printMounts(target, mounts) } return nil }, } var mountCommand = cli.Command{ Name: "mounts", Aliases: []string{"m", "mount"}, Usage: "mount gets mount commands for the snapshots", ArgsUsage: " ", Action: func(context *cli.Context) error { if context.NArg() != 2 { return cli.ShowSubcommandHelp(context) } var ( target = context.Args().Get(0) key = context.Args().Get(1) ) client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) mounts, err := snapshotter.Mounts(ctx, key) if err != nil { return err } printMounts(target, mounts) return nil }, } var commitCommand = cli.Command{ Name: "commit", Usage: "commit an active snapshot into the provided name", ArgsUsage: " ", Action: func(context *cli.Context) error { if context.NArg() != 2 { return cli.ShowSubcommandHelp(context) } var ( key = context.Args().Get(0) active = context.Args().Get(1) ) client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) labels := map[string]string{ "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), } return snapshotter.Commit(ctx, key, active, snapshots.WithLabels(labels)) }, } var treeCommand = cli.Command{ Name: "tree", Usage: "display tree view of snapshot branches", Action: func(context *cli.Context) error { client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() var ( snapshotter = client.SnapshotService(context.GlobalString("snapshotter")) tree = newSnapshotTree() ) if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error { // Get or create node and add node details tree.add(info) return nil }); err != nil { return err } printTree(tree) return nil }, } var infoCommand = cli.Command{ Name: "info", Usage: "get info about a snapshot", ArgsUsage: "", Action: func(context *cli.Context) error { if context.NArg() != 1 { return cli.ShowSubcommandHelp(context) } key := context.Args().Get(0) client, ctx, cancel, err := commands.NewClient(context) if err != nil { return err } defer cancel() snapshotter := client.SnapshotService(context.GlobalString("snapshotter")) info, err := snapshotter.Stat(ctx, key) if err != nil { return err } commands.PrintAsJSON(info) return nil }, } var setLabelCommand = cli.Command{ Name: "label", Usage: "add labels to content", ArgsUsage: " [