diff --git a/client.go b/client.go index 5771f9e76..acc37e295 100644 --- a/client.go +++ b/client.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "log" "net/http" + "runtime" "time" "github.com/containerd/containerd/api/services/containers" @@ -34,30 +35,43 @@ import ( "google.golang.org/grpc/grpclog" ) -// New returns a new containerd client that is connected to the containerd -// instance provided by address -func New(address string) (*Client, error) { +func init() { // reset the grpc logger so that it does not output in the STDIO of the calling process grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags)) +} - opts := []grpc.DialOption{ +type NewClientOpts func(c *Client) error + +// New returns a new containerd client that is connected to the containerd +// instance provided by address +func New(address string, opts ...NewClientOpts) (*Client, error) { + gopts := []grpc.DialOption{ grpc.WithInsecure(), grpc.WithTimeout(100 * time.Second), grpc.WithDialer(dialer), } - conn, err := grpc.Dial(dialAddress(address), opts...) + conn, err := grpc.Dial(dialAddress(address), gopts...) if err != nil { return nil, errors.Wrapf(err, "failed to dial %q", address) } - return &Client{ - conn: conn, - }, nil + c := &Client{ + conn: conn, + Runtime: runtime.GOOS, + } + for _, o := range opts { + if err := o(c); err != nil { + return nil, err + } + } + return c, nil } // Client is the client to interact with containerd and its various services // using a uniform interface type Client struct { conn *grpc.ClientConn + + Runtime string } // Containers returns all containers created in containerd @@ -97,9 +111,9 @@ func NewContainerWithExistingRootFS(id string) NewContainerOpts { // NewContainerWithNewRootFS allocates a new snapshot to be used by the container as the // root filesystem in read-write mode -func NewContainerWithNewRootFS(id string, image v1.Descriptor) NewContainerOpts { +func NewContainerWithNewRootFS(id string, image *Image) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { - diffIDs, err := images.RootFS(ctx, client.content(), image) + diffIDs, err := image.i.RootFS(ctx, client.content()) if err != nil { return err } @@ -113,9 +127,9 @@ func NewContainerWithNewRootFS(id string, image v1.Descriptor) NewContainerOpts // NewContainerWithNewReadonlyRootFS allocates a new snapshot to be used by the container as the // root filesystem in read-only mode -func NewContainerWithNewReadonlyRootFS(id string, image v1.Descriptor) NewContainerOpts { +func NewContainerWithNewReadonlyRootFS(id string, image *Image) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { - diffIDs, err := images.RootFS(ctx, client.content(), image) + diffIDs, err := image.i.RootFS(ctx, client.content()) if err != nil { return err } @@ -127,6 +141,13 @@ func NewContainerWithNewReadonlyRootFS(id string, image v1.Descriptor) NewContai } } +func NewContainerWithRuntime(name string) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + c.Runtime = name + return nil + } +} + // NewContainer will create a new container in container with the provided id // the id must be unique within the namespace func (c *Client) NewContainer(ctx context.Context, id string, spec *specs.Spec, opts ...NewContainerOpts) (*Container, error) { @@ -135,7 +156,8 @@ func (c *Client) NewContainer(ctx context.Context, id string, spec *specs.Spec, return nil, err } container := containers.Container{ - ID: id, + ID: id, + Runtime: c.Runtime, Spec: &protobuf.Any{ TypeUrl: specs.Version, Value: data, @@ -237,7 +259,11 @@ func (c *Client) Pull(ctx context.Context, ref string, opts ...PullOpts) (*Image } store := c.content() - name, desc, fetcher, err := pullCtx.Resolver.Resolve(ctx, ref) + name, desc, err := pullCtx.Resolver.Resolve(ctx, ref) + if err != nil { + return nil, err + } + fetcher, err := pullCtx.Resolver.Fetcher(ctx, name) if err != nil { return nil, err } @@ -263,7 +289,8 @@ func (c *Client) Pull(ctx context.Context, ref string, opts ...PullOpts) (*Image } } return &Image{ - i: i, + client: c, + i: i, }, nil } diff --git a/container.go b/container.go index 62599e577..34c64c9c2 100644 --- a/container.go +++ b/container.go @@ -43,9 +43,13 @@ func (c *Container) Spec() (*specs.Spec, error) { func (c *Container) Delete(ctx context.Context) error { // TODO: should the client be the one removing resources attached // to the container at the moment before we have GC? - _, err := c.client.containers().Delete(ctx, &containers.DeleteContainerRequest{ + err := c.client.snapshotter().Remove(ctx, c.c.RootFS) + + if _, cerr := c.client.containers().Delete(ctx, &containers.DeleteContainerRequest{ ID: c.c.ID, - }) + }); err == nil { + err = cerr + } return err } diff --git a/image.go b/image.go index 2fa94b6d5..83e042ded 100644 --- a/image.go +++ b/image.go @@ -3,5 +3,7 @@ package containerd import "github.com/containerd/containerd/images" type Image struct { + client *Client + i images.Image } diff --git a/spec.go b/spec.go index 5913d29b6..835f0ad72 100644 --- a/spec.go +++ b/spec.go @@ -14,13 +14,6 @@ func WithImageRef(ref string) SpecOpts { } } -func WithHostname(id string) SpecOpts { - return func(s *specs.Spec) error { - s.Hostname = id - return nil - } -} - func WithArgs(args ...string) SpecOpts { return func(s *specs.Spec) error { s.Process.Args = args @@ -30,8 +23,8 @@ func WithArgs(args ...string) SpecOpts { // GenerateSpec will generate a default spec from the provided image // for use as a containerd container -func GenerateSpec(opts ...SpecOpts) (*specs.Spec, error) { - s, err := createDefaultSpec() +func GenerateSpec(id string, opts ...SpecOpts) (*specs.Spec, error) { + s, err := createDefaultSpec(id) if err != nil { return nil, err } diff --git a/spec_unix.go b/spec_unix.go index b994125bf..94fdce1ad 100644 --- a/spec_unix.go +++ b/spec_unix.go @@ -1,11 +1,14 @@ package containerd import ( + "context" + "encoding/json" "fmt" "runtime" "strconv" "strings" + "github.com/containerd/containerd/images" "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" ) @@ -54,7 +57,7 @@ func defaultNamespaces() []specs.LinuxNamespace { } } -func createDefaultSpec() (*specs.Spec, error) { +func createDefaultSpec(id string) (*specs.Spec, error) { s := &specs.Spec{ Version: specs.Version, Platform: specs.Platform{ @@ -64,6 +67,7 @@ func createDefaultSpec() (*specs.Spec, error) { Root: specs.Root{ Path: defaultRootfsPath, }, + Hostname: id, Process: specs.Process{ Cwd: "/", NoNewPrivileges: true, @@ -180,16 +184,40 @@ func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts { } } -func WithImage(config *v1.ImageConfig) SpecOpts { +func WithImage(ctx context.Context, image *Image) SpecOpts { return func(s *specs.Spec) error { + store := image.client.content() + ic, err := image.i.Config(ctx, store) + if err != nil { + return err + } + var ( + ociimage v1.Image + config v1.ImageConfig + ) + switch ic.MediaType { + case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config: + r, err := store.Reader(ctx, ic.Digest) + if err != nil { + return err + } + if err := json.NewDecoder(r).Decode(&ociimage); err != nil { + r.Close() + return err + } + r.Close() + config = ociimage.Config + default: + return fmt.Errorf("unknown image config media type %s", ic.MediaType) + } env := []string{ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", } s.Process.Env = append(env, config.Env...) - cmd := config.Cmd var ( uid, gid uint32 ) + cmd := config.Cmd s.Process.Args = append(config.Entrypoint, cmd...) if config.User != "" { parts := strings.Split(config.User, ":") diff --git a/task.go b/task.go index def67fd7e..36f6440ea 100644 --- a/task.go +++ b/task.go @@ -10,6 +10,7 @@ import ( "syscall" "github.com/containerd/containerd/api/services/execution" + "github.com/containerd/containerd/api/types/task" "github.com/containerd/fifo" ) @@ -31,9 +32,9 @@ func (i *IO) Close() error { type IOCreation func() (*IO, error) -// STDIO returns an IO implementation to be used for a task -// that outputs the container's IO as the current processes STDIO -func STDIO() (*IO, error) { +// Stdio returns an IO implementation to be used for a task +// that outputs the container's IO as the current processes Stdio +func Stdio() (*IO, error) { paths, err := fifoPaths() if err != nil { return nil, err @@ -57,7 +58,11 @@ func STDIO() (*IO, error) { } func fifoPaths() (*fifoSet, error) { - dir, err := ioutil.TempDir(filepath.Join(os.TempDir(), "containerd"), "") + root := filepath.Join(os.TempDir(), "containerd") + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + dir, err := ioutil.TempDir(root, "") if err != nil { return nil, err } @@ -135,16 +140,21 @@ func copyIO(fifos *fifoSet, ioset *ioSet, tty bool) (closer io.Closer, err error }(f) } return &wgCloser{ - wg: wg, + wg: wg, + dir: fifos.dir, }, nil } type wgCloser struct { - wg *sync.WaitGroup + wg *sync.WaitGroup + dir string } func (g *wgCloser) Close() error { g.wg.Wait() + if g.dir != "" { + return os.RemoveAll(g.dir) + } return nil } @@ -161,8 +171,16 @@ func (t *Task) Pid() uint32 { return t.pid } -func (t *Task) Kill(ctx context.Context, s os.Signal) error { +func (t *Task) Start(ctx context.Context) error { + _, err := t.client.tasks().Start(ctx, &execution.StartRequest{ + ContainerID: t.containerID, + }) + return err +} + +func (t *Task) Kill(ctx context.Context, s syscall.Signal) error { _, err := t.client.tasks().Kill(ctx, &execution.KillRequest{ + Signal: uint32(s), ContainerID: t.containerID, PidOrAll: &execution.KillRequest_All{ All: true, @@ -171,6 +189,26 @@ func (t *Task) Kill(ctx context.Context, s os.Signal) error { return err } +// Wait is a blocking call that will wait for the task to exit and return the exit status +func (t *Task) Wait(ctx context.Context) (uint32, error) { + events, err := t.client.tasks().Events(ctx, &execution.EventsRequest{}) + if err != nil { + return 255, err + } + for { + e, err := events.Recv() + if err != nil { + return 255, err + } + if e.Type != task.Event_EXIT { + continue + } + if e.ID == t.containerID && e.Pid == t.pid { + return e.ExitStatus, nil + } + } +} + // Delete deletes the task and its runtime state // it returns the exit status of the task and any errors that were encountered // during cleanup