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 }