github.com/containerd/Containerd@v1.4.13/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  }