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