github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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  }