github.com/runcom/containerd@v0.0.0-20160708090337-9bff9f934c0d/ctr/container.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"net"
    10  	"os"
    11  	"os/signal"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"syscall"
    16  	"text/tabwriter"
    17  	"time"
    18  
    19  	"github.com/codegangsta/cli"
    20  	"github.com/docker/containerd/api/grpc/types"
    21  	"github.com/docker/containerd/specs"
    22  	"github.com/docker/docker/pkg/term"
    23  	"github.com/golang/protobuf/ptypes"
    24  	netcontext "golang.org/x/net/context"
    25  	"google.golang.org/grpc"
    26  	"google.golang.org/grpc/grpclog"
    27  	"google.golang.org/grpc/transport"
    28  )
    29  
    30  // TODO: parse flags and pass opts
    31  func getClient(ctx *cli.Context) types.APIClient {
    32  	// Parse proto://address form addresses.
    33  	bindSpec := ctx.GlobalString("address")
    34  	bindParts := strings.SplitN(bindSpec, "://", 2)
    35  	if len(bindParts) != 2 {
    36  		fatal(fmt.Sprintf("bad bind address format %s, expected proto://address", bindSpec), 1)
    37  	}
    38  
    39  	// reset the logger for grpc to log to dev/null so that it does not mess with our stdio
    40  	grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags))
    41  	dialOpts := []grpc.DialOption{grpc.WithInsecure(), grpc.WithTimeout(ctx.GlobalDuration("conn-timeout"))}
    42  	dialOpts = append(dialOpts,
    43  		grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
    44  			return net.DialTimeout(bindParts[0], bindParts[1], timeout)
    45  		},
    46  		))
    47  	conn, err := grpc.Dial(bindSpec, dialOpts...)
    48  	if err != nil {
    49  		fatal(err.Error(), 1)
    50  	}
    51  	return types.NewAPIClient(conn)
    52  }
    53  
    54  var contSubCmds = []cli.Command{
    55  	execCommand,
    56  	killCommand,
    57  	listCommand,
    58  	pauseCommand,
    59  	resumeCommand,
    60  	startCommand,
    61  	stateCommand,
    62  	statsCommand,
    63  	watchCommand,
    64  	updateCommand,
    65  }
    66  
    67  var containersCommand = cli.Command{
    68  	Name:        "containers",
    69  	Usage:       "interact with running containers",
    70  	ArgsUsage:   "COMMAND [arguments...]",
    71  	Subcommands: contSubCmds,
    72  	Description: func() string {
    73  		desc := "\n    COMMAND:\n"
    74  		for _, command := range contSubCmds {
    75  			desc += fmt.Sprintf("    %-10.10s%s\n", command.Name, command.Usage)
    76  		}
    77  		return desc
    78  	}(),
    79  	Action: listContainers,
    80  }
    81  
    82  var stateCommand = cli.Command{
    83  	Name:  "state",
    84  	Usage: "get a raw dump of the containerd state",
    85  	Action: func(context *cli.Context) {
    86  		c := getClient(context)
    87  		resp, err := c.State(netcontext.Background(), &types.StateRequest{
    88  			Id: context.Args().First(),
    89  		})
    90  		if err != nil {
    91  			fatal(err.Error(), 1)
    92  		}
    93  		data, err := json.Marshal(resp)
    94  		if err != nil {
    95  			fatal(err.Error(), 1)
    96  		}
    97  		fmt.Print(string(data))
    98  	},
    99  }
   100  
   101  var listCommand = cli.Command{
   102  	Name:   "list",
   103  	Usage:  "list all running containers",
   104  	Action: listContainers,
   105  }
   106  
   107  func listContainers(context *cli.Context) {
   108  	c := getClient(context)
   109  	resp, err := c.State(netcontext.Background(), &types.StateRequest{
   110  		Id: context.Args().First(),
   111  	})
   112  	if err != nil {
   113  		fatal(err.Error(), 1)
   114  	}
   115  	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
   116  	fmt.Fprint(w, "ID\tPATH\tSTATUS\tPROCESSES\n")
   117  	sortContainers(resp.Containers)
   118  	for _, c := range resp.Containers {
   119  		procs := []string{}
   120  		for _, p := range c.Processes {
   121  			procs = append(procs, p.Pid)
   122  		}
   123  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Id, c.BundlePath, c.Status, strings.Join(procs, ","))
   124  	}
   125  	if err := w.Flush(); err != nil {
   126  		fatal(err.Error(), 1)
   127  	}
   128  }
   129  
   130  var startCommand = cli.Command{
   131  	Name:      "start",
   132  	Usage:     "start a container",
   133  	ArgsUsage: "ID BundlePath",
   134  	Flags: []cli.Flag{
   135  		cli.StringFlag{
   136  			Name:  "checkpoint,c",
   137  			Value: "",
   138  			Usage: "checkpoint to start the container from",
   139  		},
   140  		cli.StringFlag{
   141  			Name:  "checkpoint-dir",
   142  			Value: "",
   143  			Usage: "path to checkpoint directory",
   144  		},
   145  		cli.BoolFlag{
   146  			Name:  "attach,a",
   147  			Usage: "connect to the stdio of the container",
   148  		},
   149  		cli.StringSliceFlag{
   150  			Name:  "label,l",
   151  			Value: &cli.StringSlice{},
   152  			Usage: "set labels for the container",
   153  		},
   154  		cli.BoolFlag{
   155  			Name:  "no-pivot",
   156  			Usage: "do not use pivot root",
   157  		},
   158  		cli.StringFlag{
   159  			Name:  "runtime,r",
   160  			Value: "runc",
   161  			Usage: "name or path of the OCI compliant runtime to use when executing containers",
   162  		},
   163  		cli.StringSliceFlag{
   164  			Name:  "runtime-args",
   165  			Value: &cli.StringSlice{},
   166  			Usage: "specify additional runtime args",
   167  		},
   168  	},
   169  	Action: func(context *cli.Context) {
   170  		var (
   171  			id   = context.Args().Get(0)
   172  			path = context.Args().Get(1)
   173  		)
   174  		if path == "" {
   175  			fatal("bundle path cannot be empty", ExitStatusMissingArg)
   176  		}
   177  		if id == "" {
   178  			fatal("container id cannot be empty", ExitStatusMissingArg)
   179  		}
   180  		bpath, err := filepath.Abs(path)
   181  		if err != nil {
   182  			fatal(fmt.Sprintf("cannot get the absolute path of the bundle: %v", err), 1)
   183  		}
   184  		s, err := createStdio()
   185  		defer func() {
   186  			if s.stdin != "" {
   187  				os.RemoveAll(filepath.Dir(s.stdin))
   188  			}
   189  		}()
   190  		if err != nil {
   191  			fatal(err.Error(), 1)
   192  		}
   193  		var (
   194  			restoreAndCloseStdin func()
   195  			tty                  bool
   196  			c                    = getClient(context)
   197  			r                    = &types.CreateContainerRequest{
   198  				Id:            id,
   199  				BundlePath:    bpath,
   200  				Checkpoint:    context.String("checkpoint"),
   201  				CheckpointDir: context.String("checkpoint-dir"),
   202  				Stdin:         s.stdin,
   203  				Stdout:        s.stdout,
   204  				Stderr:        s.stderr,
   205  				Labels:        context.StringSlice("label"),
   206  				NoPivotRoot:   context.Bool("no-pivot"),
   207  				Runtime:       context.String("runtime"),
   208  				RuntimeArgs:   context.StringSlice("runtime-args"),
   209  			}
   210  		)
   211  		restoreAndCloseStdin = func() {
   212  			if state != nil {
   213  				term.RestoreTerminal(os.Stdin.Fd(), state)
   214  			}
   215  			if stdin != nil {
   216  				stdin.Close()
   217  			}
   218  		}
   219  		defer restoreAndCloseStdin()
   220  		if context.Bool("attach") {
   221  			mkterm, err := readTermSetting(bpath)
   222  			if err != nil {
   223  				fatal(err.Error(), 1)
   224  			}
   225  			tty = mkterm
   226  			if mkterm {
   227  				s, err := term.SetRawTerminal(os.Stdin.Fd())
   228  				if err != nil {
   229  					fatal(err.Error(), 1)
   230  				}
   231  				state = s
   232  			}
   233  			if err := attachStdio(s); err != nil {
   234  				fatal(err.Error(), 1)
   235  			}
   236  		}
   237  		events, err := c.Events(netcontext.Background(), &types.EventsRequest{})
   238  		if err != nil {
   239  			fatal(err.Error(), 1)
   240  		}
   241  		if _, err := c.CreateContainer(netcontext.Background(), r); err != nil {
   242  			fatal(err.Error(), 1)
   243  		}
   244  		if context.Bool("attach") {
   245  			go func() {
   246  				io.Copy(stdin, os.Stdin)
   247  				if _, err := c.UpdateProcess(netcontext.Background(), &types.UpdateProcessRequest{
   248  					Id:         id,
   249  					Pid:        "init",
   250  					CloseStdin: true,
   251  				}); err != nil {
   252  					fatal(err.Error(), 1)
   253  				}
   254  				restoreAndCloseStdin()
   255  			}()
   256  			if tty {
   257  				resize(id, "init", c)
   258  				go func() {
   259  					s := make(chan os.Signal, 64)
   260  					signal.Notify(s, syscall.SIGWINCH)
   261  					for range s {
   262  						if err := resize(id, "init", c); err != nil {
   263  							log.Println(err)
   264  						}
   265  					}
   266  				}()
   267  			}
   268  			waitForExit(c, events, id, "init", restoreAndCloseStdin)
   269  		}
   270  	},
   271  }
   272  
   273  func resize(id, pid string, c types.APIClient) error {
   274  	ws, err := term.GetWinsize(os.Stdin.Fd())
   275  	if err != nil {
   276  		return err
   277  	}
   278  	if _, err := c.UpdateProcess(netcontext.Background(), &types.UpdateProcessRequest{
   279  		Id:     id,
   280  		Pid:    "init",
   281  		Width:  uint32(ws.Width),
   282  		Height: uint32(ws.Height),
   283  	}); err != nil {
   284  		return err
   285  	}
   286  	return nil
   287  }
   288  
   289  var (
   290  	stdin io.WriteCloser
   291  	state *term.State
   292  )
   293  
   294  // readTermSetting reads the Terminal option out of the specs configuration
   295  // to know if ctr should allocate a pty
   296  func readTermSetting(path string) (bool, error) {
   297  	f, err := os.Open(filepath.Join(path, "config.json"))
   298  	if err != nil {
   299  		return false, err
   300  	}
   301  	defer f.Close()
   302  	var spec specs.Spec
   303  	if err := json.NewDecoder(f).Decode(&spec); err != nil {
   304  		return false, err
   305  	}
   306  	return spec.Process.Terminal, nil
   307  }
   308  
   309  func attachStdio(s stdio) error {
   310  	stdinf, err := os.OpenFile(s.stdin, syscall.O_RDWR, 0)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	// FIXME: assign to global
   315  	stdin = stdinf
   316  	stdoutf, err := os.OpenFile(s.stdout, syscall.O_RDWR, 0)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	go io.Copy(os.Stdout, stdoutf)
   321  	stderrf, err := os.OpenFile(s.stderr, syscall.O_RDWR, 0)
   322  	if err != nil {
   323  		return err
   324  	}
   325  	go io.Copy(os.Stderr, stderrf)
   326  	return nil
   327  }
   328  
   329  var watchCommand = cli.Command{
   330  	Name:  "watch",
   331  	Usage: "print container events",
   332  	Action: func(context *cli.Context) {
   333  		c := getClient(context)
   334  		id := context.Args().First()
   335  		if id != "" {
   336  			resp, err := c.State(netcontext.Background(), &types.StateRequest{Id: id})
   337  			if err != nil {
   338  				fatal(err.Error(), 1)
   339  			}
   340  			for _, c := range resp.Containers {
   341  				if c.Id == id {
   342  					break
   343  				}
   344  			}
   345  			if id == "" {
   346  				fatal("Invalid container id", 1)
   347  			}
   348  		}
   349  		events, reqErr := c.Events(netcontext.Background(), &types.EventsRequest{})
   350  		if reqErr != nil {
   351  			fatal(reqErr.Error(), 1)
   352  		}
   353  
   354  		for {
   355  			e, err := events.Recv()
   356  			if err != nil {
   357  				fatal(err.Error(), 1)
   358  			}
   359  
   360  			if id == "" || e.Id == id {
   361  				fmt.Printf("%#v\n", e)
   362  			}
   363  		}
   364  	},
   365  }
   366  
   367  var pauseCommand = cli.Command{
   368  	Name:  "pause",
   369  	Usage: "pause a container",
   370  	Action: func(context *cli.Context) {
   371  		id := context.Args().First()
   372  		if id == "" {
   373  			fatal("container id cannot be empty", ExitStatusMissingArg)
   374  		}
   375  		c := getClient(context)
   376  		_, err := c.UpdateContainer(netcontext.Background(), &types.UpdateContainerRequest{
   377  			Id:     id,
   378  			Pid:    "init",
   379  			Status: "paused",
   380  		})
   381  		if err != nil {
   382  			fatal(err.Error(), 1)
   383  		}
   384  	},
   385  }
   386  
   387  var resumeCommand = cli.Command{
   388  	Name:  "resume",
   389  	Usage: "resume a paused container",
   390  	Action: func(context *cli.Context) {
   391  		id := context.Args().First()
   392  		if id == "" {
   393  			fatal("container id cannot be empty", ExitStatusMissingArg)
   394  		}
   395  		c := getClient(context)
   396  		_, err := c.UpdateContainer(netcontext.Background(), &types.UpdateContainerRequest{
   397  			Id:     id,
   398  			Pid:    "init",
   399  			Status: "running",
   400  		})
   401  		if err != nil {
   402  			fatal(err.Error(), 1)
   403  		}
   404  	},
   405  }
   406  
   407  var killCommand = cli.Command{
   408  	Name:  "kill",
   409  	Usage: "send a signal to a container or its processes",
   410  	Flags: []cli.Flag{
   411  		cli.StringFlag{
   412  			Name:  "pid,p",
   413  			Value: "init",
   414  			Usage: "pid of the process to signal within the container",
   415  		},
   416  		cli.IntFlag{
   417  			Name:  "signal,s",
   418  			Value: 15,
   419  			Usage: "signal to send to the container",
   420  		},
   421  	},
   422  	Action: func(context *cli.Context) {
   423  		id := context.Args().First()
   424  		if id == "" {
   425  			fatal("container id cannot be empty", ExitStatusMissingArg)
   426  		}
   427  		c := getClient(context)
   428  		if _, err := c.Signal(netcontext.Background(), &types.SignalRequest{
   429  			Id:     id,
   430  			Pid:    context.String("pid"),
   431  			Signal: uint32(context.Int("signal")),
   432  		}); err != nil {
   433  			fatal(err.Error(), 1)
   434  		}
   435  	},
   436  }
   437  
   438  var execCommand = cli.Command{
   439  	Name:  "exec",
   440  	Usage: "exec another process in an existing container",
   441  	Flags: []cli.Flag{
   442  		cli.StringFlag{
   443  			Name:  "id",
   444  			Usage: "container id to add the process to",
   445  		},
   446  		cli.StringFlag{
   447  			Name:  "pid",
   448  			Usage: "process id for the new process",
   449  		},
   450  		cli.BoolFlag{
   451  			Name:  "attach,a",
   452  			Usage: "connect to the stdio of the container",
   453  		},
   454  		cli.StringFlag{
   455  			Name:  "cwd",
   456  			Usage: "current working directory for the process",
   457  		},
   458  		cli.BoolFlag{
   459  			Name:  "tty,t",
   460  			Usage: "create a terminal for the process",
   461  		},
   462  		cli.StringSliceFlag{
   463  			Name:  "env,e",
   464  			Value: &cli.StringSlice{},
   465  			Usage: "environment variables for the process",
   466  		},
   467  		cli.IntFlag{
   468  			Name:  "uid,u",
   469  			Usage: "user id of the user for the process",
   470  		},
   471  		cli.IntFlag{
   472  			Name:  "gid,g",
   473  			Usage: "group id of the user for the process",
   474  		},
   475  	},
   476  	Action: func(context *cli.Context) {
   477  		var restoreAndCloseStdin func()
   478  
   479  		p := &types.AddProcessRequest{
   480  			Id:       context.String("id"),
   481  			Pid:      context.String("pid"),
   482  			Args:     context.Args(),
   483  			Cwd:      context.String("cwd"),
   484  			Terminal: context.Bool("tty"),
   485  			Env:      context.StringSlice("env"),
   486  			User: &types.User{
   487  				Uid: uint32(context.Int("uid")),
   488  				Gid: uint32(context.Int("gid")),
   489  			},
   490  		}
   491  		s, err := createStdio()
   492  		defer func() {
   493  			if s.stdin != "" {
   494  				os.RemoveAll(filepath.Dir(s.stdin))
   495  			}
   496  		}()
   497  		if err != nil {
   498  			fatal(err.Error(), 1)
   499  		}
   500  		p.Stdin = s.stdin
   501  		p.Stdout = s.stdout
   502  		p.Stderr = s.stderr
   503  		restoreAndCloseStdin = func() {
   504  			if state != nil {
   505  				term.RestoreTerminal(os.Stdin.Fd(), state)
   506  			}
   507  			if stdin != nil {
   508  				stdin.Close()
   509  			}
   510  		}
   511  		defer restoreAndCloseStdin()
   512  		if context.Bool("attach") {
   513  			if context.Bool("tty") {
   514  				s, err := term.SetRawTerminal(os.Stdin.Fd())
   515  				if err != nil {
   516  					fatal(err.Error(), 1)
   517  				}
   518  				state = s
   519  			}
   520  			if err := attachStdio(s); err != nil {
   521  				fatal(err.Error(), 1)
   522  			}
   523  		}
   524  		c := getClient(context)
   525  		events, err := c.Events(netcontext.Background(), &types.EventsRequest{})
   526  		if err != nil {
   527  			fatal(err.Error(), 1)
   528  		}
   529  		if _, err := c.AddProcess(netcontext.Background(), p); err != nil {
   530  			fatal(err.Error(), 1)
   531  		}
   532  		if context.Bool("attach") {
   533  			go func() {
   534  				io.Copy(stdin, os.Stdin)
   535  				if _, err := c.UpdateProcess(netcontext.Background(), &types.UpdateProcessRequest{
   536  					Id:         p.Id,
   537  					Pid:        p.Pid,
   538  					CloseStdin: true,
   539  				}); err != nil {
   540  					log.Println(err)
   541  				}
   542  				restoreAndCloseStdin()
   543  			}()
   544  			if context.Bool("tty") {
   545  				resize(p.Id, p.Pid, c)
   546  				go func() {
   547  					s := make(chan os.Signal, 64)
   548  					signal.Notify(s, syscall.SIGWINCH)
   549  					for range s {
   550  						if err := resize(p.Id, p.Pid, c); err != nil {
   551  							log.Println(err)
   552  						}
   553  					}
   554  				}()
   555  			}
   556  			waitForExit(c, events, context.String("id"), context.String("pid"), restoreAndCloseStdin)
   557  		}
   558  	},
   559  }
   560  
   561  var statsCommand = cli.Command{
   562  	Name:  "stats",
   563  	Usage: "get stats for running container",
   564  	Action: func(context *cli.Context) {
   565  		req := &types.StatsRequest{
   566  			Id: context.Args().First(),
   567  		}
   568  		c := getClient(context)
   569  		stats, err := c.Stats(netcontext.Background(), req)
   570  		if err != nil {
   571  			fatal(err.Error(), 1)
   572  		}
   573  		data, err := json.Marshal(stats)
   574  		if err != nil {
   575  			fatal(err.Error(), 1)
   576  		}
   577  		fmt.Print(string(data))
   578  	},
   579  }
   580  
   581  func getUpdateCommandInt64Flag(context *cli.Context, name string) uint64 {
   582  	str := context.String(name)
   583  	if str == "" {
   584  		return 0
   585  	}
   586  
   587  	val, err := strconv.ParseUint(str, 0, 64)
   588  	if err != nil {
   589  		fatal(err.Error(), 1)
   590  	}
   591  
   592  	return val
   593  }
   594  
   595  var updateCommand = cli.Command{
   596  	Name:  "update",
   597  	Usage: "update a containers resources",
   598  	Flags: []cli.Flag{
   599  		cli.StringFlag{
   600  			Name: "memory-limit",
   601  		},
   602  		cli.StringFlag{
   603  			Name: "memory-reservation",
   604  		},
   605  		cli.StringFlag{
   606  			Name: "memory-swap",
   607  		},
   608  		cli.StringFlag{
   609  			Name: "cpu-quota",
   610  		},
   611  		cli.StringFlag{
   612  			Name: "cpu-period",
   613  		},
   614  		cli.StringFlag{
   615  			Name: "kernel-limit",
   616  		},
   617  		cli.StringFlag{
   618  			Name: "kernel-tcp-limit",
   619  		},
   620  		cli.StringFlag{
   621  			Name: "blkio-weight",
   622  		},
   623  		cli.StringFlag{
   624  			Name: "cpuset-cpus",
   625  		},
   626  		cli.StringFlag{
   627  			Name: "cpuset-mems",
   628  		},
   629  	},
   630  	Action: func(context *cli.Context) {
   631  		req := &types.UpdateContainerRequest{
   632  			Id: context.Args().First(),
   633  		}
   634  		req.Resources = &types.UpdateResource{}
   635  		req.Resources.MemoryLimit = getUpdateCommandInt64Flag(context, "memory-limit")
   636  		req.Resources.MemoryReservation = getUpdateCommandInt64Flag(context, "memory-reservation")
   637  		req.Resources.MemorySwap = getUpdateCommandInt64Flag(context, "memory-swap")
   638  		req.Resources.BlkioWeight = getUpdateCommandInt64Flag(context, "blkio-weight")
   639  		req.Resources.CpuPeriod = getUpdateCommandInt64Flag(context, "cpu-period")
   640  		req.Resources.CpuQuota = getUpdateCommandInt64Flag(context, "cpu-quota")
   641  		req.Resources.CpuShares = getUpdateCommandInt64Flag(context, "cpu-shares")
   642  		req.Resources.CpusetCpus = context.String("cpuset-cpus")
   643  		req.Resources.CpusetMems = context.String("cpuset-mems")
   644  		req.Resources.KernelMemoryLimit = getUpdateCommandInt64Flag(context, "kernel-limit")
   645  		req.Resources.KernelTCPMemoryLimit = getUpdateCommandInt64Flag(context, "kernel-tcp-limit")
   646  		c := getClient(context)
   647  		if _, err := c.UpdateContainer(netcontext.Background(), req); err != nil {
   648  			fatal(err.Error(), 1)
   649  		}
   650  	},
   651  }
   652  
   653  func waitForExit(c types.APIClient, events types.API_EventsClient, id, pid string, closer func()) {
   654  	timestamp := time.Now()
   655  	for {
   656  		e, err := events.Recv()
   657  		if err != nil {
   658  			if grpc.ErrorDesc(err) == transport.ErrConnClosing.Desc {
   659  				closer()
   660  				os.Exit(128 + int(syscall.SIGHUP))
   661  			}
   662  			time.Sleep(1 * time.Second)
   663  			tsp, err := ptypes.TimestampProto(timestamp)
   664  			if err != nil {
   665  				closer()
   666  				fmt.Fprintf(os.Stderr, "%s", err.Error())
   667  				os.Exit(1)
   668  			}
   669  			events, _ = c.Events(netcontext.Background(), &types.EventsRequest{Timestamp: tsp})
   670  			continue
   671  		}
   672  		timestamp, err = ptypes.Timestamp(e.Timestamp)
   673  		if e.Id == id && e.Type == "exit" && e.Pid == pid {
   674  			closer()
   675  			os.Exit(int(e.Status))
   676  		}
   677  	}
   678  }
   679  
   680  type stdio struct {
   681  	stdin  string
   682  	stdout string
   683  	stderr string
   684  }