diff --git a/client.go b/client.go index 7d3c4ccff..adb9834e5 100644 --- a/client.go +++ b/client.go @@ -17,6 +17,7 @@ import ( imagesapi "github.com/containerd/containerd/api/services/images" namespacesapi "github.com/containerd/containerd/api/services/namespaces" snapshotapi "github.com/containerd/containerd/api/services/snapshot" + versionservice "github.com/containerd/containerd/api/services/version" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/remotes" @@ -28,6 +29,7 @@ import ( imagesservice "github.com/containerd/containerd/services/images" snapshotservice "github.com/containerd/containerd/services/snapshot" "github.com/containerd/containerd/snapshot" + pempty "github.com/golang/protobuf/ptypes/empty" "github.com/opencontainers/image-spec/identity" "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -462,3 +464,23 @@ func (c *Client) DiffService() diff.DiffService { func (c *Client) HealthService() grpc_health_v1.HealthClient { return grpc_health_v1.NewHealthClient(c.conn) } + +func (c *Client) VersionService() versionservice.VersionClient { + return versionservice.NewVersionClient(c.conn) +} + +type Version struct { + Version string + Revision string +} + +func (c *Client) Version(ctx context.Context) (Version, error) { + response, err := c.VersionService().Version(ctx, &pempty.Empty{}) + if err != nil { + return Version{}, err + } + return Version{ + Version: response.Version, + Revision: response.Revision, + }, nil +} diff --git a/cmd/ctr/checkpoint.go b/cmd/ctr/checkpoint.go index 29590069a..fd75e07d3 100644 --- a/cmd/ctr/checkpoint.go +++ b/cmd/ctr/checkpoint.go @@ -1,22 +1,9 @@ package main import ( - "bytes" - gocontext "context" - "encoding/json" "fmt" - "io" - "runtime" - "github.com/Sirupsen/logrus" - "github.com/containerd/containerd/api/services/execution" - "github.com/containerd/containerd/api/types/descriptor" - "github.com/containerd/containerd/archive" - "github.com/containerd/containerd/content" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/rootfs" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/containerd/containerd" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -25,169 +12,42 @@ var checkpointCommand = cli.Command{ Name: "checkpoint", Usage: "checkpoint a container", Flags: []cli.Flag{ - cli.StringFlag{ - Name: "id", - Usage: "id of the container", - }, cli.BoolFlag{ Name: "exit", Usage: "stop the container after the checkpoint", }, - cli.BoolFlag{ - Name: "binds", - Usage: "checkpoint bind mounts with the checkpoint", - }, }, Action: func(context *cli.Context) error { var ( - id = context.String("id") ctx, cancel = appContext(context) + id = context.Args().First() ) defer cancel() if id == "" { return errors.New("container id must be provided") } - - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - content, err := getContentStore(context) + container, err := client.LoadContainer(ctx, id) if err != nil { return err } - imageStore, err := getImageStore(context) - if err != nil { - return errors.Wrap(err, "failed resolving image store") - } - var spec specs.Spec - info, err := tasks.Info(ctx, &execution.InfoRequest{ - ContainerID: id, - }) + task, err := container.Task(ctx, nil) if err != nil { return err } - if err := json.Unmarshal(info.Task.Spec.Value, &spec); err != nil { - return err + var opts []containerd.CheckpointOpts + if context.Bool("exit") { + opts = append(opts, containerd.WithExit) } - stopped := context.Bool("exit") - // if the container will still be running after the checkpoint make sure that - // we pause the container and give us time to checkpoint the filesystem before - // it resumes execution - if !stopped { - if _, err := tasks.Pause(ctx, &execution.PauseRequest{ - ContainerID: id, - }); err != nil { - return err - } - defer func() { - if _, err := tasks.Resume(ctx, &execution.ResumeRequest{ - ContainerID: id, - }); err != nil { - logrus.WithError(err).Error("ctr: unable to resume container") - } - }() - } - checkpoint, err := tasks.Checkpoint(ctx, &execution.CheckpointRequest{ - ContainerID: id, - Exit: context.Bool("exit"), - }) + checkpoint, err := task.Checkpoint(ctx, opts...) if err != nil { return err } - image, err := imageStore.Get(ctx, spec.Annotations["image"]) - if err != nil { - return err - } - var additionalDescriptors []*descriptor.Descriptor - if context.Bool("binds") { - if additionalDescriptors, err = checkpointBinds(ctx, &spec, content); err != nil { - return err - } - } - var index ocispec.Index - for _, d := range append(checkpoint.Descriptors, additionalDescriptors...) { - index.Manifests = append(index.Manifests, ocispec.Descriptor{ - MediaType: d.MediaType, - Size: d.Size_, - Digest: d.Digest, - Platform: &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - }, - }) - } - // add image to the index - index.Manifests = append(index.Manifests, image.Target) - // checkpoint rw layer - snapshotter, err := getSnapshotter(context) - if err != nil { - return err - } - differ, err := getDiffService(context) - if err != nil { - return err - } - rw, err := rootfs.Diff(ctx, id, fmt.Sprintf("checkpoint-rw-%s", id), snapshotter, differ) - if err != nil { - return err - } - rw.Platform = &ocispec.Platform{ - OS: runtime.GOOS, - Architecture: runtime.GOARCH, - } - index.Manifests = append(index.Manifests, rw) - data, err := json.Marshal(index) - if err != nil { - return err - } - // write the index to the content store - buf := bytes.NewReader(data) - desc, err := writeContent(ctx, content, ocispec.MediaTypeImageIndex, id, buf) - if err != nil { - return err - } - fmt.Println(desc.Digest.String()) + fmt.Println(checkpoint.Digest.String()) return nil }, } - -func checkpointBinds(ctx gocontext.Context, s *specs.Spec, store content.Store) ([]*descriptor.Descriptor, error) { - var out []*descriptor.Descriptor - for _, m := range s.Mounts { - if m.Type != "bind" { - continue - } - tar := archive.Diff(ctx, "", m.Source) - d, err := writeContent(ctx, store, images.MediaTypeContainerd1Resource, m.Source, tar) - if err := tar.Close(); err != nil { - return nil, err - } - if err != nil { - return nil, err - } - out = append(out, d) - } - return out, nil -} - -func writeContent(ctx gocontext.Context, store content.Store, mediaType, ref string, r io.Reader) (*descriptor.Descriptor, error) { - writer, err := store.Writer(ctx, ref, 0, "") - if err != nil { - return nil, err - } - defer writer.Close() - size, err := io.Copy(writer, r) - if err != nil { - return nil, err - } - if err := writer.Commit(0, ""); err != nil { - return nil, err - } - return &descriptor.Descriptor{ - MediaType: mediaType, - Digest: writer.Digest(), - Size_: size, - }, nil -} diff --git a/cmd/ctr/delete.go b/cmd/ctr/delete.go index 920695dfa..34421371c 100644 --- a/cmd/ctr/delete.go +++ b/cmd/ctr/delete.go @@ -1,14 +1,8 @@ package main import ( - "runtime" + "fmt" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - - containersapi "github.com/containerd/containerd/api/services/containers" - "github.com/containerd/containerd/api/services/execution" - "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -19,49 +13,17 @@ var deleteCommand = cli.Command{ Action: func(context *cli.Context) error { ctx, cancel := appContext(context) defer cancel() - - containers, err := getContainersService(context) + client, err := newClient(context) if err != nil { return err } - tasks, err := getTasksService(context) + container, err := client.LoadContainer(ctx, context.Args().First()) if err != nil { return err } - - snapshotter, err := getSnapshotter(context) - if err != nil { - return err + if _, err := container.Task(ctx, nil); err == nil { + return fmt.Errorf("cannot delete a container with a running task") } - id := context.Args().First() - if id == "" { - return errors.New("container id must be provided") - } - - _, err = containers.Delete(ctx, &containersapi.DeleteContainerRequest{ - ID: id, - }) - if err != nil { - return errors.Wrap(err, "failed to delete container") - } - - _, err = tasks.Delete(ctx, &execution.DeleteRequest{ - ContainerID: id, - }) - if err != nil { - // Ignore error if task has already been removed, task is - // removed by default after run - if grpc.Code(errors.Cause(err)) != codes.NotFound { - return errors.Wrap(err, "failed to task container") - } - } - - if runtime.GOOS != "windows" { - if err := snapshotter.Remove(ctx, id); err != nil { - return errors.Wrapf(err, "failed to remove snapshot %q", id) - } - } - - return nil + return container.Delete(ctx) }, } diff --git a/cmd/ctr/events.go b/cmd/ctr/events.go deleted file mode 100644 index 001db8675..000000000 --- a/cmd/ctr/events.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "fmt" - "os" - "text/tabwriter" - - "github.com/containerd/containerd/api/services/execution" - "github.com/urfave/cli" -) - -var eventsCommand = cli.Command{ - Name: "events", - Usage: "display containerd events", - Action: func(context *cli.Context) error { - ctx, cancel := appContext(context) - defer cancel() - - tasks, err := getTasksService(context) - if err != nil { - return err - } - events, err := tasks.Events(ctx, &execution.EventsRequest{}) - if err != nil { - return err - } - w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0) - fmt.Fprintln(w, "TYPE\tID\tPID\tEXIT_STATUS") - for { - e, err := events.Recv() - if err != nil { - return err - } - if _, err := fmt.Fprintf(w, - "%s\t%s\t%d\t%d\n", - e.Type.String(), - e.ID, - e.Pid, - e.ExitStatus, - ); err != nil { - return err - } - if err := w.Flush(); err != nil { - return err - } - } - }, -} diff --git a/cmd/ctr/exec.go b/cmd/ctr/exec.go index 2df95db4c..552c5f2fb 100644 --- a/cmd/ctr/exec.go +++ b/cmd/ctr/exec.go @@ -1,12 +1,11 @@ package main import ( - "os" + "errors" "github.com/Sirupsen/logrus" "github.com/containerd/console" - "github.com/containerd/containerd/api/services/execution" - "github.com/pkg/errors" + "github.com/containerd/containerd" "github.com/urfave/cli" ) @@ -14,10 +13,6 @@ var execCommand = cli.Command{ Name: "exec", Usage: "execute additional processes in an existing container", Flags: []cli.Flag{ - cli.StringFlag{ - Name: "id", - Usage: "id of the container", - }, cli.StringFlag{ Name: "cwd", Usage: "working directory of the new process", @@ -29,61 +24,75 @@ var execCommand = cli.Command{ }, Action: func(context *cli.Context) error { var ( - id = context.String("id") ctx, cancel = appContext(context) + id = context.Args().First() + args = context.Args().Tail() + tty = context.Bool("tty") ) defer cancel() if id == "" { return errors.New("container id must be provided") } + client, err := newClient(context) + if err != nil { + return err + } + container, err := client.LoadContainer(ctx, id) + if err != nil { + return err + } + spec, err := container.Spec() + if err != nil { + return err + } + task, err := container.Task(ctx, nil) + if err != nil { + return err + } - tasks, err := getTasksService(context) - if err != nil { - return err - } - events, err := tasks.Events(ctx, &execution.EventsRequest{}) - if err != nil { - return err - } - tmpDir, err := getTempDir(id) - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) - request, err := newExecRequest(context, tmpDir, id) + pspec := &spec.Process + pspec.Terminal = tty + pspec.Args = args + + io := containerd.Stdio + if tty { + io = containerd.StdioTerminal + } + process, err := task.Exec(ctx, pspec, io) if err != nil { return err } + defer process.Delete() + + statusC := make(chan uint32, 1) + go func() { + status, err := process.Wait(ctx) + if err != nil { + logrus.WithError(err).Error("wait process") + } + statusC <- status + }() var con console.Console - if request.Terminal { + if tty { con = console.Current() defer con.Reset() if err := con.SetRaw(); err != nil { return err } } - fwg, err := prepareStdio(request.Stdin, request.Stdout, request.Stderr, request.Terminal) - if err != nil { + if err := process.Start(ctx); err != nil { return err } - response, err := tasks.Exec(ctx, request) - if err != nil { - return err - } - if request.Terminal { - if err := handleConsoleResize(ctx, tasks, id, response.Pid, con); err != nil { + if tty { + if err := handleConsoleResize(ctx, process, con); err != nil { logrus.WithError(err).Error("console resize") } + } else { + sigc := forwardAllSignals(ctx, process) + defer stopCatch(sigc) } - - // Ensure we read all io only if container started successfully. - defer fwg.Wait() - - status, err := waitContainer(events, id, response.Pid) - if err != nil { - return err - } + status := <-statusC if status != 0 { return cli.NewExitError("", int(status)) } diff --git a/cmd/ctr/exec_unix.go b/cmd/ctr/exec_unix.go deleted file mode 100644 index 78736508f..000000000 --- a/cmd/ctr/exec_unix.go +++ /dev/null @@ -1,69 +0,0 @@ -// +build !windows - -package main - -import ( - "encoding/json" - "path/filepath" - - "github.com/containerd/containerd/api/services/execution" - protobuf "github.com/gogo/protobuf/types" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/urfave/cli" -) - -func createProcessSpec(args []string, cwd string, tty bool) specs.Process { - env := []string{ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - } - if tty { - env = append(env, "TERM=xterm") - } - if cwd == "" { - cwd = "/" - } - return specs.Process{ - Args: args, - Env: env, - Terminal: tty, - Cwd: cwd, - NoNewPrivileges: true, - User: specs.User{ - UID: 0, - GID: 0, - }, - Capabilities: &specs.LinuxCapabilities{ - Bounding: capabilities, - Permitted: capabilities, - Inheritable: capabilities, - Effective: capabilities, - Ambient: capabilities, - }, - Rlimits: []specs.LinuxRlimit{ - { - Type: "RLIMIT_NOFILE", - Hard: uint64(1024), - Soft: uint64(1024), - }, - }, - } -} - -func newExecRequest(context *cli.Context, tmpDir, id string) (*execution.ExecRequest, error) { - process := createProcessSpec(context.Args(), context.String("cwd"), context.Bool("tty")) - data, err := json.Marshal(process) - if err != nil { - return nil, err - } - return &execution.ExecRequest{ - ContainerID: id, - Spec: &protobuf.Any{ - TypeUrl: specs.Version, - Value: data, - }, - Terminal: context.Bool("tty"), - Stdin: filepath.Join(tmpDir, "stdin"), - Stdout: filepath.Join(tmpDir, "stdout"), - Stderr: filepath.Join(tmpDir, "stderr"), - }, nil -} diff --git a/cmd/ctr/exec_windows.go b/cmd/ctr/exec_windows.go deleted file mode 100644 index 5e1ee6473..000000000 --- a/cmd/ctr/exec_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/containerd/containerd/api/services/execution" - protobuf "github.com/gogo/protobuf/types" - specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/urfave/cli" -) - -func newExecRequest(context *cli.Context, tmpDir, id string) (*execution.ExecRequest, error) { - process := specs.Process{ - Args: context.Args(), - Terminal: context.Bool("tty"), - Cwd: context.String("cwd"), - } - data, err := json.Marshal(process) - if err != nil { - return nil, err - } - now := time.Now().UnixNano() - request := &execution.ExecRequest{ - ContainerID: id, - Spec: &protobuf.Any{ - TypeUrl: specs.Version, - Value: data, - }, - Terminal: context.Bool("tty"), - Stdin: fmt.Sprintf(`%s\ctr-%s-stdin-%d`, pipeRoot, id, now), - Stdout: fmt.Sprintf(`%s\ctr-%s-stdout-%d`, pipeRoot, id, now), - } - if !request.Terminal { - request.Stderr = fmt.Sprintf(`%s\ctr-%s-stderr-%d`, pipeRoot, id, now) - } - - return request, nil -} diff --git a/cmd/ctr/info.go b/cmd/ctr/info.go index 3dec3bccb..45b9a5aab 100644 --- a/cmd/ctr/info.go +++ b/cmd/ctr/info.go @@ -3,11 +3,8 @@ package main import ( "encoding/json" - gocontext "context" "fmt" - containersapi "github.com/containerd/containerd/api/services/containers" - "github.com/containerd/containerd/api/services/execution" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -15,55 +12,29 @@ import ( var infoCommand = cli.Command{ Name: "info", Usage: "get info about a container", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "id", - Usage: "id of the container", - }, - }, Action: func(context *cli.Context) error { var ( - id = context.String("id") ctx, cancel = appContext(context) + id = context.Args().First() ) defer cancel() - if id == "" { return errors.New("container id must be provided") } - - containers, err := getContainersService(context) - if err != nil { - return err - } - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - containerResponse, err := containers.Get(ctx, &containersapi.GetContainerRequest{ID: id}) + container, err := client.LoadContainer(ctx, id) if err != nil { return err } - - // TODO(stevvooe): Just dumping the container and the task, for now. We - // should split this into two separate commands. - cjson, err := json.MarshalIndent(containerResponse, "", " ") + cjson, err := json.MarshalIndent(container, "", " ") if err != nil { return err } - fmt.Println(string(cjson)) - - response, err := tasks.Info(gocontext.Background(), &execution.InfoRequest{ContainerID: id}) - if err != nil { - return err - } - json, err := json.MarshalIndent(response, "", " ") - if err != nil { - return err - } - fmt.Println(string(json)) return nil }, } diff --git a/cmd/ctr/kill.go b/cmd/ctr/kill.go index b2b9fb2a1..f743e2937 100644 --- a/cmd/ctr/kill.go +++ b/cmd/ctr/kill.go @@ -1,7 +1,6 @@ package main import ( - "github.com/containerd/containerd/api/services/execution" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -11,8 +10,9 @@ var killCommand = cli.Command{ Usage: "signal a container (default: SIGTERM)", Flags: []cli.Flag{ cli.StringFlag{ - Name: "id", - Usage: "id of the container", + Name: "signal, s", + Value: "SIGTERM", + Usage: "signal to send to the container", }, cli.IntFlag{ Name: "pid", @@ -26,53 +26,36 @@ var killCommand = cli.Command{ }, Action: func(context *cli.Context) error { var ( - id = context.String("id") + id = context.Args().First() ctx, cancel = appContext(context) ) defer cancel() - if id == "" { return errors.New("container id must be provided") } - - sigstr := context.Args().First() - if sigstr == "" { - sigstr = "SIGTERM" - } - - signal, err := parseSignal(sigstr) + signal, err := parseSignal(context.String("signal")) if err != nil { return err } - - pid := context.Int("pid") - all := context.Bool("all") + var ( + pid = context.Int("pid") + all = context.Bool("all") + ) if pid > 0 && all { return errors.New("enter a pid or all; not both") } - - killRequest := &execution.KillRequest{ - ContainerID: id, - Signal: uint32(signal), - PidOrAll: &execution.KillRequest_Pid{ - Pid: uint32(pid), - }, - } - - if all { - killRequest.PidOrAll = &execution.KillRequest_All{ - All: true, - } - } - - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - _, err = tasks.Kill(ctx, killRequest) + container, err := client.LoadContainer(ctx, id) if err != nil { return err } - return nil + task, err := container.Task(ctx, nil) + if err != nil { + return err + } + return task.Kill(ctx, signal) }, } diff --git a/cmd/ctr/list.go b/cmd/ctr/list.go index b8a5ab756..9599f5b91 100644 --- a/cmd/ctr/list.go +++ b/cmd/ctr/list.go @@ -1,14 +1,11 @@ package main import ( - gocontext "context" "fmt" "os" "text/tabwriter" - containersapi "github.com/containerd/containerd/api/services/containers" - "github.com/containerd/containerd/api/services/execution" - tasktypes "github.com/containerd/containerd/api/types/task" + "github.com/containerd/containerd" "github.com/urfave/cli" ) @@ -33,69 +30,59 @@ var listCommand = cli.Command{ ) defer cancel() - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - - containers, err := getContainersService(context) + containers, err := client.Containers(ctx) if err != nil { return err } - - response, err := containers.List(gocontext.Background(), &containersapi.ListContainersRequest{}) - if err != nil { - return err - } - if quiet { - for _, c := range response.Containers { - fmt.Println(c.ID) + for _, c := range containers { + fmt.Println(c.ID()) } - } else { + return nil + } - tasksResponse, err := tasks.List(ctx, &execution.ListRequest{}) + w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0) + fmt.Fprintln(w, "ID\tIMAGE\tPID\tSTATUS") + for _, c := range containers { + image, err := c.Image(ctx) if err != nil { return err } - - // Join with tasks to get status. - tasksByContainerID := map[string]*tasktypes.Task{} - for _, task := range tasksResponse.Tasks { - task.Descriptor() - tasksByContainerID[task.ContainerID] = task + var ( + status string + pid uint32 + ) + task, err := c.Task(ctx, nil) + if err == nil { + s, err := task.Status(ctx) + if err != nil { + return err + } + status = string(s) + pid = task.Pid() + } else { + if err != containerd.ErrNoRunningTask { + return err + } + status = string(containerd.Stopped) + pid = 0 } - - w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0) - fmt.Fprintln(w, "ID\tIMAGE\tPID\tSTATUS") - for _, c := range response.Containers { - var ( - status string - pid uint32 - ) - task, ok := tasksByContainerID[c.ID] - if ok { - status = task.Status.String() - pid = task.Pid - } else { - status = "STOPPED" // TODO(stevvooe): Is this assumption correct? - pid = 0 - } - - if _, err := fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", - c.ID, - c.Image, - pid, - status, - ); err != nil { - return err - } - if err := w.Flush(); err != nil { - return err - } + if _, err := fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", + c.ID(), + image.Name(), + pid, + status, + ); err != nil { + return err + } + if err := w.Flush(); err != nil { + return err } } - return nil }, } diff --git a/cmd/ctr/main.go b/cmd/ctr/main.go index acd527fd0..44ed6ebda 100644 --- a/cmd/ctr/main.go +++ b/cmd/ctr/main.go @@ -9,9 +9,7 @@ import ( "github.com/urfave/cli" ) -var ( - extraCmds = []cli.Command{} -) +var extraCmds = []cli.Command{} func init() { cli.VersionPrinter = func(c *cli.Context) { @@ -30,7 +28,7 @@ func main() { / /__/ /_/ / \___/\__/_/ -containerd client +containerd CLI ` app.Flags = []cli.Flag{ cli.BoolFlag{ @@ -53,10 +51,9 @@ containerd client EnvVar: "CONTAINERD_NAMESPACE", }, } - app.Commands = []cli.Command{ + app.Commands = append([]cli.Command{ checkpointCommand, runCommand, - eventsCommand, deleteCommand, namespacesCommand, listCommand, @@ -69,8 +66,7 @@ containerd client snapshotCommand, versionCommand, psCommand, - } - app.Commands = append(app.Commands, extraCmds...) + }, extraCmds...) app.Before = func(context *cli.Context) error { if context.GlobalBool("debug") { logrus.SetLevel(logrus.DebugLevel) diff --git a/cmd/ctr/pause.go b/cmd/ctr/pause.go index e68e5bdad..5b2647ae1 100644 --- a/cmd/ctr/pause.go +++ b/cmd/ctr/pause.go @@ -1,11 +1,6 @@ package main -import ( - "errors" - - "github.com/containerd/containerd/api/services/execution" - "github.com/urfave/cli" -) +import "github.com/urfave/cli" var pauseCommand = cli.Command{ Name: "pause", @@ -15,17 +10,18 @@ var pauseCommand = cli.Command{ ctx, cancel := appContext(context) defer cancel() - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - id := context.Args().First() - if id == "" { - return errors.New("container id must be provided") + container, err := client.LoadContainer(ctx, context.Args().First()) + if err != nil { + return err } - _, err = tasks.Pause(ctx, &execution.PauseRequest{ - ContainerID: id, - }) - return err + task, err := container.Task(ctx, nil) + if err != nil { + return err + } + return task.Pause(ctx) }, } diff --git a/cmd/ctr/ps.go b/cmd/ctr/ps.go index b6ff9f88c..d19babb63 100644 --- a/cmd/ctr/ps.go +++ b/cmd/ctr/ps.go @@ -5,7 +5,6 @@ import ( "os" "text/tabwriter" - "github.com/containerd/containerd/api/services/execution" "github.com/pkg/errors" "github.com/urfave/cli" ) @@ -13,15 +12,9 @@ import ( var psCommand = cli.Command{ Name: "ps", Usage: "list processes for container", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "id", - Usage: "id of the container", - }, - }, Action: func(context *cli.Context) error { var ( - id = context.String("id") + id = context.Args().First() ctx, cancel = appContext(context) ) defer cancel() @@ -29,34 +22,33 @@ var psCommand = cli.Command{ if id == "" { return errors.New("container id must be provided") } - - pr := &execution.ProcessesRequest{ - ContainerID: id, + client, err := newClient(context) + if err != nil { + return err } - - tasks, err := getTasksService(context) + container, err := client.LoadContainer(ctx, id) if err != nil { return err } - resp, err := tasks.Processes(ctx, pr) + task, err := container.Task(ctx, nil) + if err != nil { + return err + } + processes, err := task.Processes(ctx) if err != nil { return err } - w := tabwriter.NewWriter(os.Stdout, 10, 1, 3, ' ', 0) fmt.Fprintln(w, "PID") - for _, ps := range resp.Processes { - if _, err := fmt.Fprintf(w, "%d\n", - ps.Pid, - ); err != nil { + for _, ps := range processes { + if _, err := fmt.Fprintf(w, "%d\n", ps); err != nil { return err } } if err := w.Flush(); err != nil { return err } - return nil }, } diff --git a/cmd/ctr/resume.go b/cmd/ctr/resume.go index f0cc6210a..aeb5dbf55 100644 --- a/cmd/ctr/resume.go +++ b/cmd/ctr/resume.go @@ -1,11 +1,6 @@ package main -import ( - "errors" - - "github.com/containerd/containerd/api/services/execution" - "github.com/urfave/cli" -) +import "github.com/urfave/cli" var resumeCommand = cli.Command{ Name: "resume", @@ -15,17 +10,18 @@ var resumeCommand = cli.Command{ ctx, cancel := appContext(context) defer cancel() - tasks, err := getTasksService(context) + client, err := newClient(context) if err != nil { return err } - id := context.Args().First() - if id == "" { - return errors.New("container id must be provided") + container, err := client.LoadContainer(ctx, context.Args().First()) + if err != nil { + return err } - _, err = tasks.Resume(ctx, &execution.ResumeRequest{ - ContainerID: id, - }) - return err + task, err := container.Task(ctx, nil) + if err != nil { + return err + } + return task.Resume(ctx) }, } diff --git a/cmd/ctr/run.go b/cmd/ctr/run.go index e655ee760..cac694964 100644 --- a/cmd/ctr/run.go +++ b/cmd/ctr/run.go @@ -1,52 +1,63 @@ package main import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "runtime" + gocontext "context" + "syscall" "github.com/Sirupsen/logrus" "github.com/containerd/console" - containersapi "github.com/containerd/containerd/api/services/containers" - "github.com/containerd/containerd/api/services/execution" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/snapshot" + "github.com/containerd/containerd" digest "github.com/opencontainers/go-digest" - "github.com/opencontainers/image-spec/identity" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/urfave/cli" ) +type resizer interface { + Resize(ctx gocontext.Context, w, h uint32) error +} + +type killer interface { + Kill(gocontext.Context, syscall.Signal) error +} + +func withEnv(context *cli.Context) containerd.SpecOpts { + return func(s *specs.Spec) error { + env := context.StringSlice("env") + if len(env) > 0 { + s.Process.Env = append(s.Process.Env, env...) + } + return nil + } +} + +func withMounts(context *cli.Context) containerd.SpecOpts { + return func(s *specs.Spec) error { + for _, mount := range context.StringSlice("mount") { + m, err := parseMountFlag(mount) + if err != nil { + return err + } + s.Mounts = append(s.Mounts, m) + } + return nil + } +} + var runCommand = cli.Command{ Name: "run", Usage: "run a container", - ArgsUsage: "IMAGE [COMMAND] [ARG...]", + ArgsUsage: "IMAGE ID [COMMAND] [ARG...]", Flags: []cli.Flag{ - cli.StringFlag{ - Name: "id", - Usage: "id of the container", - }, cli.BoolFlag{ Name: "tty,t", Usage: "allocate a TTY for the container", }, - cli.StringFlag{ - Name: "rootfs", - Usage: "path to rootfs", - }, cli.StringFlag{ Name: "runtime", Usage: "runtime name (linux, windows, vmware-linux)", Value: "linux", }, - cli.StringFlag{ - Name: "runtime-config", - Usage: "set the OCI config file for the container", - }, cli.BoolFlag{ Name: "readonly", Usage: "set the containers filesystem as readonly", @@ -74,237 +85,72 @@ var runCommand = cli.Command{ }, Action: func(context *cli.Context) error { var ( - err error - mounts []mount.Mount - imageConfig ocispec.Image + err error + checkpointIndex digest.Digest ctx, cancel = appContext(context) - id = context.String("id") + id = context.Args().Get(1) + tty = context.Bool("tty") ) defer cancel() if id == "" { return errors.New("container id must be provided") } - containers, err := getContainersService(context) - if err != nil { - return err - } - tasks, err := getTasksService(context) - if err != nil { - return err - } - tmpDir, err := getTempDir(id) - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) - events, err := tasks.Events(ctx, &execution.EventsRequest{}) - if err != nil { - return err - } - - content, err := getContentStore(context) - if err != nil { - return err - } - - snapshotter, err := getSnapshotter(context) - if err != nil { - return err - } - imageStore, err := getImageStore(context) - if err != nil { - return errors.Wrap(err, "failed resolving image store") - } - differ, err := getDiffService(context) - if err != nil { - return err - } - var ( - checkpoint *ocispec.Descriptor - checkpointIndex digest.Digest - ref = context.Args().First() - ) if raw := context.String("checkpoint"); raw != "" { if checkpointIndex, err = digest.Parse(raw); err != nil { return err } } - var spec []byte - if checkpointIndex != "" { - var index ocispec.Index - r, err := content.Reader(ctx, checkpointIndex) + client, err := newClient(context) + if err != nil { + return err + } + container, err := newContainer(ctx, client, context) + if err != nil { + return err + } + if context.Bool("rm") { + defer container.Delete(ctx) + } + task, err := newTask(ctx, container, checkpointIndex, tty) + if err != nil { + return err + } + defer task.Delete(ctx) + + statusC := make(chan uint32, 1) + go func() { + status, err := task.Wait(ctx) if err != nil { - return err + logrus.WithError(err).Error("wait process") } - err = json.NewDecoder(r).Decode(&index) - r.Close() - if err != nil { - return err - } - var rw ocispec.Descriptor - for _, m := range index.Manifests { - switch m.MediaType { - case images.MediaTypeContainerd1Checkpoint: - fkingo := m - checkpoint = &fkingo - case images.MediaTypeContainerd1CheckpointConfig: - if r, err = content.Reader(ctx, m.Digest); err != nil { - return err - } - spec, err = ioutil.ReadAll(r) - r.Close() - if err != nil { - return err - } - case images.MediaTypeDockerSchema2Manifest: - // make sure we have the original image that was used during checkpoint - diffIDs, err := images.RootFS(ctx, content, m) - if err != nil { - return err - } - if _, err := snapshotter.Prepare(ctx, id, identity.ChainID(diffIDs).String()); err != nil { - if !snapshot.IsExist(err) { - return err - } - } - case ocispec.MediaTypeImageLayer: - rw = m - } - } - if mounts, err = snapshotter.Mounts(ctx, id); err != nil { - return err - } - if _, err := differ.Apply(ctx, rw, mounts); err != nil { - return err - } - } else { - if runtime.GOOS != "windows" && context.String("rootfs") == "" { - image, err := imageStore.Get(ctx, ref) - if err != nil { - return errors.Wrapf(err, "could not resolve %q", ref) - } - // let's close out our db and tx so we don't hold the lock whilst running. - diffIDs, err := image.RootFS(ctx, content) - if err != nil { - return err - } - if context.Bool("readonly") { - mounts, err = snapshotter.View(ctx, id, identity.ChainID(diffIDs).String()) - } else { - mounts, err = snapshotter.Prepare(ctx, id, identity.ChainID(diffIDs).String()) - } - defer func() { - if err != nil || context.Bool("rm") { - if err := snapshotter.Remove(ctx, id); err != nil { - logrus.WithError(err).Errorf("failed to remove snapshot %q", id) - } - } - }() - if err != nil { - if !snapshot.IsExist(err) { - return err - } - mounts, err = snapshotter.Mounts(ctx, id) - if err != nil { - return err - } - } - ic, err := image.Config(ctx, content) - if err != nil { - return err - } - switch ic.MediaType { - case ocispec.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config: - r, err := content.Reader(ctx, ic.Digest) - if err != nil { - return err - } - if err := json.NewDecoder(r).Decode(&imageConfig); err != nil { - r.Close() - return err - } - r.Close() - default: - return fmt.Errorf("unknown image config media type %s", ic.MediaType) - } - } else { - // TODO: get the image / rootfs through the API once windows has a snapshotter - } - } - if err != nil { - return err - } - if len(spec) == 0 { - if spec, err = newContainerSpec(context, &imageConfig.Config, ref); err != nil { - return err - } - } - - createContainer, err := newCreateContainerRequest(context, id, id, ref, spec) - if err != nil { - return err - } - - _, err = containers.Create(ctx, createContainer) - if err != nil { - return err - } - - create, err := newCreateTaskRequest(context, id, tmpDir, checkpoint, mounts) - if err != nil { - return err - } + statusC <- status + }() var con console.Console - if create.Terminal { + if tty { con = console.Current() defer con.Reset() if err := con.SetRaw(); err != nil { return err } } - fwg, err := prepareStdio(create.Stdin, create.Stdout, create.Stderr, create.Terminal) - if err != nil { + if err := task.Start(ctx); err != nil { return err } - response, err := tasks.Create(ctx, create) - if err != nil { - return err - } - pid := response.Pid - if create.Terminal { - if err := handleConsoleResize(ctx, tasks, id, pid, con); err != nil { + if tty { + if err := handleConsoleResize(ctx, task, con); err != nil { logrus.WithError(err).Error("console resize") } } else { - sigc := forwardAllSignals(tasks, id) + sigc := forwardAllSignals(ctx, task) defer stopCatch(sigc) } - if checkpoint == nil { - if _, err := tasks.Start(ctx, &execution.StartRequest{ - ContainerID: id, - }); err != nil { - return err - } - } - // Ensure we read all io only if container started successfully. - defer fwg.Wait() - status, err := waitContainer(events, id, pid) - if err != nil { + status := <-statusC + if _, err := task.Delete(ctx); err != nil { return err } - if _, err := tasks.Delete(ctx, &execution.DeleteRequest{ - ContainerID: response.ContainerID, - }); err != nil { - return err - } - if context.Bool("rm") { - if _, err := containers.Delete(ctx, &containersapi.DeleteContainerRequest{ID: id}); err != nil { - return err - } - } if status != 0 { return cli.NewExitError("", int(status)) } diff --git a/cmd/ctr/run_unix.go b/cmd/ctr/run_unix.go index 448770c35..df6d040cd 100644 --- a/cmd/ctr/run_unix.go +++ b/cmd/ctr/run_unix.go @@ -3,343 +3,29 @@ package main import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" + gocontext "context" "os" "os/signal" - "path/filepath" - "runtime" - "strconv" - "strings" "golang.org/x/sys/unix" "github.com/Sirupsen/logrus" "github.com/containerd/console" - containersapi "github.com/containerd/containerd/api/services/containers" - "github.com/containerd/containerd/api/services/execution" - "github.com/containerd/containerd/api/types/descriptor" - "github.com/containerd/containerd/api/types/mount" - mountt "github.com/containerd/containerd/mount" - protobuf "github.com/gogo/protobuf/types" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/containerd/containerd" + digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/urfave/cli" ) -const ( - rwm = "rwm" - defaultRootfsPath = "rootfs" -) - -var capabilities = []string{ - "CAP_CHOWN", - "CAP_DAC_OVERRIDE", - "CAP_FSETID", - "CAP_FOWNER", - "CAP_MKNOD", - "CAP_NET_RAW", - "CAP_SETGID", - "CAP_SETUID", - "CAP_SETFCAP", - "CAP_SETPCAP", - "CAP_NET_BIND_SERVICE", - "CAP_SYS_CHROOT", - "CAP_KILL", - "CAP_AUDIT_WRITE", -} - -func spec(id string, config *ocispec.ImageConfig, context *cli.Context, rootfs string) (*specs.Spec, error) { - defaultEnv := []string{ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - } - defaultEnv = append(defaultEnv, config.Env...) - cmd := config.Cmd - if v := context.Args().Tail(); len(v) > 0 { - cmd = v - } - var ( - // TODO: support overriding entrypoint - args = append(config.Entrypoint, cmd...) - tty = context.Bool("tty") - uid, gid uint32 - ) - if config.User != "" { - parts := strings.Split(config.User, ":") - switch len(parts) { - case 1: - v, err := strconv.ParseUint(parts[0], 0, 10) - if err != nil { - return nil, err - } - uid, gid = uint32(v), uint32(v) - case 2: - v, err := strconv.ParseUint(parts[0], 0, 10) - if err != nil { - return nil, err - } - uid = uint32(v) - if v, err = strconv.ParseUint(parts[1], 0, 10); err != nil { - return nil, err - } - gid = uint32(v) - default: - return nil, fmt.Errorf("invalid USER value %s", config.User) - } - } - if tty { - defaultEnv = append(defaultEnv, "TERM=xterm") - } - - // additional environment vars - env := replaceOrAppendEnvValues(defaultEnv, context.StringSlice("env")) - - cwd := config.WorkingDir - if cwd == "" { - cwd = "/" - } - if rootfs == "" { - rootfs = defaultRootfsPath - } - s := &specs.Spec{ - Version: specs.Version, - Platform: specs.Platform{ - OS: runtime.GOOS, - Arch: runtime.GOARCH, - }, - Root: specs.Root{ - Path: rootfs, - Readonly: context.Bool("readonly"), - }, - Process: specs.Process{ - Args: args, - Env: env, - Terminal: tty, - Cwd: cwd, - NoNewPrivileges: true, - User: specs.User{ - UID: uid, - GID: gid, - }, - Capabilities: &specs.LinuxCapabilities{ - Bounding: capabilities, - Permitted: capabilities, - Inheritable: capabilities, - Effective: capabilities, - Ambient: capabilities, - }, - Rlimits: []specs.LinuxRlimit{ - { - Type: "RLIMIT_NOFILE", - Hard: uint64(1024), - Soft: uint64(1024), - }, - }, - }, - Mounts: []specs.Mount{ - { - Destination: "/proc", - Type: "proc", - Source: "proc", - }, - { - Destination: "/dev", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, - }, - { - Destination: "/dev/pts", - Type: "devpts", - Source: "devpts", - Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, - }, - { - Destination: "/dev/shm", - Type: "tmpfs", - Source: "shm", - Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"}, - }, - { - Destination: "/dev/mqueue", - Type: "mqueue", - Source: "mqueue", - Options: []string{"nosuid", "noexec", "nodev"}, - }, - { - Destination: "/sys", - Type: "sysfs", - Source: "sysfs", - Options: []string{"nosuid", "noexec", "nodev", "ro"}, - }, - { - Destination: "/run", - Type: "tmpfs", - Source: "tmpfs", - Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, - }, - { - Destination: "/etc/resolv.conf", - Type: "bind", - Source: "/etc/resolv.conf", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/hosts", - Type: "bind", - Source: "/etc/hosts", - Options: []string{"rbind", "ro"}, - }, - { - Destination: "/etc/localtime", - Type: "bind", - Source: "/etc/localtime", - Options: []string{"rbind", "ro"}, - }, - }, - Hostname: id, - Linux: &specs.Linux{ - Resources: &specs.LinuxResources{ - Devices: []specs.LinuxDeviceCgroup{ - { - Allow: false, - Access: rwm, - }, - }, - }, - Namespaces: []specs.LinuxNamespace{ - { - Type: "pid", - }, - { - Type: "ipc", - }, - { - Type: "uts", - }, - { - Type: "mount", - }, - }, - }, - } - if !context.Bool("net-host") { - s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{ - Type: "network", - }) - } - for _, mount := range context.StringSlice("mount") { - m, err := parseMountFlag(mount) - if err != nil { - return nil, err - } - - s.Mounts = append(s.Mounts, m) - } - return s, nil -} - -func customSpec(configPath string, rootfs string) (*specs.Spec, error) { - b, err := ioutil.ReadFile(configPath) - if err != nil { - return nil, err - } - var s specs.Spec - if err := json.Unmarshal(b, &s); err != nil { - return nil, err - } - if rootfs == "" { - if s.Root.Path != defaultRootfsPath { - logrus.Warnf("ignoring Root.Path %q, setting %q forcibly", s.Root.Path, defaultRootfsPath) - s.Root.Path = defaultRootfsPath - } - } else { - s.Root.Path = rootfs - } - return &s, nil -} - -func getConfig(context *cli.Context, imageConfig *ocispec.ImageConfig, rootfs string) (*specs.Spec, error) { - config := context.String("runtime-config") - if config == "" { - return spec(context.String("id"), imageConfig, context, rootfs) - } - - return customSpec(config, rootfs) -} - -func newContainerSpec(context *cli.Context, config *ocispec.ImageConfig, imageRef string) ([]byte, error) { - s, err := getConfig(context, config, context.String("rootfs")) - if err != nil { - return nil, err - } - if s.Annotations == nil { - s.Annotations = make(map[string]string) - } - s.Annotations["image"] = imageRef - return json.Marshal(s) -} - -func newCreateContainerRequest(context *cli.Context, id, snapshot, image string, spec []byte) (*containersapi.CreateContainerRequest, error) { - create := &containersapi.CreateContainerRequest{ - Container: containersapi.Container{ - ID: id, - Image: image, - Spec: &protobuf.Any{ - TypeUrl: specs.Version, - Value: spec, - }, - Runtime: context.String("runtime"), - RootFS: snapshot, - }, - } - - return create, nil -} - -func newCreateTaskRequest(context *cli.Context, id, tmpDir string, checkpoint *ocispec.Descriptor, mounts []mountt.Mount) (*execution.CreateRequest, error) { - create := &execution.CreateRequest{ - ContainerID: id, - Terminal: context.Bool("tty"), - Stdin: filepath.Join(tmpDir, "stdin"), - Stdout: filepath.Join(tmpDir, "stdout"), - Stderr: filepath.Join(tmpDir, "stderr"), - } - - for _, m := range mounts { - create.Rootfs = append(create.Rootfs, &mount.Mount{ - Type: m.Type, - Source: m.Source, - Options: m.Options, - }) - } - - if checkpoint != nil { - create.Checkpoint = &descriptor.Descriptor{ - MediaType: checkpoint.MediaType, - Size_: checkpoint.Size, - Digest: checkpoint.Digest, - } - } - - return create, nil -} - -func handleConsoleResize(ctx context.Context, service execution.TasksClient, id string, pid uint32, con console.Console) error { +func handleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error { // do an initial resize of the console size, err := con.Size() if err != nil { return err } - if _, err := service.Pty(ctx, &execution.PtyRequest{ - ContainerID: id, - Pid: pid, - Width: uint32(size.Width), - Height: uint32(size.Height), - }); err != nil { - return err + if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil { + logrus.WithError(err).Error("resize pty") } s := make(chan os.Signal, 16) signal.Notify(s, unix.SIGWINCH) @@ -350,15 +36,86 @@ func handleConsoleResize(ctx context.Context, service execution.TasksClient, id logrus.WithError(err).Error("get pty size") continue } - if _, err := service.Pty(ctx, &execution.PtyRequest{ - ContainerID: id, - Pid: pid, - Width: uint32(size.Width), - Height: uint32(size.Height), - }); err != nil { + if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil { logrus.WithError(err).Error("resize pty") } } }() return nil } + +func withTTY() containerd.SpecOpts { + return containerd.WithTTY +} + +func setHostNetworking() containerd.SpecOpts { + return containerd.WithHostNamespace(specs.NetworkNamespace) +} + +func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) { + var ( + err error + checkpointIndex digest.Digest + + ref = context.Args().First() + id = context.Args().Get(1) + args = context.Args()[2:] + tty = context.Bool("tty") + ) + if raw := context.String("checkpoint"); raw != "" { + if checkpointIndex, err = digest.Parse(raw); err != nil { + return nil, err + } + } + image, err := client.GetImage(ctx, ref) + if err != nil { + return nil, err + } + if checkpointIndex == "" { + opts := []containerd.SpecOpts{ + containerd.WithImageConfig(ctx, image), + withEnv(context), + withMounts(context), + } + if len(args) > 0 { + opts = append(opts, containerd.WithProcessArgs(args...)) + } + if tty { + opts = append(opts, withTTY()) + } + if context.Bool("net-host") { + opts = append(opts, setHostNetworking()) + } + spec, err := containerd.GenerateSpec(opts...) + if err != nil { + return nil, err + } + var rootfs containerd.NewContainerOpts + if context.Bool("readonly") { + rootfs = containerd.WithNewReadonlyRootFS(id, image) + } else { + rootfs = containerd.WithNewRootFS(id, image) + } + return client.NewContainer(ctx, id, + containerd.WithSpec(spec), + containerd.WithImage(image), + rootfs, + ) + } + return client.NewContainer(ctx, id, containerd.WithCheckpoint(v1.Descriptor{ + Digest: checkpointIndex, + }, id)) +} + +func newTask(ctx gocontext.Context, container containerd.Container, checkpoint digest.Digest, tty bool) (containerd.Task, error) { + if checkpoint == "" { + io := containerd.Stdio + if tty { + io = containerd.StdioTerminal + } + return container.NewTask(ctx, io) + } + return container.NewTask(ctx, containerd.Stdio, containerd.WithTaskCheckpoint(v1.Descriptor{ + Digest: checkpoint, + })) +} diff --git a/cmd/ctr/run_windows.go b/cmd/ctr/run_windows.go index 7e08c1b9d..81a0e6c40 100644 --- a/cmd/ctr/run_windows.go +++ b/cmd/ctr/run_windows.go @@ -1,7 +1,7 @@ package main import ( - "context" + gocontext "context" "encoding/json" "fmt" "io/ioutil" @@ -10,6 +10,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/containerd/console" + "github.com/containerd/containerd" containersapi "github.com/containerd/containerd/api/services/containers" "github.com/containerd/containerd/api/services/execution" "github.com/containerd/containerd/log" @@ -17,6 +18,7 @@ import ( "github.com/containerd/containerd/windows" "github.com/containerd/containerd/windows/hcs" protobuf "github.com/gogo/protobuf/types" + digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/urfave/cli" @@ -169,7 +171,7 @@ func newCreateTaskRequest(context *cli.Context, id, tmpDir string, checkpoint *o return create, nil } -func handleConsoleResize(ctx context.Context, service execution.TasksClient, id string, pid uint32, con console.Console) error { +func handleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error { // do an initial resize of the console size, err := con.Size() if err != nil { @@ -187,13 +189,8 @@ func handleConsoleResize(ctx context.Context, service execution.TasksClient, id } if size.Width != prevSize.Width || size.Height != prevSize.Height { - if _, err := service.Pty(ctx, &execution.PtyRequest{ - ContainerID: id, - Pid: pid, - Width: uint32(size.Width), - Height: uint32(size.Height), - }); err != nil { - log.G(ctx).WithError(err).Error("resize pty") + if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil { + logrus.WithError(err).Error("resize pty") } prevSize = size } @@ -201,3 +198,68 @@ func handleConsoleResize(ctx context.Context, service execution.TasksClient, id }() return nil } + +func withTTY() containerd.SpecOpts { + con := console.Current() + size, err := con.Size() + if err != nil { + logrus.WithError(err).Error("console size") + } + return containerd.WithTTY(int(size.Width), int(size.Height)) +} + +func setHostNetworking() containerd.SpecOpts { + return nil +} + +func newContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) { + var ( + err error + + ref = context.Args().First() + id = context.Args().Get(1) + args = context.Args()[2:] + tty = context.Bool("tty") + ) + image, err := client.GetImage(ctx, ref) + if err != nil { + return nil, err + } + opts := []containerd.SpecOpts{ + containerd.WithImageConfig(ctx, image), + withEnv(context), + withMounts(context), + } + if len(args) > 0 { + opts = append(opts, containerd.WithProcessArgs(args...)) + } + if tty { + opts = append(opts, withTTY()) + } + if context.Bool("net-host") { + opts = append(opts, setHostNetworking()) + } + spec, err := containerd.GenerateSpec(opts...) + if err != nil { + return nil, err + } + var rootfs containerd.NewContainerOpts + if context.Bool("readonly") { + rootfs = containerd.WithNewReadonlyRootFS(id, image) + } else { + rootfs = containerd.WithNewRootFS(id, image) + } + return client.NewContainer(ctx, id, + containerd.WithSpec(spec), + containerd.WithImage(image), + rootfs, + ) +} + +func newTask(ctx gocontext.Context, container containerd.Container, _ digest.Digest, tty bool) (containerd.Task, error) { + io := containerd.Stdio + if tty { + io = containerd.StdioTerminal + } + return container.NewTask(ctx, io) +} diff --git a/cmd/ctr/utils.go b/cmd/ctr/utils.go index 01c429433..4b944c5c1 100644 --- a/cmd/ctr/utils.go +++ b/cmd/ctr/utils.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/Sirupsen/logrus" + "github.com/containerd/containerd" containersapi "github.com/containerd/containerd/api/services/containers" contentapi "github.com/containerd/containerd/api/services/content" diffapi "github.com/containerd/containerd/api/services/diff" @@ -70,6 +71,10 @@ func getNamespacesService(clicontext *cli.Context) (namespaces.Store, error) { return namespacesservice.NewStoreFromClient(namespacesapi.NewNamespacesClient(conn)), nil } +func newClient(context *cli.Context) (*containerd.Client, error) { + return containerd.New(context.GlobalString("address")) +} + func getContainersService(context *cli.Context) (containersapi.ContainersClient, error) { conn, err := getGRPCConnection(context) if err != nil { @@ -153,23 +158,14 @@ func waitContainer(events execution.Tasks_EventsClient, id string, pid uint32) ( } } -func forwardAllSignals(containers execution.TasksClient, id string) chan os.Signal { +func forwardAllSignals(ctx gocontext.Context, task killer) chan os.Signal { sigc := make(chan os.Signal, 128) signal.Notify(sigc) - go func() { for s := range sigc { - logrus.Debug("Forwarding signal ", s) - killRequest := &execution.KillRequest{ - ContainerID: id, - Signal: uint32(s.(syscall.Signal)), - PidOrAll: &execution.KillRequest_All{ - All: false, - }, - } - _, err := containers.Kill(gocontext.Background(), killRequest) - if err != nil { - logrus.Fatalln(err) + logrus.Debug("forwarding signal ", s) + if err := task.Kill(ctx, s.(syscall.Signal)); err != nil { + logrus.WithError(err).Errorf("forward signal %s", s) } } }() diff --git a/cmd/ctr/version.go b/cmd/ctr/version.go index 169c03e1e..1628e3044 100644 --- a/cmd/ctr/version.go +++ b/cmd/ctr/version.go @@ -6,7 +6,6 @@ import ( "os" "github.com/containerd/containerd/version" - empty "github.com/golang/protobuf/ptypes/empty" "github.com/urfave/cli" ) @@ -21,14 +20,15 @@ var versionCommand = cli.Command{ fmt.Printf(" Version: %s\n", version.Version) fmt.Printf(" Revision: %s\n", version.Revision) fmt.Println("") - vs, err := getVersionService(context) + client, err := newClient(context) if err != nil { return err } - v, err := vs.Version(gocontext.Background(), &empty.Empty{}) + v, err := client.Version(gocontext.Background()) if err != nil { return err } + fmt.Println("Server:") fmt.Printf(" Version: %s\n", v.Version) fmt.Printf(" Revision: %s\n", v.Revision) diff --git a/container.go b/container.go index 8534512f1..792af4ed0 100644 --- a/container.go +++ b/container.go @@ -3,21 +3,30 @@ package containerd import ( "context" "encoding/json" + "fmt" "path/filepath" + "sync" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" "github.com/containerd/containerd/api/services/containers" "github.com/containerd/containerd/api/services/execution" "github.com/containerd/containerd/api/types/mount" specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" ) +var ErrNoRunningTask = errors.New("no running task") + type Container interface { ID() string + Proto() containers.Container Delete(context.Context) error NewTask(context.Context, IOCreation, ...NewTaskOpts) (Task, error) Spec() (*specs.Spec, error) - Task() Task - LoadTask(context.Context, IOAttach) (Task, error) + Task(context.Context, IOAttach) (Task, error) + Image(context.Context) (Image, error) } func containerFromProto(client *Client, c containers.Container) *container { @@ -30,10 +39,11 @@ func containerFromProto(client *Client, c containers.Container) *container { var _ = (Container)(&container{}) type container struct { - client *Client + mu sync.Mutex - c containers.Container - task *task + client *Client + c containers.Container + task *task } // ID returns the container's unique id @@ -41,6 +51,10 @@ func (c *container) ID() string { return c.c.ID } +func (c *container) Proto() containers.Container { + return c.c +} + // Spec returns the current OCI specification for the container func (c *container) Spec() (*specs.Spec, error) { var s specs.Spec @@ -58,7 +72,6 @@ func (c *container) Delete(ctx context.Context) (err error) { if c.c.RootFS != "" { err = c.client.SnapshotService().Remove(ctx, c.c.RootFS) } - if _, cerr := c.client.ContainerService().Delete(ctx, &containers.DeleteContainerRequest{ ID: c.c.ID, }); err == nil { @@ -67,13 +80,39 @@ func (c *container) Delete(ctx context.Context) (err error) { return err } -func (c *container) Task() Task { - return c.task +func (c *container) Task(ctx context.Context, attach IOAttach) (Task, error) { + c.mu.Lock() + defer c.mu.Unlock() + if c.task == nil { + t, err := c.loadTask(ctx, attach) + if err != nil { + return nil, err + } + c.task = t.(*task) + } + return c.task, nil +} + +// Image returns the image that the container is based on +func (c *container) Image(ctx context.Context) (Image, error) { + if c.c.Image == "" { + return nil, fmt.Errorf("container is not based on an image") + } + i, err := c.client.ImageService().Get(ctx, c.c.Image) + if err != nil { + return nil, err + } + return &image{ + client: c.client, + i: i, + }, nil } type NewTaskOpts func(context.Context, *Client, *execution.CreateRequest) error func (c *container) NewTask(ctx context.Context, ioCreate IOCreation, opts ...NewTaskOpts) (Task, error) { + c.mu.Lock() + defer c.mu.Unlock() i, err := ioCreate() if err != nil { return nil, err @@ -126,28 +165,33 @@ func (c *container) NewTask(ctx context.Context, ioCreate IOCreation, opts ...Ne return t, nil } -func (c *container) LoadTask(ctx context.Context, ioAttach IOAttach) (Task, error) { +func (c *container) loadTask(ctx context.Context, ioAttach IOAttach) (Task, error) { response, err := c.client.TaskService().Info(ctx, &execution.InfoRequest{ ContainerID: c.c.ID, }) if err != nil { + if grpc.Code(errors.Cause(err)) == codes.NotFound { + return nil, ErrNoRunningTask + } return nil, err } - // get the existing fifo paths from the task information stored by the daemon - paths := &FifoSet{ - Dir: getFifoDir([]string{ - response.Task.Stdin, - response.Task.Stdout, - response.Task.Stderr, - }), - In: response.Task.Stdin, - Out: response.Task.Stdout, - Err: response.Task.Stderr, - Terminal: response.Task.Terminal, - } - i, err := ioAttach(paths) - if err != nil { - return nil, err + var i *IO + if ioAttach != nil { + // get the existing fifo paths from the task information stored by the daemon + paths := &FifoSet{ + Dir: getFifoDir([]string{ + response.Task.Stdin, + response.Task.Stdout, + response.Task.Stderr, + }), + In: response.Task.Stdin, + Out: response.Task.Stdout, + Err: response.Task.Stderr, + Terminal: response.Task.Terminal, + } + if i, err = ioAttach(paths); err != nil { + return nil, err + } } t := &task{ client: c.client, diff --git a/container_test.go b/container_test.go index 9245c1203..3a85255f0 100644 --- a/container_test.go +++ b/container_test.go @@ -558,7 +558,7 @@ func TestContainerAttach(t *testing.T) { t.Error(err) return } - if task, err = container.LoadTask(ctx, WithAttach(r, ow, ioutil.Discard)); err != nil { + if task, err = container.Task(ctx, WithAttach(r, ow, ioutil.Discard)); err != nil { t.Error(err) return } diff --git a/container_linux.go b/container_unix.go similarity index 91% rename from container_linux.go rename to container_unix.go index d3d2b25e8..8a97daa89 100644 --- a/container_linux.go +++ b/container_unix.go @@ -1,3 +1,5 @@ +// +build !windows + package containerd import ( @@ -19,20 +21,6 @@ import ( specs "github.com/opencontainers/runtime-spec/specs-go" ) -func WithSpec(spec *specs.Spec) NewContainerOpts { - return func(ctx context.Context, client *Client, c *containers.Container) error { - data, err := json.Marshal(spec) - if err != nil { - return err - } - c.Spec = &protobuf.Any{ - TypeUrl: spec.Version, - Value: data, - } - return nil - } -} - func WithCheckpoint(desc v1.Descriptor, rootfsID string) NewContainerOpts { // set image and rw, and spec return func(ctx context.Context, client *Client, c *containers.Container) error { diff --git a/process.go b/process.go index f98304fee..2bcd8d479 100644 --- a/process.go +++ b/process.go @@ -110,3 +110,7 @@ func (p *process) Resize(ctx context.Context, w, h uint32) error { }) return err } + +func (p *process) Delete() error { + return p.io.Close() +} diff --git a/spec_unix.go b/spec_unix.go index 393c96135..f0b071068 100644 --- a/spec_unix.go +++ b/spec_unix.go @@ -10,7 +10,9 @@ import ( "strconv" "strings" + "github.com/containerd/containerd/api/services/containers" "github.com/containerd/containerd/images" + protobuf "github.com/gogo/protobuf/types" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -255,3 +257,17 @@ func WithImageConfig(ctx context.Context, i Image) SpecOpts { return nil } } + +func WithSpec(spec *specs.Spec) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + data, err := json.Marshal(spec) + if err != nil { + return err + } + c.Spec = &protobuf.Any{ + TypeUrl: spec.Version, + Value: data, + } + return nil + } +} diff --git a/spec_windows.go b/spec_windows.go index 61fba1e59..4af108ada 100644 --- a/spec_windows.go +++ b/spec_windows.go @@ -6,7 +6,9 @@ import ( "fmt" "runtime" + "github.com/containerd/containerd/api/services/containers" "github.com/containerd/containerd/images" + protobuf "github.com/gogo/protobuf/types" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -76,3 +78,17 @@ func WithTTY(width, height int) SpecOpts { return nil } } + +func WithSpec(spec *specs.Spec) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + data, err := json.Marshal(spec) + if err != nil { + return err + } + c.Spec = &protobuf.Any{ + TypeUrl: spec.Version, + Value: data, + } + return nil + } +} diff --git a/task.go b/task.go index 9ba99bbf1..25753a4ae 100644 --- a/task.go +++ b/task.go @@ -52,6 +52,7 @@ type Task interface { type Process interface { Pid() uint32 Start(context.Context) error + Delete() error Kill(context.Context, syscall.Signal) error Wait(context.Context) (uint32, error) CloseStdin(context.Context) error