Skip to content

Commit

Permalink
namespaces: support within containerd
Browse files Browse the repository at this point in the history
To support multi-tenancy, containerd allows the collection of metadata
and runtime objects within a heirarchical storage primitive known as
namespaces. Data cannot be shared across these namespaces, unless
allowed by the service. This allows multiple sets of containers to
managed without interaction between the clients that management. This
means that different users, such as SwarmKit, K8s, Docker and others can
use containerd without coordination. Through labels, one may use
namespaces as a tool for cleanly organizing the use of containerd
containers, including the metadata storage for higher level features,
such as ACLs.

Namespaces

Namespaces cross-cut all containerd operations and are communicated via
context, either within the Go context or via GRPC headers. As a general
rule, no features are tied to namespace, other than organization. This
will be maintained into the future. They are created as a side-effect of
operating on them or may be created manually. Namespaces can be labeled
for organization. They cannot be deleted unless the namespace is empty,
although we may want to make it so one can clean up the entirety of
containerd by deleting a namespace.

Most users will interface with namespaces by setting in the
context or via the `CONTAINERD_NAMESPACE` environment variable, but the
experience is mostly left to the client. For `ctr` and `dist`, we have
defined a "default" namespace that will be created up on use, but there
is nothing special about it. As part of this PR we have plumbed this
behavior through all commands, cleaning up context management along the
way.

Namespaces in Action

Namespaces can be managed with the `ctr namespaces` subcommand. They
can be created, labeled and destroyed.

A few commands can demonstrate the power of namespaces for use with
images. First, lets create a namespace:

```
$ ctr namespaces create foo mylabel=bar
$ ctr namespaces ls
NAME LABELS
foo  mylabel=bar
```

We can see that we have a namespace `foo` and it has a label. Let's pull
an image:

```
$ dist pull docker.io/library/redis:latest
docker.io/library/redis:latest: resolved       |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:548a75066f3f280eb017a6ccda34c561ccf4f25459ef8e36d6ea582b6af1decf: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d45bc46b48e45e8c72c41aedd2a173bcc7f1ea4084a8fcfc5251b1da2a09c0b6: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:5b690bc4eaa6434456ceaccf9b3e42229bd2691869ba439e515b28fe1a66c009: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:a858478874d144f6bfc03ae2d4598e2942fc9994159f2872e39fae88d45bd847: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:4cdd94354d2a873333a205a02dbb853dd763c73600e0cf64f60b4bd7ab694875: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:c54584150374aa94b9f7c3fbd743adcff5adead7a3cf7207b0e51551ac4a5517: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d1f9221193a65eaf1b0afc4f1d4fbb7f0f209369d2696e1c07671668e150ed2b: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:71c1f30d820f0457df186531dc4478967d075ba449bd3168a3e82137a47daf03: done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.9 s total:   0.0 B (0.0 B/s)
INFO[0000] unpacking rootfs
INFO[0000] Unpacked chain id: sha256:41719840acf0f89e761f4a97c6074b6e2c6c25e3830fcb39301496b5d36f9b51
```

Now, let's list the image:

```
$ dist images ls
REF                            TYPE  DIGEST SIZE
docker.io/library/redis:latest application/vnd.docker.distribution.manifest.v2+json sha256:548a75066f3f280eb017a6ccda34c561ccf4f25459ef8e36d6ea582b6af1decf 72.7 MiB
```

That looks normal. Let's list the images for the `foo` namespace and see
this in action:

```
$ CONTAINERD_NAMESPACE=foo dist images ls
REF TYPE DIGEST SIZE
```

Look at that! Nothing was pulled in the namespace `foo`. Let's do the
same pull:

