diff --git a/client.go b/client.go index 5f049af60..b52d25e7c 100644 --- a/client.go +++ b/client.go @@ -2,11 +2,23 @@ package containerd import ( "context" + "encoding/json" "io/ioutil" "log" "time" "github.com/containerd/containerd/api/services/containers" + contentapi "github.com/containerd/containerd/api/services/content" + snapshotapi "github.com/containerd/containerd/api/services/snapshot" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + contentservice "github.com/containerd/containerd/services/content" + snapshotservice "github.com/containerd/containerd/services/snapshot" + "github.com/containerd/containerd/snapshot" + protobuf "github.com/gogo/protobuf/types" + "github.com/opencontainers/image-spec/identity" + "github.com/opencontainers/image-spec/specs-go/v1" + specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" @@ -51,6 +63,88 @@ func (c *Client) Containers(ctx context.Context) ([]*Container, error) { return out, nil } +type NewContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error + +// NewContainerWithLables adds the provided labels to the container +func NewContainerWithLables(labels map[string]string) NewContainerOpts { + return func(_ context.Context, _ *Client, c *containers.Container) error { + c.Labels = labels + return nil + } +} + +// NewContainerWithExistingRootFS uses an existing root filesystem for the container +func NewContainerWithExistingRootFS(id string) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + // check that the snapshot exists, if not, fail on creation + if _, err := client.snapshotter().Mounts(ctx, id); err != nil { + return err + } + c.RootFS = id + return nil + } +} + +// 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 { + return func(ctx context.Context, client *Client, c *containers.Container) error { + diffIDs, err := images.RootFS(ctx, client.content(), image) + if err != nil { + return err + } + if _, err := client.snapshotter().Prepare(ctx, id, identity.ChainID(diffIDs).String()); err != nil { + return err + } + c.RootFS = id + return nil + } +} + +// 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 { + return func(ctx context.Context, client *Client, c *containers.Container) error { + diffIDs, err := images.RootFS(ctx, client.content(), image) + if err != nil { + return err + } + if _, err := client.snapshotter().View(ctx, id, identity.ChainID(diffIDs).String()); err != nil { + return err + } + c.RootFS = id + 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) { + data, err := json.Marshal(spec) + if err != nil { + return nil, err + } + container := containers.Container{ + ID: id, + Spec: &protobuf.Any{ + TypeUrl: specs.Version, + Value: data, + }, + } + for _, o := range opts { + if err := o(ctx, c, &container); err != nil { + return nil, err + } + } + r, err := c.containers().Create(ctx, &containers.CreateContainerRequest{ + Container: container, + }) + if err != nil { + return nil, err + } + return containerFromProto(c, r.Container), nil +} + // Close closes the clients connection to containerd func (c *Client) Close() error { return c.conn.Close() @@ -59,3 +153,11 @@ func (c *Client) Close() error { func (c *Client) containers() containers.ContainersClient { return containers.NewContainersClient(c.conn) } + +func (c *Client) content() content.Store { + return contentservice.NewStoreFromClient(contentapi.NewContentClient(c.conn)) +} + +func (c *Client) snapshotter() snapshot.Snapshotter { + return snapshotservice.NewSnapshotterFromClient(snapshotapi.NewSnapshotClient(c.conn)) +} diff --git a/client_test.go b/client_test.go index 12e29df59..054c02a64 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,9 @@ package containerd -import "testing" +import ( + "context" + "testing" +) const defaultAddress = "/run/containerd/containerd.sock" @@ -16,3 +19,38 @@ func TestNewClient(t *testing.T) { t.Errorf("client closed returned errror %v", err) } } + +func TestNewContainer(t *testing.T) { + client, err := New(defaultAddress) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + id := "test" + spec, err := GenerateSpec(WithHostname(id)) + if err != nil { + t.Error(err) + return + } + container, err := client.NewContainer(context.Background(), id, spec) + if err != nil { + t.Error(err) + return + } + if container.ID() != id { + t.Errorf("expected container id %q but received %q", id, container.ID()) + } + if spec, err = container.Spec(); err != nil { + t.Error(err) + return + } + if spec.Hostname != id { + t.Errorf("expected spec hostname id %q but received %q", id, container.ID()) + return + } + if err := container.Delete(context.Background()); err != nil { + t.Error(err) + return + } +} diff --git a/container.go b/container.go index d47b0615e..861a71496 100644 --- a/container.go +++ b/container.go @@ -1,21 +1,47 @@ package containerd -import "github.com/containerd/containerd/api/services/containers" +import ( + "context" + "encoding/json" + + "github.com/containerd/containerd/api/services/containers" + specs "github.com/opencontainers/runtime-spec/specs-go" +) func containerFromProto(client *Client, c containers.Container) *Container { return &Container{ client: client, - id: c.ID, + c: c, } } type Container struct { client *Client - id string + c containers.Container } // ID returns the container's unique id func (c *Container) ID() string { - return c.id + return c.c.ID +} + +// Spec returns the current OCI specification for the container +func (c *Container) Spec() (*specs.Spec, error) { + var s specs.Spec + if err := json.Unmarshal(c.c.Spec.Value, &s); err != nil { + return nil, err + } + return &s, nil +} + +// Delete deletes an existing container +// an error is returned if the container has running tasks +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{ + ID: c.c.ID, + }) + return err }