380 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			380 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"io/ioutil"
 | 
						|
	"net"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"syscall"
 | 
						|
	"text/tabwriter"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/codegangsta/cli"
 | 
						|
	"github.com/docker/containerd/api/grpc/types"
 | 
						|
	"github.com/docker/docker/pkg/term"
 | 
						|
	"github.com/opencontainers/runc/libcontainer"
 | 
						|
	"github.com/opencontainers/specs"
 | 
						|
	netcontext "golang.org/x/net/context"
 | 
						|
	"google.golang.org/grpc"
 | 
						|
)
 | 
						|
 | 
						|
// TODO: parse flags and pass opts
 | 
						|
func getClient(ctx *cli.Context) types.APIClient {
 | 
						|
	dialOpts := []grpc.DialOption{grpc.WithInsecure()}
 | 
						|
	dialOpts = append(dialOpts,
 | 
						|
		grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) {
 | 
						|
			return net.DialTimeout("unix", addr, timeout)
 | 
						|
		},
 | 
						|
		))
 | 
						|
	conn, err := grpc.Dial(ctx.GlobalString("address"), dialOpts...)
 | 
						|
	if err != nil {
 | 
						|
		fatal(err.Error(), 1)
 | 
						|
	}
 | 
						|
	return types.NewAPIClient(conn)
 | 
						|
}
 | 
						|
 | 
						|
var ContainersCommand = cli.Command{
 | 
						|
	Name:  "containers",
 | 
						|
	Usage: "interact with running containers",
 | 
						|
	Subcommands: []cli.Command{
 | 
						|
		ExecCommand,
 | 
						|
		KillCommand,
 | 
						|
		ListCommand,
 | 
						|
		StartCommand,
 | 
						|
		StatsCommand,
 | 
						|
	},
 | 
						|
	Action: listContainers,
 | 
						|
}
 | 
						|
 | 
						|
var ListCommand = cli.Command{
 | 
						|
	Name:   "list",
 | 
						|
	Usage:  "list all running containers",
 | 
						|
	Action: listContainers,
 | 
						|
}
 | 
						|
 | 
						|