```
$ CONTAINERD_NAMESPACE=foo dist pull docker.io/library/redis:latest
docker.io/library/redis:latest: resolved       |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:548a75066f3f280eb017a6ccda34c561ccf4f25459ef8e36d6ea582b6af1decf: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d45bc46b48e45e8c72c41aedd2a173bcc7f1ea4084a8fcfc5251b1da2a09c0b6: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:a858478874d144f6bfc03ae2d4598e2942fc9994159f2872e39fae88d45bd847: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:4cdd94354d2a873333a205a02dbb853dd763c73600e0cf64f60b4bd7ab694875: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:c54584150374aa94b9f7c3fbd743adcff5adead7a3cf7207b0e51551ac4a5517: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:71c1f30d820f0457df186531dc4478967d075ba449bd3168a3e82137a47daf03: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d1f9221193a65eaf1b0afc4f1d4fbb7f0f209369d2696e1c07671668e150ed2b: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:10a267c67f423630f3afe5e04bbbc93d578861ddcc54283526222f3ad5e895b9: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:5b690bc4eaa6434456ceaccf9b3e42229bd2691869ba439e515b28fe1a66c009: done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.8 s total:   0.0 B (0.0 B/s)
INFO[0000] unpacking rootfs
INFO[0000] Unpacked chain id: sha256:41719840acf0f89e761f4a97c6074b6e2c6c25e3830fcb39301496b5d36f9b51
```

Wow, that was very snappy! Looks like we pulled that image into out
namespace but didn't have to download any new data because we are
sharing storage. Let's take a peak at the images we have in `foo`:

```
$ CONTAINERD_NAMESPACE=foo dist images ls
REF                            TYPE DIGEST SIZE
docker.io/library/redis:latest application/vnd.docker.distribution.manifest.v2+json sha256:548a75066f3f280eb017a6ccda34c561ccf4f25459ef8e36d6ea582b6af1decf 72.7 MiB
```

Now, let's remove that image from `foo`:

```
$ CONTAINERD_NAMESPACE=foo dist images rm
docker.io/library/redis:latest
```

Looks like it is gone:

```
$ CONTAINERD_NAMESPACE=foo dist images ls
REF TYPE DIGEST SIZE
```

But, as we can see, it is present in the `default` namespace:

```
$ dist images ls
REF                            TYPE DIGEST SIZE
docker.io/library/redis:latest application/vnd.docker.distribution.manifest.v2+json sha256:548a75066f3f280eb017a6ccda34c561ccf4f25459ef8e36d6ea582b6af1decf 72.7 MiB
```

What happened here? We can tell by listing the namespaces to get a
better understanding:

```
$ ctr namespaces ls
NAME    LABELS
default
foo     mylabel=bar
```

From the above, we can see that the `default` namespace was created with
the standard commands without the environment variable set. Isolating
the set of shared images while sharing the data that matters.

Since we removed the images for namespace `foo`, we can remove it now:

```
$ ctr namespaces rm foo
foo
```

However, when we try to remove the `default` namespace, we get an error:

```
$ ctr namespaces rm default
ctr: unable to delete default: rpc error: code = FailedPrecondition desc = namespace default must be empty
```

This is because we require that namespaces be empty when removed.

Caveats

- While most metadata objects are namespaced, containers and tasks may
exhibit some issues. We still need to move runtimes to namespaces and
the container metadata storage may not be fully worked out.
- Still need to migrate content store to metadata storage and namespace
the content store such that some data storage (ie images).
- Specifics of snapshot driver's relation to namespace needs to be
worked out in detail.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
  • Loading branch information
stevvooe committed Jun 6, 2017
1 parent 25cc761 commit af2718b
Show file tree
Hide file tree
Showing 52 changed files with 1,184 additions and 223 deletions.
105 changes: 54 additions & 51 deletions api/services/namespaces/namespace.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions api/services/namespaces/namespace.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ service Namespaces {
}