func listContainers(context *cli.Context) {
 | 
						|
	c := getClient(context)
 | 
						|
	resp, err := c.State(netcontext.Background(), &types.StateRequest{})
 | 
						|
	if err != nil {
 | 
						|
		fatal(err.Error(), 1)
 | 
						|
	}
 | 
						|
	w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
 | 
						|
	fmt.Fprint(w, "ID\tPATH\tSTATUS\tPID1\n")
 | 
						|
	for _, c := range resp.Containers {
 | 
						|
		fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", c.Id, c.BundlePath, c.Status, c.Processes[0].Pid)
 | 
						|
	}
 | 
						|
	if err := w.Flush(); err != nil {
 | 
						|
		fatal(err.Error(), 1)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
var StartCommand = cli.Command{
 | 
						|
	Name:  "start",
 | 
						|
	Usage: "start a container",
 | 
						|
	Flags: []cli.Flag{
 | 
						|
		cli.StringFlag{
 | 
						|
			Name:  "checkpoint,c",
 | 
						|
			Value: "",
 | 
						|
			Usage: "checkpoint to start the container from",
 | 
						|
		},
 | 
						|
		cli.BoolFlag{
 | 
						|
			Name:  "attach,a",
 | 
						|
			Usage: "connect to the stdio of the container",
 | 
						|
		},
 | 
						|
	},
 | 
						|
	Action: func(context *cli.Context) {
 | 
						|
		var (
 | 
						|
			id   = context.Args().Get(0)
 | 
						|
			path = context.Args().Get(1)
 | 
						|
		)
 | 
						|
		if path == "" {
 | 
						|
			fatal("bundle path cannot be empty", 1)
 | 
						|
		}
 | 
						|
		if id == "" {
 | 
						|
			fatal("container id cannot be empty", 1)
 | 
						|
		}
 | 
						|
		c := getClient(context)
 | 
						|
		events, err := c.Events(netcontext.Background(), &types.EventsRequest{})
 | 
						|
		if err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
		r := &types.CreateContainerRequest{
 | 
						|
			Id:         id,
 | 
						|
			BundlePath: path,
 | 
						|
			Checkpoint: context.String("checkpoint"),
 | 
						|
		}
 | 
						|
		if context.Bool("attach") {
 | 
						|
			mkterm, err := readTermSetting(path)
 | 
						|
			if err != nil {
 | 
						|
				fatal(err.Error(), 1)
 | 
						|
			}
 | 
						|
			if mkterm {
 | 
						|
				if err := attachTty(&r.Console); err != nil {
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if err := attachStdio(&r.Stdin, &r.Stdout, &r.Stderr); err != nil {
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		resp, err := c.CreateContainer(netcontext.Background(), r)
 | 
						|
		if err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
		if context.Bool("attach") {
 | 
						|
			restoreAndCloseStdin := func() {
 | 
						|
				if state != nil {
 | 
						|
					term.RestoreTerminal(os.Stdin.Fd(), state)
 | 
						|
				}
 | 
						|
				stdin.Close()
 | 
						|
			}
 | 
						|
			go func() {
 | 
						|
				io.Copy(stdin, os.Stdin)
 | 
						|
				restoreAndCloseStdin()
 | 
						|
			}()
 | 
						|
			for {
 | 
						|
				e, err := events.Recv()
 | 
						|
				if err != nil {
 | 
						|
					restoreAndCloseStdin()
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
				if e.Id == id && e.Type == "exit" {
 | 
						|
					restoreAndCloseStdin()
 | 
						|
					os.Exit(int(e.Status))
 | 
						|
				}
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			fmt.Println(resp.Pid)
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	stdin io.WriteCloser
 | 
						|
	state *term.State
 | 
						|
)
 | 
						|
 | 
						|
// readTermSetting reads the Terminal option out of the specs configuration
 | 
						|
// to know if ctr should allocate a pty
 | 
						|
func readTermSetting(path string) (bool, error) {
 | 
						|
	f, err := os.Open(filepath.Join(path, "config.json"))
 | 
						|
	if err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
	var spec specs.Spec
 | 
						|
	if err := json.NewDecoder(f).Decode(&spec); err != nil {
 | 
						|
		return false, err
 | 
						|
	}
 | 
						|
	return spec.Process.Terminal, nil
 | 
						|
}
 | 
						|
 | 
						|
func attachTty(consolePath *string) error {
 | 
						|
	console, err := libcontainer.NewConsole(os.Getuid(), os.Getgid())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	*consolePath = console.Path()
 | 
						|
	stdin = console
 | 
						|
	go func() {
 | 
						|
		io.Copy(os.Stdout, console)
 | 
						|
		console.Close()
 | 
						|
	}()
 | 
						|
	s, err := term.SetRawTerminal(os.Stdin.Fd())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	state = s
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func attachStdio(stdins, stdout, stderr *string) error {
 | 
						|
	dir, err := ioutil.TempDir("", "ctr-")
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	for _, p := range []struct {
 | 
						|
		path string
 | 
						|
		flag int
 | 
						|
		done func(f *os.File)
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			path: filepath.Join(dir, "stdin"),
 | 
						|
			flag: syscall.O_RDWR,
 | 
						|
			done: func(f *os.File) {
 | 
						|
				*stdins = filepath.Join(dir, "stdin")
 | 
						|
				stdin = f
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: filepath.Join(dir, "stdout"),
 | 
						|
			flag: syscall.O_RDWR,
 | 
						|
			done: func(f *os.File) {
 | 
						|
				*stdout = filepath.Join(dir, "stdout")
 | 
						|
				go io.Copy(os.Stdout, f)
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			path: filepath.Join(dir, "stderr"),
 | 
						|
			flag: syscall.O_RDWR,
 | 
						|
			done: func(f *os.File) {
 | 
						|
				*stderr = filepath.Join(dir, "stderr")
 | 
						|
				go io.Copy(os.Stderr, f)
 | 
						|
			},
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		if err := syscall.Mkfifo(p.path, 0755); err != nil {
 | 
						|
			return fmt.Errorf("mkfifo: %s %v", p.path, err)
 | 
						|
		}
 | 
						|
		f, err := os.OpenFile(p.path, p.flag, 0)
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("open: %s %v", p.path, err)
 | 
						|
		}
 | 
						|
		p.done(f)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
var KillCommand = cli.Command{
 | 
						|
	Name:  "kill",
 | 
						|
	Usage: "send a signal to a container or it's processes",
 | 
						|
	Flags: []cli.Flag{
 | 
						|
		cli.IntFlag{
 | 
						|
			Name:  "pid,p",
 | 
						|
			Usage: "pid of the process to signal within the container",
 | 
						|
		},
 | 
						|
		cli.IntFlag{
 | 
						|
			Name:  "signal,s",
 | 
						|
			Value: 15,
 | 
						|
			Usage: "signal to send to the container",
 | 
						|
		},
 | 
						|
	},
 | 
						|
	Action: func(context *cli.Context) {
 | 
						|
		id := context.Args().First()
 | 
						|
		if id == "" {
 | 
						|
			fatal("container id cannot be empty", 1)
 | 
						|
		}
 | 
						|
		c := getClient(context)
 | 
						|
		if _, err := c.Signal(netcontext.Background(), &types.SignalRequest{
 | 
						|
			Id:     id,
 | 
						|
			Pid:    uint32(context.Int("pid")),
 | 
						|
			Signal: uint32(context.Int("signal")),
 | 
						|
		}); err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 | 
						|
 | 
						|
var ExecCommand = cli.Command{
 | 
						|
	Name:  "exec",
 | 
						|
	Usage: "exec another process in an existing container",
 | 
						|
	Flags: []cli.Flag{
 | 
						|
		cli.StringFlag{
 | 
						|
			Name:  "id",
 | 
						|
			Usage: "container id to add the process to",
 | 
						|
		},
 | 
						|
		cli.BoolFlag{
 | 
						|
			Name:  "attach,a",
 | 
						|
			Usage: "connect to the stdio of the container",
 | 
						|
		},
 | 
						|
		cli.StringFlag{
 | 
						|
			Name:  "cwd",
 | 
						|
			Usage: "current working directory for the process",
 | 
						|
		},
 | 
						|
		cli.BoolFlag{
 | 
						|
			Name:  "tty,t",
 | 
						|
			Usage: "create a terminal for the process",
 | 
						|
		},
 | 
						|
		cli.StringSliceFlag{
 | 
						|
			Name:  "env,e",
 | 
						|
			Value: &cli.StringSlice{},
 | 
						|
			Usage: "environment variables for the process",
 | 
						|
		},
 | 
						|
		cli.IntFlag{
 | 
						|
			Name:  "uid,u",
 | 
						|
			Usage: "user id of the user for the process",
 | 
						|
		},
 | 
						|
		cli.IntFlag{
 | 
						|
			Name:  "gid,g",
 | 
						|
			Usage: "group id of the user for the process",
 | 
						|
		},
 | 
						|
	},
 | 
						|
	Action: func(context *cli.Context) {
 | 
						|
		p := &types.AddProcessRequest{
 | 
						|
			Args:     context.Args(),
 | 
						|
			Cwd:      context.String("cwd"),
 | 
						|
			Terminal: context.Bool("tty"),
 | 
						|
			Id:       context.String("id"),
 | 
						|
			Env:      context.StringSlice("env"),
 | 
						|
			User: &types.User{
 | 
						|
				Uid: uint32(context.Int("uid")),
 | 
						|
				Gid: uint32(context.Int("gid")),
 | 
						|
			},
 | 
						|
		}
 | 
						|
		c := getClient(context)
 | 
						|
		events, err := c.Events(netcontext.Background(), &types.EventsRequest{})
 | 
						|
		if err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
		if context.Bool("attach") {
 | 
						|
			if p.Terminal {
 | 
						|
				if err := attachTty(&p.Console); err != nil {
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				if err := attachStdio(&p.Stdin, &p.Stdout, &p.Stderr); err != nil {
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		r, err := c.AddProcess(netcontext.Background(), p)
 | 
						|
		if err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
		if context.Bool("attach") {
 | 
						|
			go func() {
 | 
						|
				io.Copy(stdin, os.Stdin)
 | 
						|
				if state != nil {
 | 
						|
					term.RestoreTerminal(os.Stdin.Fd(), state)
 | 
						|
				}
 | 
						|
				stdin.Close()
 | 
						|
			}()
 | 
						|
			for {
 | 
						|
				e, err := events.Recv()
 | 
						|
				if err != nil {
 | 
						|
					fatal(err.Error(), 1)
 | 
						|
				}
 | 
						|
				if e.Pid == r.Pid && e.Type == "exit" {
 | 
						|
					os.Exit(int(e.Status))
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 | 
						|
 | 
						|
var StatsCommand = cli.Command{
 | 
						|
	Name:  "stats",
 | 
						|
	Usage: "get stats for running container",
 | 
						|
	Action: func(context *cli.Context) {
 | 
						|
		req := &types.StatsRequest{
 | 
						|
			Id: context.Args().First(),
 | 
						|
		}
 | 
						|
		c := getClient(context)
 | 
						|
		stream, err := c.GetStats(netcontext.Background(), req)
 | 
						|
		if err != nil {
 | 
						|
			fatal(err.Error(), 1)
 | 
						|
		}
 | 
						|
		for {
 | 
						|
			stats, err := stream.Recv()
 | 
						|
			if err != nil {
 | 
						|
				fatal(err.Error(), 1)
 | 
						|
			}
 | 
						|
			fmt.Println(stats)
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 |