message Namespace {
string namespace = 1;
string name = 1;

// Labels provides an area to include arbitrary data on namespaces.
//
Expand Down Expand Up @@ -72,6 +72,10 @@ message UpdateNamespaceRequest {

// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// For the most part, this applies only to selectively updating labels on
// the namespace. While field masks are typically limited to ascii alphas
// and digits, we just take everything after the "labels." as the map key.
google.protobuf.FieldMask update_mask = 2;
}

Expand All @@ -80,5 +84,5 @@ message UpdateNamespaceResponse {
}

message DeleteNamespaceRequest {
string namespace = 1;
string name = 1;
}
11 changes: 7 additions & 4 deletions checkpoint_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package containerd

import (
"context"
"syscall"
"testing"
)
Expand All @@ -17,9 +16,11 @@ func TestCheckpointRestore(t *testing.T) {
defer client.Close()

var (
ctx = context.Background()
id = "CheckpointRestore"
ctx, cancel = testContext()
id = "CheckpointRestore"
)
defer cancel()

image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
Expand Down Expand Up @@ -107,7 +108,9 @@ func TestCheckpointRestoreNewContainer(t *testing.T) {
defer client.Close()

const id = "CheckpointRestoreNewContainer"
ctx := context.Background()
ctx, cancel := testContext()
defer cancel()

image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
Expand Down
15 changes: 6 additions & 9 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
diffapi "github.com/containerd/containerd/api/services/diff"
"github.com/containerd/containerd/api/services/execution"
imagesapi "github.com/containerd/containerd/api/services/images"
namespacesapi "github.com/containerd/containerd/api/services/namespaces"
snapshotapi "github.com/containerd/containerd/api/services/snapshot"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
Expand Down Expand Up @@ -43,13 +44,6 @@ func init() {

type NewClientOpts func(c *Client) error

func WithNamespace(namespace string) NewClientOpts {
return func(c *Client) error {
c.namespace = namespace
return nil
}
}

// New returns a new containerd client that is connected to the containerd
// instance provided by address
func New(address string, opts ...NewClientOpts) (*Client, error) {
Expand Down Expand Up @@ -79,8 +73,7 @@ func New(address string, opts ...NewClientOpts) (*Client, error) {
type Client struct {
conn *grpc.ClientConn

runtime string
namespace string
runtime string
}

func (c *Client) IsServing(ctx context.Context) (bool, error) {
Expand Down Expand Up @@ -438,6 +431,10 @@ func (c *Client) Close() error {
return c.conn.Close()
}

func (c *Client) NamespaceService() namespacesapi.NamespacesClient {
return namespacesapi.NewNamespacesClient(c.conn)
}

func (c *Client) ContainerService() containers.ContainersClient {
return containers.NewContainersClient(c.conn)
}
Expand Down
30 changes: 23 additions & 7 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"syscall"
"testing"
"time"

"github.com/containerd/containerd/namespaces"
)

const (
Expand All @@ -29,14 +31,23 @@ func init() {
flag.Parse()
}

func testContext() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
ctx = namespaces.WithNamespace(ctx, "testing")
return ctx, cancel
}

func TestMain(m *testing.M) {
if testing.Short() {
os.Exit(m.Run())
}
var (
cmd *exec.Cmd
buf = bytes.NewBuffer(nil)
cmd *exec.Cmd
buf = bytes.NewBuffer(nil)
ctx, cancel = testContext()
)
defer cancel()

if !noDaemon {
// setup a new containerd daemon if !testing.Short
cmd = exec.Command("containerd",
Expand All @@ -55,12 +66,13 @@ func TestMain(m *testing.M) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := waitForDaemonStart(client); err != nil {
if err := waitForDaemonStart(ctx, client); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

// pull a seed image
if _, err = client.Pull(context.Background(), testImage, WithPullUnpack); err != nil {
if _, err = client.Pull(ctx, testImage, WithPullUnpack); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)

Expand Down Expand Up @@ -92,13 +104,14 @@ func TestMain(m *testing.M) {
os.Exit(status)
}

func waitForDaemonStart(client *Client) error {
func waitForDaemonStart(ctx context.Context, client *Client) error {
var (
serving bool
err error
)

for i := 0; i < 20; i++ {
serving, err = client.IsServing(context.Background())
serving, err = client.IsServing(ctx)
if serving {
return nil
}
Expand Down Expand Up @@ -127,13 +140,16 @@ func TestImagePull(t *testing.T) {
if testing.Short() {
t.Skip()
}
ctx, cancel := testContext()
defer cancel()

client, err := New(address)
if err != nil {
t.Fatal(err)
}
defer client.Close()

_, err = client.Pull(context.Background(), testImage)
_, err = client.Pull(ctx, testImage)
if err != nil {
t.Error(err)
return
Expand Down
Loading

0 comments on commit af2718b

Please sign in to comment.