namespaces: support within containerd

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>
This commit is contained in:
Stephen J Day 2017-06-05 17:45:50 -07:00
parent 25cc7614ae
commit af2718b01f
No known key found for this signature in database
GPG Key ID: 67B3DED84EDC823F
52 changed files with 1184 additions and 223 deletions

View File

@ -52,7 +52,7 @@ var _ = math.Inf
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type Namespace struct {
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Labels provides an area to include arbitrary data on namespaces.
//
// Note that to add a new value to this field, read the existing set and
@ -124,6 +124,10 @@ type UpdateNamespaceRequest struct {
Namespace Namespace `protobuf:"bytes,1,opt,name=namespace" json:"namespace"`
// 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.
UpdateMask *google_protobuf2.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask" json:"update_mask,omitempty"`
}
@ -140,7 +144,7 @@ func (*UpdateNamespaceResponse) ProtoMessage() {}
func (*UpdateNamespaceResponse) Descriptor() ([]byte, []int) { return fileDescriptorNamespace, []int{8} }
type DeleteNamespaceRequest struct {
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (m *DeleteNamespaceRequest) Reset() { *m = DeleteNamespaceRequest{} }
@ -379,11 +383,11 @@ func (m *Namespace) MarshalTo(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if len(m.Namespace) > 0 {
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintNamespace(dAtA, i, uint64(len(m.Namespace)))
i += copy(dAtA[i:], m.Namespace)
i = encodeVarintNamespace(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Labels) > 0 {
for k, _ := range m.Labels {
@ -638,11 +642,11 @@ func (m *DeleteNamespaceRequest) MarshalTo(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if len(m.Namespace) > 0 {
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintNamespace(dAtA, i, uint64(len(m.Namespace)))
i += copy(dAtA[i:], m.Namespace)
i = encodeVarintNamespace(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
return i, nil
}
@ -677,7 +681,7 @@ func encodeVarintNamespace(dAtA []byte, offset int, v uint64) int {
func (m *Namespace) Size() (n int) {
var l int
_ = l
l = len(m.Namespace)
l = len(m.Name)
if l > 0 {
n += 1 + l + sovNamespace(uint64(l))
}
@ -771,7 +775,7 @@ func (m *UpdateNamespaceResponse) Size() (n int) {
func (m *DeleteNamespaceRequest) Size() (n int) {
var l int
_ = l
l = len(m.Namespace)
l = len(m.Name)
if l > 0 {
n += 1 + l + sovNamespace(uint64(l))
}
@ -806,7 +810,7 @@ func (this *Namespace) String() string {
}
mapStringForLabels += "}"
s := strings.Join([]string{`&Namespace{`,
`Namespace:` + fmt.Sprintf("%v", this.Namespace) + `,`,
`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
`Labels:` + mapStringForLabels + `,`,
`}`,
}, "")
@ -898,7 +902,7 @@ func (this *DeleteNamespaceRequest) String() string {
return "nil"
}
s := strings.Join([]string{`&DeleteNamespaceRequest{`,
`Namespace:` + fmt.Sprintf("%v", this.Namespace) + `,`,
`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
`}`,
}, "")
return s
@ -942,7 +946,7 @@ func (m *Namespace) Unmarshal(dAtA []byte) error {
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Namespace", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
@ -967,7 +971,7 @@ func (m *Namespace) Unmarshal(dAtA []byte) error {
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Namespace = string(dAtA[iNdEx:postIndex])
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
@ -1809,7 +1813,7 @@ func (m *DeleteNamespaceRequest) Unmarshal(dAtA []byte) error {
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Namespace", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
@ -1834,7 +1838,7 @@ func (m *DeleteNamespaceRequest) Unmarshal(dAtA []byte) error {
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Namespace = string(dAtA[iNdEx:postIndex])
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
@ -1967,39 +1971,38 @@ func init() {
}
var fileDescriptorNamespace = []byte{
// 533 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcd, 0x8e, 0xd3, 0x30,
0x10, 0xae, 0xdb, 0x52, 0xa9, 0x93, 0x0b, 0x32, 0x25, 0x44, 0x01, 0x85, 0x2a, 0x5c, 0x96, 0x03,
0x0e, 0x5b, 0x24, 0xc4, 0xcf, 0x6d, 0x61, 0x29, 0x48, 0x0b, 0x87, 0x48, 0x9c, 0x57, 0x4e, 0xeb,
0x86, 0xa8, 0xf9, 0x23, 0x76, 0x2a, 0xf5, 0xc6, 0x1b, 0xf0, 0x06, 0xbc, 0x01, 0xef, 0xd1, 0x23,
0x47, 0x4e, 0x88, 0xed, 0x93, 0xa0, 0x38, 0x69, 0xd3, 0x6d, 0xd3, 0xa8, 0x2b, 0x95, 0xdb, 0xd8,
0x9e, 0xcf, 0xdf, 0xcc, 0xf8, 0xfb, 0x12, 0x78, 0xef, 0x7a, 0xe2, 0x4b, 0xea, 0x90, 0x51, 0x14,
0x58, 0xa3, 0x28, 0x14, 0xd4, 0x0b, 0x59, 0x32, 0xde, 0x0c, 0x69, 0xec, 0x59, 0x9c, 0x25, 0x33,
0x6f, 0xc4, 0xb8, 0x15, 0xd2, 0x80, 0xf1, 0x98, 0x5e, 0x0b, 0x49, 0x9c, 0x44, 0x22, 0xc2, 0x5a,
0x89, 0x21, 0xb3, 0x53, 0x52, 0x66, 0xea, 0x3d, 0x37, 0x72, 0x23, 0x99, 0x64, 0x65, 0x51, 0x9e,
0xaf, 0xdf, 0x77, 0xa3, 0xc8, 0xf5, 0x99, 0x25, 0x57, 0x4e, 0x3a, 0xb1, 0x58, 0x10, 0x8b, 0x79,
0x71, 0xd8, 0xdf, 0x3e, 0x9c, 0x78, 0xcc, 0x1f, 0x5f, 0x06, 0x94, 0x4f, 0xf3, 0x0c, 0xf3, 0x27,
0x82, 0xee, 0xa7, 0x15, 0x07, 0x7e, 0x00, 0xdd, 0x35, 0xa1, 0x86, 0xfa, 0xe8, 0xa4, 0x6b, 0x97,
0x1b, 0x78, 0x08, 0x1d, 0x9f, 0x3a, 0xcc, 0xe7, 0x5a, 0xb3, 0xdf, 0x3a, 0x51, 0x06, 0x16, 0xd9,
0x57, 0x2b, 0x59, 0x5f, 0x49, 0x2e, 0x24, 0xe2, 0x3c, 0x14, 0xc9, 0xdc, 0x2e, 0xe0, 0xfa, 0x4b,
0x50, 0x36, 0xb6, 0xf1, 0x6d, 0x68, 0x4d, 0xd9, 0xbc, 0xe0, 0xcb, 0x42, 0xdc, 0x83, 0x5b, 0x33,
0xea, 0xa7, 0x4c, 0x6b, 0xca, 0xbd, 0x7c, 0xf1, 0xaa, 0xf9, 0x02, 0x99, 0x8f, 0xe1, 0xce, 0x90,
0x89, 0xf5, 0xf5, 0x36, 0xfb, 0x9a, 0x32, 0x2e, 0x30, 0x86, 0x76, 0xc6, 0x5e, 0xdc, 0x21, 0x63,
0xf3, 0x12, 0x7a, 0xd7, 0x53, 0x79, 0x1c, 0x85, 0x3c, 0x6b, 0x63, 0xab, 0x49, 0x65, 0xf0, 0xe8,
0x80, 0x4e, 0xce, 0xda, 0x8b, 0x3f, 0x0f, 0x1b, 0x1b, 0xf3, 0x30, 0x2d, 0xb8, 0x7b, 0xe1, 0xf1,
0x92, 0x81, 0xaf, 0xaa, 0x51, 0xa1, 0x33, 0xf1, 0x7c, 0xc1, 0x92, 0xa2, 0x9e, 0x62, 0x65, 0x8e,
0x40, 0xdd, 0x06, 0x14, 0x35, 0x7d, 0x00, 0x28, 0x39, 0x35, 0x24, 0xc7, 0x7b, 0x83, 0xa2, 0x36,
0xc0, 0x26, 0x05, 0xf5, 0x4d, 0xc2, 0xa8, 0x60, 0x3b, 0x43, 0x3a, 0x5a, 0xe3, 0x0e, 0xdc, 0xdb,
0xa1, 0x38, 0xf6, 0x70, 0x7f, 0x20, 0x50, 0x3f, 0xc7, 0xe3, 0xff, 0xd9, 0x07, 0x7e, 0x0d, 0x4a,
0x2a, 0x29, 0xa4, 0x23, 0xa4, 0xd8, 0x94, 0x81, 0x4e, 0x72, 0xd3, 0x90, 0x95, 0x69, 0xc8, 0xbb,
0xcc, 0x34, 0x1f, 0x29, 0x9f, 0xda, 0x90, 0xa7, 0x67, 0x71, 0x36, 0x84, 0x9d, 0xfa, 0x8e, 0x3d,
0x84, 0xe7, 0xa0, 0xbe, 0x65, 0x3e, 0xab, 0x98, 0x41, 0xad, 0x53, 0x07, 0xdf, 0xdb, 0x00, 0xa5,
0xca, 0xf0, 0x18, 0x5a, 0x43, 0x26, 0xf0, 0x93, 0xfd, 0x35, 0x54, 0x78, 0x4a, 0x27, 0x87, 0xa6,
0x17, 0x5d, 0x7b, 0xd0, 0xce, 0xd4, 0x8d, 0x6b, 0x3e, 0x0b, 0x95, 0x76, 0xd1, 0x9f, 0x1e, 0x0e,
0x28, 0xa8, 0x02, 0xe8, 0xe4, 0x02, 0xc4, 0x35, 0xd8, 0x6a, 0x17, 0xe8, 0xa7, 0x37, 0x40, 0x94,
0x74, 0xf9, 0x53, 0xd7, 0xd1, 0x55, 0x8b, 0xb5, 0x8e, 0x6e, 0x9f, 0x7c, 0x6c, 0xe8, 0xe4, 0xaf,
0x5e, 0x47, 0x57, 0xad, 0x0b, 0x5d, 0xdd, 0x51, 0xef, 0x79, 0xf6, 0x3f, 0x38, 0xd3, 0x16, 0x57,
0x46, 0xe3, 0xf7, 0x95, 0xd1, 0xf8, 0xb6, 0x34, 0xd0, 0x62, 0x69, 0xa0, 0x5f, 0x4b, 0x03, 0xfd,
0x5d, 0x1a, 0xc8, 0xe9, 0xc8, 0xcc, 0x67, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0xd8, 0xf7, 0xf8,
0x92, 0xc3, 0x06, 0x00, 0x00,
// 528 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x54, 0xbd, 0x8e, 0xd3, 0x4c,
0x14, 0xcd, 0x24, 0xf9, 0x2c, 0xe5, 0xba, 0xf9, 0x34, 0x04, 0x63, 0x19, 0xc9, 0x44, 0xa6, 0x59,
0x24, 0x18, 0xb3, 0xa1, 0xe1, 0xa7, 0x5b, 0x58, 0x02, 0xd2, 0x42, 0x61, 0x89, 0x7a, 0x35, 0x4e,
0x26, 0xc6, 0x8a, 0xff, 0xf0, 0x8c, 0x23, 0xa5, 0xe3, 0x0d, 0x78, 0x03, 0x1a, 0x5e, 0x26, 0x25,
0x25, 0x15, 0x62, 0xf3, 0x24, 0xc8, 0x63, 0x27, 0xce, 0x6e, 0x1c, 0x2b, 0x2b, 0x85, 0xee, 0x8e,
0x7d, 0x8e, 0xcf, 0xb9, 0xd7, 0xe7, 0x0e, 0xbc, 0xf3, 0x7c, 0xf1, 0x39, 0x73, 0xc9, 0x38, 0x0e,
0xed, 0x71, 0x1c, 0x09, 0xea, 0x47, 0x2c, 0x9d, 0x6c, 0x97, 0x34, 0xf1, 0x6d, 0xce, 0xd2, 0xb9,
0x3f, 0x66, 0xdc, 0x8e, 0x68, 0xc8, 0x78, 0x42, 0xaf, 0x95, 0x24, 0x49, 0x63, 0x11, 0x63, 0xbd,
0xe2, 0x90, 0xf9, 0x29, 0xa9, 0x90, 0x46, 0xdf, 0x8b, 0xbd, 0x58, 0x82, 0xec, 0xbc, 0x2a, 0xf0,
0xc6, 0x7d, 0x2f, 0x8e, 0xbd, 0x80, 0xd9, 0xf2, 0xe4, 0x66, 0x53, 0x9b, 0x85, 0x89, 0x58, 0x94,
0x2f, 0x07, 0x37, 0x5f, 0x4e, 0x7d, 0x16, 0x4c, 0x2e, 0x43, 0xca, 0x67, 0x05, 0xc2, 0xfa, 0x81,
0xa0, 0xf7, 0x71, 0xad, 0x81, 0x31, 0x74, 0x73, 0x41, 0x1d, 0x0d, 0xd0, 0x49, 0xcf, 0x91, 0x35,
0x1e, 0x81, 0x12, 0x50, 0x97, 0x05, 0x5c, 0x6f, 0x0f, 0x3a, 0x27, 0xea, 0xd0, 0x26, 0xfb, 0x1c,
0x92, 0xcd, 0x87, 0xc8, 0x85, 0x64, 0x9c, 0x47, 0x22, 0x5d, 0x38, 0x25, 0xdd, 0x78, 0x01, 0xea,
0xd6, 0x63, 0xfc, 0x3f, 0x74, 0x66, 0x6c, 0x51, 0x4a, 0xe5, 0x25, 0xee, 0xc3, 0x7f, 0x73, 0x1a,
0x64, 0x4c, 0x6f, 0xcb, 0x67, 0xc5, 0xe1, 0x65, 0xfb, 0x39, 0xb2, 0x1e, 0xc1, 0x9d, 0x11, 0x13,
0x9b, 0xcf, 0x3b, 0xec, 0x4b, 0xc6, 0xb8, 0xa8, 0xb3, 0x6b, 0x5d, 0x42, 0xff, 0x3a, 0x94, 0x27,
0x71, 0xc4, 0xf3, 0x36, 0x7a, 0x1b, 0xa7, 0x92, 0xa0, 0x0e, 0x1f, 0x1e, 0xd0, 0xc9, 0x59, 0x77,
0xf9, 0xfb, 0x41, 0xcb, 0xa9, 0xb8, 0x96, 0x0d, 0x77, 0x2f, 0x7c, 0x5e, 0x29, 0xf0, 0xb5, 0x1b,
0x0d, 0x94, 0xa9, 0x1f, 0x08, 0x96, 0x96, 0x7e, 0xca, 0x93, 0x35, 0x06, 0xed, 0x26, 0xa1, 0xf4,
0xf4, 0x1e, 0xa0, 0xd2, 0xd4, 0x91, 0x1c, 0xef, 0x2d, 0x4c, 0x6d, 0x91, 0x2d, 0x0a, 0xda, 0xeb,
0x94, 0x51, 0xc1, 0x76, 0x86, 0x74, 0xb4, 0xc6, 0x5d, 0xb8, 0xb7, 0x23, 0x71, 0xec, 0xe1, 0x7e,
0x47, 0xa0, 0x7d, 0x4a, 0x26, 0xff, 0xb2, 0x0f, 0xfc, 0x0a, 0xd4, 0x4c, 0x4a, 0xc8, 0x3d, 0x90,
0x61, 0x53, 0x87, 0x06, 0x29, 0x56, 0x85, 0xac, 0x57, 0x85, 0xbc, 0xcd, 0x57, 0xe5, 0x03, 0xe5,
0x33, 0x07, 0x0a, 0x78, 0x5e, 0xe7, 0x43, 0xd8, 0xf1, 0x77, 0xec, 0x21, 0x3c, 0x06, 0xed, 0x0d,
0x0b, 0x58, 0xcd, 0x0c, 0x6a, 0x02, 0x3f, 0xfc, 0xd6, 0x05, 0xa8, 0xb2, 0x85, 0x27, 0xd0, 0x19,
0x31, 0x81, 0x9f, 0xec, 0x57, 0xae, 0xd9, 0x24, 0x83, 0x1c, 0x0a, 0x2f, 0x7b, 0xf5, 0xa1, 0x9b,
0x67, 0x1a, 0x37, 0x5c, 0x06, 0xb5, 0x4b, 0x62, 0x3c, 0x3d, 0x9c, 0x50, 0x4a, 0x85, 0xa0, 0x14,
0xb1, 0xc3, 0x0d, 0xdc, 0xfa, 0xec, 0x1b, 0xa7, 0xb7, 0x60, 0x54, 0x72, 0xc5, 0x0f, 0x6e, 0x92,
0xab, 0x8f, 0x68, 0x93, 0xdc, 0xbe, 0xd0, 0x38, 0xa0, 0x14, 0xff, 0xba, 0x49, 0xae, 0x3e, 0x0d,
0x86, 0xb6, 0x93, 0xd9, 0xf3, 0xfc, 0xee, 0x3f, 0xd3, 0x97, 0x57, 0x66, 0xeb, 0xd7, 0x95, 0xd9,
0xfa, 0xba, 0x32, 0xd1, 0x72, 0x65, 0xa2, 0x9f, 0x2b, 0x13, 0xfd, 0x59, 0x99, 0xc8, 0x55, 0x24,
0xf2, 0xd9, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x2e, 0xc3, 0x29, 0xaf, 0x06, 0x00, 0x00,
}

View File

@ -26,7 +26,7 @@ service Namespaces {
}
message Namespace {
string namespace = 1;
string name = 1;
// Labels provides an area to include arbitrary data on namespaces.
//
@ -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;
}
@ -80,5 +84,5 @@ message UpdateNamespaceResponse {
}
message DeleteNamespaceRequest {
string namespace = 1;
string name = 1;
}

View File

@ -1,7 +1,6 @@
package containerd
import (
"context"
"syscall"
"testing"
)
@ -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)
@ -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)

View File

@ -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"
@ -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) {
@ -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) {
@ -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)
}

View File

@ -10,6 +10,8 @@ import (
"syscall"
"testing"
"time"
"github.com/containerd/containerd/namespaces"
)
const (
@ -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",
@ -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)
@ -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
}
@ -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

View File

@ -10,6 +10,7 @@ import (
_ "github.com/containerd/containerd/services/healthcheck"
_ "github.com/containerd/containerd/services/images"
_ "github.com/containerd/containerd/services/metrics"
_ "github.com/containerd/containerd/services/namespaces"
_ "github.com/containerd/containerd/services/snapshot"
_ "github.com/containerd/containerd/services/version"
)

View File

@ -23,11 +23,11 @@ import (
diffapi "github.com/containerd/containerd/api/services/diff"
api "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"
versionapi "github.com/containerd/containerd/api/services/version"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/snapshot"
"github.com/containerd/containerd/sys"
@ -265,10 +265,6 @@ func resolveMetaDB(ctx *cli.Context) (*bolt.DB, error) {
return nil, err
}
if err := metadata.InitDB(db); err != nil {
return nil, err
}
return db, nil
}
@ -474,6 +470,8 @@ func interceptor(ctx gocontext.Context,
ctx = log.WithModule(ctx, "snapshot")
case diffapi.DiffServer:
ctx = log.WithModule(ctx, "diff")
case namespacesapi.NamespacesServer:
ctx = log.WithModule(ctx, "namespaces")
default:
log.G(ctx).Warnf("unknown GRPC server type: %#v\n", info.Server)
}

View File

@ -40,9 +40,11 @@ var checkpointCommand = cli.Command{
},
Action: func(context *cli.Context) error {
var (
id = context.String("id")
ctx = gocontext.Background()
id = context.String("id")
ctx, cancel = appContext(context)
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"runtime"
"google.golang.org/grpc"
@ -18,6 +17,9 @@ var deleteCommand = cli.Command{
Usage: "delete an existing container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
ctx, cancel := appContext(context)
defer cancel()
containers, err := getContainersService(context)
if err != nil {
return err
@ -35,7 +37,7 @@ var deleteCommand = cli.Command{
if id == "" {
return errors.New("container id must be provided")
}
ctx := gocontext.TODO()
_, err = containers.Delete(ctx, &containersapi.DeleteContainerRequest{
ID: id,
})

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"fmt"
"os"
"text/tabwriter"
@ -14,11 +13,14 @@ 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(gocontext.Background(), &execution.EventsRequest{})
events, err := tasks.Events(ctx, &execution.EventsRequest{})
if err != nil {
return err
}

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"os"
"github.com/Sirupsen/logrus"
@ -30,9 +29,11 @@ var execCommand = cli.Command{
},
Action: func(context *cli.Context) error {
var (
id = context.String("id")
ctx = gocontext.Background()
id = context.String("id")
ctx, cancel = appContext(context)
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}

View File

@ -22,7 +22,12 @@ var infoCommand = cli.Command{
},
},
Action: func(context *cli.Context) error {
id := context.String("id")
var (
id = context.String("id")
ctx, cancel = appContext(context)
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}
@ -36,7 +41,7 @@ var infoCommand = cli.Command{
return err
}
containerResponse, err := containers.Get(gocontext.TODO(), &containersapi.GetContainerRequest{ID: id})
containerResponse, err := containers.Get(ctx, &containersapi.GetContainerRequest{ID: id})
if err != nil {
return err
}

View File

@ -1,8 +1,6 @@
package main
import (
gocontext "context"
"github.com/containerd/containerd/api/services/execution"
"github.com/pkg/errors"
"github.com/urfave/cli"
@ -27,7 +25,12 @@ var killCommand = cli.Command{
},
},
Action: func(context *cli.Context) error {
id := context.String("id")
var (
id = context.String("id")
ctx, cancel = appContext(context)
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}
@ -66,7 +69,7 @@ var killCommand = cli.Command{
if err != nil {
return err
}
_, err = tasks.Kill(gocontext.Background(), killRequest)
_, err = tasks.Kill(ctx, killRequest)
if err != nil {
return err
}

View File

@ -27,7 +27,11 @@ var listCommand = cli.Command{
},
},
Action: func(context *cli.Context) error {
quiet := context.Bool("quiet")
var (
quiet = context.Bool("quiet")
ctx, cancel = appContext(context)
)
defer cancel()
tasks, err := getTasksService(context)
if err != nil {
@ -50,7 +54,7 @@ var listCommand = cli.Command{
}
} else {
tasksResponse, err := tasks.List(gocontext.TODO(), &execution.ListRequest{})
tasksResponse, err := tasks.List(ctx, &execution.ListRequest{})
if err != nil {
return err
}

View File

@ -42,12 +42,15 @@ containerd client
Usage: "address for containerd's GRPC server",
Value: "/run/containerd/containerd.sock",
},
cli.DurationFlag{
Name: "timeout",
Usage: "total timeout for ctr commands",
},
cli.StringFlag{
// TODO(stevvooe): for now, we allow circumventing the GRPC. Once
// we have clear separation, this will likely go away.
Name: "root",
Usage: "path to content store root",
Value: "/var/lib/containerd",
Name: "namespace, n",
Usage: "namespace to use with commands",
Value: "default",
EnvVar: "CONTAINERD_NAMESPACE",
},
}
app.Commands = []cli.Command{
@ -55,6 +58,7 @@ containerd client
runCommand,
eventsCommand,
deleteCommand,
namespacesCommand,
listCommand,
infoCommand,
killCommand,

201
cmd/ctr/namespaces.go Normal file
View File

@ -0,0 +1,201 @@
package main
import (
"context"
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/metadata"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var namespacesCommand = cli.Command{
Name: "namespaces",
Usage: "manage namespaces",
Subcommands: cli.Commands{
namespacesCreateCommand,
namespacesSetLabelsCommand,
namespacesListCommand,
namespacesRemoveCommand,
},
}
var namespacesCreateCommand = cli.Command{
Name: "create",
Usage: "Create a new namespace.",
ArgsUsage: "[flags] <name> [<key>=<value]",
Description: "Create a new namespace. It must be unique.",
Action: func(clicontext *cli.Context) error {
var (
ctx = context.Background()
namespace, labels = namespaceWithLabelArgs(clicontext)
)
if namespace == "" {
return errors.New("please specify a namespace")
}
namespaces, err := getNamespacesService(clicontext)
if err != nil {
return err
}
if err := namespaces.Create(ctx, namespace, labels); err != nil {
return err
}
return nil
},
}
func namespaceWithLabelArgs(clicontext *cli.Context) (string, map[string]string) {
var (
namespace = clicontext.Args().First()
labelStrings = clicontext.Args().Tail()
labels = make(map[string]string, len(labelStrings))
)
for _, label := range labelStrings {
parts := strings.SplitN(label, "=", 2)
key := parts[0]
value := "true"
if len(parts) > 1 {
value = parts[1]
}
labels[key] = value
}
return namespace, labels
}
var namespacesSetLabelsCommand = cli.Command{
Name: "set-labels",
Usage: "Set and clear labels for a namespace.",
ArgsUsage: "[flags] <name> [<key>=<value>, ...]",
Description: "Set and clear labels for a namespace.",
Flags: []cli.Flag{},
Action: func(clicontext *cli.Context) error {
var (
ctx = context.Background()
namespace, labels = namespaceWithLabelArgs(clicontext)
)
namespaces, err := getNamespacesService(clicontext)
if err != nil {
return err
}
if namespace == "" {
return errors.New("please specify a namespace")
}
for k, v := range labels {
if err := namespaces.SetLabel(ctx, namespace, k, v); err != nil {
return err
}
}
return nil
},
}
var namespacesListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List namespaces.",
ArgsUsage: "[flags]",
Description: "List namespaces.",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the namespace name.",
},
},
Action: func(clicontext *cli.Context) error {
var (
ctx = context.Background()
quiet = clicontext.Bool("quiet")
)
namespaces, err := getNamespacesService(clicontext)
if err != nil {
return err
}
nss, err := namespaces.List(ctx)
if err != nil {
return err
}
if quiet {
for _, ns := range nss {
fmt.Println(ns)
}
} else {
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
fmt.Fprintln(tw, "NAME\tLABELS\t")
for _, ns := range nss {
labels, err := namespaces.Labels(ctx, ns)
if err != nil {
return err
}
var labelStrings []string
for k, v := range labels {
labelStrings = append(labelStrings, strings.Join([]string{k, v}, "="))
}
sort.Strings(labelStrings)
fmt.Fprintf(tw, "%v\t%v\t\n", ns, strings.Join(labelStrings, ","))
}
tw.Flush()
}
return nil
},
}
var namespacesRemoveCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove one or more namespaces",
ArgsUsage: "[flags] <name> [<name>, ...]",
Description: "Remove one or more namespaces. For now, the namespace must be empty.",
Action: func(clicontext *cli.Context) error {
var (
ctx = context.Background()
exitErr error
)
namespaces, err := getNamespacesService(clicontext)
if err != nil {
return err
}
for _, target := range clicontext.Args() {
if err := namespaces.Delete(ctx, target); err != nil {
if !metadata.IsNotFound(err) {
if exitErr == nil {
exitErr = errors.Wrapf(err, "unable to delete %v", target)
}
log.G(ctx).WithError(err).Errorf("unable to delete %v", target)
continue
}
}
fmt.Println(target)
}
return exitErr
},
}

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"errors"
"github.com/containerd/containerd/api/services/execution"
@ -13,6 +12,9 @@ var pauseCommand = cli.Command{
Usage: "pause an existing container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
ctx, cancel := appContext(context)
defer cancel()
tasks, err := getTasksService(context)
if err != nil {
return err
@ -21,7 +23,7 @@ var pauseCommand = cli.Command{
if id == "" {
return errors.New("container id must be provided")
}
_, err = tasks.Pause(gocontext.Background(), &execution.PauseRequest{
_, err = tasks.Pause(ctx, &execution.PauseRequest{
ContainerID: id,
})
return err

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"fmt"
"os"
"text/tabwriter"
@ -21,7 +20,12 @@ var psCommand = cli.Command{
},
},
Action: func(context *cli.Context) error {
id := context.String("id")
var (
id = context.String("id")
ctx, cancel = appContext(context)
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}
@ -35,7 +39,7 @@ var psCommand = cli.Command{
return err
}
resp, err := tasks.Processes(gocontext.Background(), pr)
resp, err := tasks.Processes(ctx, pr)
if err != nil {
return err
}

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"errors"
"github.com/containerd/containerd/api/services/execution"
@ -13,6 +12,9 @@ var resumeCommand = cli.Command{
Usage: "resume a paused container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
ctx, cancel := appContext(context)
defer cancel()
tasks, err := getTasksService(context)
if err != nil {
return err
@ -21,7 +23,7 @@ var resumeCommand = cli.Command{
if id == "" {
return errors.New("container id must be provided")
}
_, err = tasks.Resume(gocontext.Background(), &execution.ResumeRequest{
_, err = tasks.Resume(ctx, &execution.ResumeRequest{
ContainerID: id,
})
return err

View File

@ -1,7 +1,6 @@
package main
import (
gocontext "context"
"encoding/json"
"fmt"
"io/ioutil"
@ -79,9 +78,11 @@ var runCommand = cli.Command{
mounts []mount.Mount
imageConfig ocispec.Image
ctx = gocontext.Background()
id = context.String("id")
ctx, cancel = appContext(context)
id = context.String("id")
)
defer cancel()
if id == "" {
return errors.New("container id must be provided")
}

View File

@ -1,7 +1,6 @@
package main
import (
"context"
"errors"
"fmt"
@ -20,6 +19,9 @@ var snapshotCommand = cli.Command{
},
},
Action: func(clicontext *cli.Context) error {
ctx, cancel := appContext(clicontext)
defer cancel()
id := clicontext.String("id")
if id == "" {
return errors.New("container id must be provided")
@ -37,7 +39,7 @@ var snapshotCommand = cli.Command{
contentRef := fmt.Sprintf("diff-%s", id)
d, err := rootfs.Diff(context.TODO(), id, contentRef, snapshotter, differ)
d, err := rootfs.Diff(ctx, id, contentRef, snapshotter, differ)
if err != nil {
return err
}

View File

@ -18,14 +18,17 @@ 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"
versionservice "github.com/containerd/containerd/api/services/version"
"github.com/containerd/containerd/api/types/task"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
contentservice "github.com/containerd/containerd/services/content"
"github.com/containerd/containerd/services/diff"
imagesservice "github.com/containerd/containerd/services/images"
namespacesservice "github.com/containerd/containerd/services/namespaces"
snapshotservice "github.com/containerd/containerd/services/snapshot"
"github.com/containerd/containerd/snapshot"
specs "github.com/opencontainers/runtime-spec/specs-go"
@ -35,6 +38,38 @@ import (
var grpcConn *grpc.ClientConn
// appContext returns the context for a command. Should only be called once per
// command, near the start.
//
// This will ensure the namespace is picked up and set the timeout, if one is
// defined.
func appContext(clicontext *cli.Context) (gocontext.Context, gocontext.CancelFunc) {
var (
ctx = gocontext.Background()
timeout = clicontext.GlobalDuration("timeout")
namespace = clicontext.GlobalString("namespace")
cancel = func() {}
)
ctx = namespaces.WithNamespace(ctx, namespace)
if timeout > 0 {
ctx, cancel = gocontext.WithTimeout(ctx, timeout)
} else {
ctx, cancel = gocontext.WithCancel(ctx)
}
return ctx, cancel
}
func getNamespacesService(clicontext *cli.Context) (namespaces.Store, error) {
conn, err := getGRPCConnection(clicontext)
if err != nil {
return nil, err
}
return namespacesservice.NewStoreFromClient(namespacesapi.NewNamespacesClient(conn)), nil
}
func getContainersService(context *cli.Context) (containersapi.ContainersClient, error) {
conn, err := getGRPCConnection(context)
if err != nil {

2
cmd/dist/active.go vendored
View File

@ -32,7 +32,7 @@ var activeCommand = cli.Command{
match = context.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
cs, err := resolveContentStore(context)

2
cmd/dist/apply.go vendored
View File

@ -18,7 +18,7 @@ var applyCommand = cli.Command{
var (
dir = context.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
log.G(ctx).Info("applying layer from stdin")

26
cmd/dist/common.go vendored
View File

@ -3,6 +3,7 @@ package main
import (
"bufio"
"context"
contextpkg "context"
"crypto/tls"
"encoding/json"
"fmt"
@ -17,6 +18,7 @@ import (
imagesapi "github.com/containerd/containerd/api/services/images"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/containerd/containerd/rootfs"
@ -54,6 +56,30 @@ func getClient(context *cli.Context) (*containerd.Client, error) {
return containerd.New(address)
}
// appContext returns the context for a command. Should only be called once per
// command, near the start.
//
// This will ensure the namespace is picked up and set the timeout, if one is
// defined.
func appContext(clicontext *cli.Context) (contextpkg.Context, contextpkg.CancelFunc) {
var (
ctx = contextpkg.Background()
timeout = clicontext.GlobalDuration("timeout")
namespace = clicontext.GlobalString("namespace")
cancel = func() {}
)
ctx = namespaces.WithNamespace(ctx, namespace)
if timeout > 0 {
ctx, cancel = contextpkg.WithTimeout(ctx, timeout)
} else {
ctx, cancel = contextpkg.WithCancel(ctx)
}
return ctx, cancel
}
func resolveContentStore(context *cli.Context) (content.Store, error) {
conn, err := connectGRPC(context)
if err != nil {

2
cmd/dist/delete.go vendored
View File

@ -25,7 +25,7 @@ var deleteCommand = cli.Command{
args = []string(context.Args())
exitError error
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
conn, err := connectGRPC(context)

2
cmd/dist/edit.go vendored
View File

@ -30,7 +30,7 @@ var editCommand = cli.Command{
validate = context.String("validate")
object = context.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
if validate != "" {

2
cmd/dist/fetch.go vendored
View File

@ -44,7 +44,7 @@ Most of this is experimental and there are few leaps to make this work.`,
var (
ref = clicontext.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
conn, err := connectGRPC(clicontext)

View File

@ -22,7 +22,7 @@ var fetchObjectCommand = cli.Command{
var (
ref = context.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
resolver, err := getResolver(ctx, context)

2
cmd/dist/get.go vendored
View File

@ -15,7 +15,7 @@ var getCommand = cli.Command{
Description: "Display the image object.",
Flags: []cli.Flag{},
Action: func(context *cli.Context) error {
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
dgst, err := digest.Parse(context.Args().First())

4
cmd/dist/images.go vendored
View File

@ -30,7 +30,7 @@ var imagesListCommand = cli.Command{
Description: `List images registered with containerd.`,
Flags: []cli.Flag{},
Action: func(clicontext *cli.Context) error {
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
imageStore, err := resolveImageStore(clicontext)
@ -75,7 +75,7 @@ var imageRemoveCommand = cli.Command{
var (
exitErr error
)
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
imageStore, err := resolveImageStore(clicontext)

2
cmd/dist/ingest.go vendored
View File

@ -31,7 +31,7 @@ var ingestCommand = cli.Command{
expectedDigest = digest.Digest(context.String("expected-digest"))
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
if err := expectedDigest.Validate(); expectedDigest != "" && err != nil {

2
cmd/dist/list.go vendored
View File

@ -29,7 +29,7 @@ var listCommand = cli.Command{
quiet = context.Bool("quiet")
args = []string(context.Args())
)
ctx, cancel := appContext()
ctx, cancel := appContext(context)
defer cancel()
cs, err := resolveContentStore(context)

16
cmd/dist/main.go vendored
View File

@ -1,7 +1,6 @@
package main
import (
contextpkg "context"
"fmt"
"os"
"time"
@ -22,14 +21,6 @@ func init() {
}
func appContext() (contextpkg.Context, contextpkg.CancelFunc) {
background := contextpkg.Background()
if timeout > 0 {
return contextpkg.WithTimeout(background, timeout)
}
return contextpkg.WithCancel(background)
}
func main() {
app := cli.NewApp()
app.Name = "dist"
@ -68,6 +59,12 @@ distribution tool
Usage: "address for containerd's GRPC server",
Value: "/run/containerd/containerd.sock",
},
cli.StringFlag{
Name: "namespace, n",
Usage: "namespace to use with commands",
Value: "default",
EnvVar: "CONTAINERD_NAMESPACE",
},
}
app.Commands = []cli.Command{
imageCommand,
@ -81,7 +78,6 @@ distribution tool
pushObjectCommand,
}
app.Before = func(context *cli.Context) error {
timeout = context.GlobalDuration("timeout")
if context.GlobalBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}

13
cmd/dist/pull.go vendored
View File

@ -39,7 +39,7 @@ command. As part of this process, we do the following:
ref = clicontext.Args().First()
)
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
cs, err := resolveContentStore(clicontext)
@ -100,20 +100,17 @@ command. As part of this process, we do the following:
}()
defer func() {
// we need new ctx here
ctx, cancel := appContext()
// we need new ctx here, since we run on return.
ctx, cancel := appContext(clicontext)
defer cancel()
// TODO(stevvooe): This section unpacks the layers and resolves the
// root filesystem chainid for the image. For now, we just print
// it, but we should keep track of this in the metadata storage.
image, err := imageStore.Get(ctx, resolvedImageName)
if err != nil {
log.G(ctx).WithError(err).Fatal("Failed to get image")
log.G(ctx).WithError(err).Fatal("failed to get image")
}
layers, err := getImageLayers(ctx, image, cs)
if err != nil {
log.G(ctx).WithError(err).Fatal("Failed to get rootfs layers")
log.G(ctx).WithError(err).Fatal("failed to get rootfs layers")
}
conn, err := connectGRPC(clicontext)

2
cmd/dist/push.go vendored
View File

@ -48,7 +48,7 @@ var pushCommand = cli.Command{
desc ocispec.Descriptor
)
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
client, err := getClient(clicontext)

View File

@ -26,7 +26,7 @@ var pushObjectCommand = cli.Command{
return err
}
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
resolver, err := getResolver(ctx, clicontext)

4
cmd/dist/rootfs.go vendored
View File

@ -32,7 +32,7 @@ var rootfsUnpackCommand = cli.Command{
ArgsUsage: "[flags] <digest>",
Flags: []cli.Flag{},
Action: func(clicontext *cli.Context) error {
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
dgst, err := digest.Parse(clicontext.Args().First())
@ -84,7 +84,7 @@ var rootfsPrepareCommand = cli.Command{
ArgsUsage: "[flags] <digest> <target>",
Flags: []cli.Flag{},
Action: func(clicontext *cli.Context) error {
ctx, cancel := appContext()
ctx, cancel := appContext(clicontext)
defer cancel()
if clicontext.NArg() != 2 {

View File

@ -2,7 +2,6 @@ package containerd
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
@ -27,7 +26,10 @@ func TestContainerList(t *testing.T) {
}
defer client.Close()
containers, err := client.Containers(context.Background())
ctx, cancel := testContext()
defer cancel()
containers, err := client.Containers(ctx)
if err != nil {
t.Errorf("container list returned error %v", err)
return
@ -53,12 +55,16 @@ func TestNewContainer(t *testing.T) {
t.Error(err)
return
}
container, err := client.NewContainer(context.Background(), id, WithSpec(spec))
ctx, cancel := testContext()
defer cancel()
container, err := client.NewContainer(ctx, id, WithSpec(spec))
if err != nil {
t.Error(err)
return
}
defer container.Delete(context.Background())
defer container.Delete(ctx)
if container.ID() != id {
t.Errorf("expected container id %q but received %q", id, container.ID())
}
@ -66,7 +72,7 @@ func TestNewContainer(t *testing.T) {
t.Error(err)
return
}
if err := container.Delete(context.Background()); err != nil {
if err := container.Delete(ctx); err != nil {
t.Error(err)
return
}
@ -83,9 +89,11 @@ func TestContainerStart(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerStart"
ctx, cancel = testContext()
id = "ContainerStart"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
@ -151,10 +159,12 @@ func TestContainerOutput(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerOutput"
expected = "kingkoye"
ctx, cancel = testContext()
id = "ContainerOutput"
expected = "kingkoye"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
@ -222,9 +232,11 @@ func TestContainerExec(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerExec"
ctx, cancel = testContext()
id = "ContainerExec"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
@ -307,9 +319,11 @@ func TestContainerProcesses(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerProcesses"
ctx, cancel = testContext()
id = "ContainerProcesses"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
@ -378,9 +392,11 @@ func TestContainerCloseStdin(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerCloseStdin"
ctx, cancel = testContext()
id = "ContainerCloseStdin"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)
@ -460,9 +476,11 @@ func TestContainerAttach(t *testing.T) {
defer client.Close()
var (
ctx = context.Background()
id = "ContainerAttach"
ctx, cancel = testContext()
id = "ContainerAttach"
)
defer cancel()
image, err := client.GetImage(ctx, testImage)
if err != nil {
t.Error(err)

View File

@ -2,13 +2,36 @@ package metadata
import (
"github.com/boltdb/bolt"
"github.com/containerd/containerd/log"
)
// The layout where a "/" delineates a bucket is desribed in the following
// section. Please try to follow this as closely as possible when adding
// functionality. We can bolster this with helpers and more structure if that
// becomes an issue.
//
// Generically, we try to do the following:
//
// <version>/<namespace>/<object>/<key> -> <field>
//
// version: Currently, this is "v1". Additions can be made to v1 in a backwards
// compatible way. If the layout changes, a new version must be made, along
// with a migration.
//
// namespace: the namespace to which this object belongs.
//
// object: defines which object set is stored in the bucket. There are two
// special objects, "labels" and "indexes". The "labels" bucket stores the
// labels for the parent namespace. The "indexes" object is reserved for
// indexing objects, if we require in the future.
//
// key: object-specific key identifying the storage bucket for the objects
// contents.
var (
bucketKeyStorageVersion = []byte("v1")
bucketKeyImages = []byte("images")
bucketKeyContainers = []byte("containers")
bucketKeyVersion = []byte("v1")
bucketKeyObjectLabels = []byte("labels") // stores the labels for a namespace.
bucketKeyObjectIndexes = []byte("indexes") // reserved
bucketKeyObjectImages = []byte("images") // stores image objects
bucketKeyObjectContainers = []byte("containers") // stores container objects
bucketKeyDigest = []byte("digest")
bucketKeyMediaType = []byte("mediatype")
@ -22,21 +45,6 @@ var (
bucketKeyUpdatedAt = []byte("updatedat")
)
// InitDB will initialize the database for use. The database must be opened for
// write and the caller must not be holding an open transaction.
func InitDB(db *bolt.DB) error {
log.L.Debug("init db")
return db.Update(func(tx *bolt.Tx) error {
if _, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyImages); err != nil {
return err
}
if _, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyContainers); err != nil {
return err
}
return nil
})
}
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
bkt := tx.Bucket(keys[0])
@ -66,44 +74,52 @@ func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error)
return bkt, nil
}
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
bkt := getImagesBucket(tx)
if bkt == nil {
return ErrNotFound
func namespaceLabelsBucketPath(namespace string) [][]byte {
return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectLabels}
}
func withNamespacesLabelsBucket(tx *bolt.Tx, namespace string, fn func(bkt *bolt.Bucket) error) error {
bkt, err := createBucketIfNotExists(tx, namespaceLabelsBucketPath(namespace)...)
if err != nil {
return err
}
return fn(bkt)
}
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
bkt := getImageBucket(tx, name)
if bkt == nil {
return ErrNotFound
func getNamespaceLabelsBucket(tx *bolt.Tx, namespace string) *bolt.Bucket {
return getBucket(tx, namespaceLabelsBucketPath(namespace)...)
}
func imagesBucketPath(namespace string) [][]byte {
return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectImages}
}
func withImagesBucket(tx *bolt.Tx, namespace string, fn func(bkt *bolt.Bucket) error) error {
bkt, err := createBucketIfNotExists(tx, imagesBucketPath(namespace)...)
if err != nil {
return err
}
return fn(bkt)
}
func getImagesBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages)
}
func getImageBucket(tx *bolt.Tx, name string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages, []byte(name))
func getImagesBucket(tx *bolt.Tx, namespace string) *bolt.Bucket {
return getBucket(tx, imagesBucketPath(namespace)...)
}
func createContainersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt, err := tx.CreateBucketIfNotExists(bucketKeyStorageVersion)
bkt, err := createBucketIfNotExists(tx, bucketKeyVersion, bucketKeyObjectContainers)
if err != nil {
return nil, err
}
return bkt.CreateBucketIfNotExists(bucketKeyContainers)
return bkt, nil
}
func getContainersBucket(tx *bolt.Tx) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyContainers)
return getBucket(tx, bucketKeyVersion, bucketKeyObjectContainers)
}
func getContainerBucket(tx *bolt.Tx, id string) *bolt.Bucket {
return getBucket(tx, bucketKeyStorageVersion, bucketKeyContainers, []byte(id))
return getBucket(tx, bucketKeyVersion, bucketKeyObjectContainers, []byte(id))
}

View File

@ -35,13 +35,13 @@ func (s *containerStore) Get(ctx context.Context, id string) (containers.Contain
func (s *containerStore) List(ctx context.Context, filter string) ([]containers.Container, error) {
var (
m = []containers.Container{}
m []containers.Container
bkt = getContainersBucket(s.tx)
)
if bkt == nil {
return m, nil
}
err := bkt.ForEach(func(k, v []byte) error {
if err := bkt.ForEach(func(k, v []byte) error {
cbkt := bkt.Bucket(k)
if cbkt == nil {
return nil
@ -53,8 +53,7 @@ func (s *containerStore) List(ctx context.Context, filter string) ([]containers.
}
m = append(m, container)
return nil
})
if err != nil {
}); err != nil {
return nil, err
}

View File

@ -5,6 +5,7 @@ import "github.com/pkg/errors"
var (
ErrExists = errors.New("metadata: exists")
ErrNotFound = errors.New("metadata: not found")
ErrNotEmpty = errors.New("metadata: namespace not empty")
)
// IsNotFound returns true if the error is due to a missing image.
@ -15,3 +16,7 @@ func IsNotFound(err error) bool {
func IsExists(err error) bool {
return errors.Cause(err) == ErrExists
}
func IsNotEmpty(err error) bool {
return errors.Cause(err) == ErrNotEmpty
}

View File

@ -7,6 +7,7 @@ import (
"github.com/boltdb/bolt"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -21,10 +22,24 @@ func NewImageStore(tx *bolt.Tx) images.Store {
func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error) {
var image images.Image
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
image.Name = name
return readImage(&image, bkt)
}); err != nil {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return images.Image{}, err
}
bkt := getImagesBucket(s.tx, namespace)
if bkt == nil {
return images.Image{}, ErrNotFound
}
ibkt := bkt.Bucket([]byte(name))
if ibkt == nil {
return images.Image{}, ErrNotFound
}
image.Name = name
if err := readImage(&image, ibkt); err != nil {
return images.Image{}, err
}
@ -32,7 +47,12 @@ func (s *imageStore) Get(ctx context.Context, name string) (images.Image, error)
}
func (s *imageStore) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
return withImagesBucket(s.tx, namespace, func(bkt *bolt.Bucket) error {
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
if err != nil {
return err
@ -64,23 +84,30 @@ func (s *imageStore) Put(ctx context.Context, name string, desc ocispec.Descript
func (s *imageStore) List(ctx context.Context) ([]images.Image, error) {
var m []images.Image
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return nil, err
}
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
var (
image = images.Image{
Name: string(k),
}
kbkt = bkt.Bucket(k)
)
bkt := getImagesBucket(s.tx, namespace)
if bkt == nil {
return nil, nil // empty store
}
if err := readImage(&image, kbkt); err != nil {
return err
if err := bkt.ForEach(func(k, v []byte) error {
var (
image = images.Image{
Name: string(k),
}
kbkt = bkt.Bucket(k)
)
m = append(m, image)
return nil
})
if err := readImage(&image, kbkt); err != nil {
return err
}
m = append(m, image)
return nil
}); err != nil {
return nil, err
}
@ -89,7 +116,12 @@ func (s *imageStore) List(ctx context.Context) ([]images.Image, error) {
}
func (s *imageStore) Delete(ctx context.Context, name string) error {
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
namespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
return withImagesBucket(s.tx, namespace, func(bkt *bolt.Bucket) error {
err := bkt.DeleteBucket([]byte(name))
if err == bolt.ErrBucketNotFound {
return ErrNotFound

145
metadata/namespaces.go Normal file
View File

@ -0,0 +1,145 @@
package metadata
import (
"context"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/namespaces"
)
type namespaceStore struct {
tx *bolt.Tx
}
func NewNamespaceStore(tx *bolt.Tx) namespaces.Store {
return &namespaceStore{tx: tx}
}
func (s *namespaceStore) Create(ctx context.Context, namespace string, labels map[string]string) error {
topbkt, err := createBucketIfNotExists(s.tx, bucketKeyVersion)
if err != nil {
return err
}
// provides the already exists error.
bkt, err := topbkt.CreateBucket([]byte(namespace))
if err != nil {
if err == bolt.ErrBucketExists {
return ErrExists
}
return err
}
lbkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectLabels)
if err != nil {
return err
}
for k, v := range labels {
if err := lbkt.Put([]byte(k), []byte(v)); err != nil {
return err
}
}
return nil
}
func (s *namespaceStore) Labels(ctx context.Context, namespace string) (map[string]string, error) {
labels := map[string]string{}
bkt := getNamespaceLabelsBucket(s.tx, namespace)
if bkt == nil {
return labels, nil
}
if err := bkt.ForEach(func(k, v []byte) error {
labels[string(k)] = string(v)
return nil
}); err != nil {
return nil, err
}
return labels, nil
}
func (s *namespaceStore) SetLabel(ctx context.Context, namespace, key, value string) error {
return withNamespacesLabelsBucket(s.tx, namespace, func(bkt *bolt.Bucket) error {
if value == "" {
return bkt.Delete([]byte(key))
}
return bkt.Put([]byte(key), []byte(value))
})
}
func (s *namespaceStore) List(ctx context.Context) ([]string, error) {
bkt := getBucket(s.tx, bucketKeyVersion)
if bkt == nil {
return nil, nil // no namespaces!
}
var namespaces []string
if err := bkt.ForEach(func(k, v []byte) error {
if v != nil {
return nil // not a bucket
}
namespaces = append(namespaces, string(k))
return nil
}); err != nil {
return nil, err
}
return namespaces, nil
}
func (s *namespaceStore) Delete(ctx context.Context, namespace string) error {
bkt := getBucket(s.tx, bucketKeyVersion)
if empty, err := s.namespaceEmpty(ctx, namespace); err != nil {
return err
} else if !empty {
return ErrNotEmpty
}
if err := bkt.DeleteBucket([]byte(namespace)); err != nil {
if err == bolt.ErrBucketNotFound {
return ErrNotFound
}
return err
}
return nil
}
func (s *namespaceStore) namespaceEmpty(ctx context.Context, namespace string) (bool, error) {
ctx = namespaces.WithNamespace(ctx, namespace)
// need to check the various object stores.
imageStore := NewImageStore(s.tx)
images, err := imageStore.List(ctx)
if err != nil {
return false, err
}
if len(images) > 0 {
return false, nil
}
containerStore := NewContainerStore(s.tx)
containers, err := containerStore.List(ctx, "")
if err != nil {
return false, err
}
if len(containers) > 0 {
return false, nil
}
// TODO(stevvooe): Need to add check for content store, as well. Still need
// to make content store namespace aware.
return true, nil
}

42
namespaces/context.go Normal file
View File

@ -0,0 +1,42 @@
package namespaces
import (
"github.com/pkg/errors"
"golang.org/x/net/context"
)
var (
errNamespaceRequired = errors.New("namespace is required")
)
type namespaceKey struct{}
func WithNamespace(ctx context.Context, namespace string) context.Context {
ctx = context.WithValue(ctx, namespaceKey{}, namespace) // set our key for namespace
// also store on the grpc headers so it gets picked up by any clients that
// are using this.
return withGRPCNamespaceHeader(ctx, namespace)
}
func Namespace(ctx context.Context) (string, bool) {
namespace, ok := ctx.Value(namespaceKey{}).(string)
if !ok {
return fromGRPCHeader(ctx)
}
return namespace, ok
}
func IsNamespaceRequired(err error) bool {
return errors.Cause(err) == errNamespaceRequired
}
func NamespaceRequired(ctx context.Context) (string, error) {
namespace, ok := Namespace(ctx)
if !ok || namespace == "" {
return "", errNamespaceRequired
}
return namespace, nil
}

View File

@ -0,0 +1,30 @@
package namespaces
import (
"context"
"testing"
)
func TestContext(t *testing.T) {
ctx := context.Background()
namespace, ok := Namespace(ctx)
if ok {
t.Fatal("namespace should not be present")
}
if namespace != "" {
t.Fatalf("namespace should not be defined: got %q", namespace)
}
expected := "test"
nctx := WithNamespace(ctx, expected)
namespace, ok = Namespace(nctx)
if !ok {
t.Fatal("expected to find a namespace")
}
if namespace != expected {
t.Fatalf("unexpected namespace: %q != %q", namespace, expected)
}
}

44
namespaces/grpc.go Normal file
View File

@ -0,0 +1,44 @@
package namespaces
import (
"golang.org/x/net/context"
"google.golang.org/grpc/metadata"
)
const (
// GRPCHeader defines the header name for specifying a containerd namespace.
GRPCHeader = "containerd-namespace"
)
// NOTE(stevvooe): We can stub this file out if we don't want a grpc dependency here.
func withGRPCNamespaceHeader(ctx context.Context, namespace string) context.Context {
// also store on the grpc headers so it gets picked up by any clients that
// are using this.
nsheader := metadata.Pairs(GRPCHeader, namespace)
md, ok := metadata.FromOutgoingContext(ctx) // merge with outgoing context.
if !ok {
md = nsheader
} else {
// order ensures the latest is first in this list.
md = metadata.Join(nsheader, md)
}
return metadata.NewOutgoingContext(ctx, md)
}
func fromGRPCHeader(ctx context.Context) (string, bool) {
// try to extract for use in grpc servers.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
// TODO(stevvooe): Check outgoing context?
return "", false
}
values := md[GRPCHeader]
if len(values) == 0 {
return "", false
}
return values[0], true
}

21
namespaces/store.go Normal file
View File

@ -0,0 +1,21 @@
package namespaces
import "context"
// Store provides introspection about namespaces.
//
// Note that these are slightly different than other objects, which are record
// oriented. A namespace is really just a name and a set of labels. Objects
// that belong to a namespace are returned when the namespace is assigned to a
// given context.
//
//
type Store interface {
Create(ctx context.Context, namespace string, labels map[string]string) error
Labels(ctx context.Context, namespace string) (map[string]string, error)
SetLabel(ctx context.Context, namespace, key, value string) error
List(ctx context.Context) ([]string, error)
// Delete removes the namespace. The namespace must be empty to be deleted.
Delete(ctx context.Context, namespace string) error
}

View File

@ -5,6 +5,7 @@ import (
"github.com/containerd/containerd/api/types/descriptor"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/namespaces"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"google.golang.org/grpc"
@ -82,6 +83,8 @@ func mapGRPCError(err error, id string) error {
return grpc.Errorf(codes.NotFound, "image %v not found", id)
case metadata.IsExists(err):
return grpc.Errorf(codes.AlreadyExists, "image %v already exists", id)
case namespaces.IsNamespaceRequired(err):
return grpc.Errorf(codes.InvalidArgument, "namespace required, please set %q header", namespaces.GRPCHeader)
}
return err

View File

@ -0,0 +1,95 @@
package namespaces
import (
"context"
"strings"
api "github.com/containerd/containerd/api/services/namespaces"
"github.com/containerd/containerd/namespaces"
"github.com/gogo/protobuf/types"
)
func NewStoreFromClient(client api.NamespacesClient) namespaces.Store {
return &remote{client: client}
}
type remote struct {
client api.NamespacesClient
}
func (r *remote) Create(ctx context.Context, namespace string, labels map[string]string) error {
var req api.CreateNamespaceRequest
req.Namespace = api.Namespace{
Name: namespace,
Labels: labels,
}
_, err := r.client.Create(ctx, &req)
if err != nil {
return rewriteGRPCError(err)
}
return nil
}
func (r *remote) Labels(ctx context.Context, namespace string) (map[string]string, error) {
var req api.GetNamespaceRequest
req.Name = namespace
resp, err := r.client.Get(ctx, &req)
if err != nil {
return nil, rewriteGRPCError(err)
}
return resp.Namespace.Labels, nil
}
func (r *remote) SetLabel(ctx context.Context, namespace, key, value string) error {
var req api.UpdateNamespaceRequest
req.Namespace = api.Namespace{
Name: namespace,
Labels: map[string]string{key: value},
}
req.UpdateMask = &types.FieldMask{
Paths: []string{strings.Join([]string{"labels", key}, ".")},
}
_, err := r.client.Update(ctx, &req)
if err != nil {
return rewriteGRPCError(err)
}
return nil
}
func (r *remote) List(ctx context.Context) ([]string, error) {
var req api.ListNamespacesRequest
resp, err := r.client.List(ctx, &req)
if err != nil {
return nil, rewriteGRPCError(err)
}
var namespaces []string
for _, ns := range resp.Namespaces {
namespaces = append(namespaces, ns.Name)
}
return namespaces, nil
}
func (r *remote) Delete(ctx context.Context, namespace string) error {
var req api.DeleteNamespaceRequest
req.Name = namespace
_, err := r.client.Delete(ctx, &req)
if err != nil {
return rewriteGRPCError(err)
}
return nil
}

View File

@ -0,0 +1,36 @@
package namespaces
import (
"github.com/containerd/containerd/metadata"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func mapGRPCError(err error, id string) error {
switch {
case metadata.IsNotFound(err):
return grpc.Errorf(codes.NotFound, "namespace %v not found", id)
case metadata.IsExists(err):
return grpc.Errorf(codes.AlreadyExists, "namespace %v already exists", id)
case metadata.IsNotEmpty(err):
return grpc.Errorf(codes.FailedPrecondition, "namespace %v must be empty", id)
}
return err
}
func rewriteGRPCError(err error) error {
if err == nil {
return err
}
switch grpc.Code(errors.Cause(err)) {
case codes.AlreadyExists:
return metadata.ErrExists
case codes.NotFound:
return metadata.ErrNotFound
}
return err
}

View File

@ -0,0 +1,164 @@
package namespaces
import (
"strings"
"github.com/boltdb/bolt"
api "github.com/containerd/containerd/api/services/namespaces"
"github.com/containerd/containerd/metadata"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/plugin"
"github.com/golang/protobuf/ptypes/empty"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
func init() {
plugin.Register("namespaces-grpc", &plugin.Registration{
Type: plugin.GRPCPlugin,
Init: func(ic *plugin.InitContext) (interface{}, error) {
return NewService(ic.Meta), nil
},
})
}
type Service struct {
db *bolt.DB
}
var _ api.NamespacesServer = &Service{}
func NewService(db *bolt.DB) api.NamespacesServer {
return &Service{db: db}
}
func (s *Service) Register(server *grpc.Server) error {
api.RegisterNamespacesServer(server, s)
return nil
}
func (s *Service) Get(ctx context.Context, req *api.GetNamespaceRequest) (*api.GetNamespaceResponse, error) {
var resp api.GetNamespaceResponse
return &resp, s.withStoreView(ctx, func(ctx context.Context, store namespaces.Store) error {
labels, err := store.Labels(ctx, req.Name)
if err != nil {
return mapGRPCError(err, req.Name)
}
resp.Namespace = api.Namespace{
Name: req.Name,
Labels: labels,
}
return nil
})
}
func (s *Service) List(ctx context.Context, req *api.ListNamespacesRequest) (*api.ListNamespacesResponse, error) {
var resp api.ListNamespacesResponse
return &resp, s.withStoreView(ctx, func(ctx context.Context, store namespaces.Store) error {
namespaces, err := store.List(ctx)
if err != nil {
return err
}
for _, namespace := range namespaces {
labels, err := store.Labels(ctx, namespace)
if err != nil {
// In general, this should be unlikely, since we are holding a
// transaction to service this request.
return mapGRPCError(err, namespace)
}
resp.Namespaces = append(resp.Namespaces, api.Namespace{
Name: namespace,
Labels: labels,
})
}
return nil
})
}
func (s *Service) Create(ctx context.Context, req *api.CreateNamespaceRequest) (*api.CreateNamespaceResponse, error) {
var resp api.CreateNamespaceResponse
return &resp, s.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
if err := store.Create(ctx, req.Namespace.Name, req.Namespace.Labels); err != nil {
return mapGRPCError(err, req.Namespace.Name)
}
for k, v := range req.Namespace.Labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, v); err != nil {
return err
}
}
resp.Namespace = req.Namespace
return nil
})
}
func (s *Service) Update(ctx context.Context, req *api.UpdateNamespaceRequest) (*api.UpdateNamespaceResponse, error) {
var resp api.UpdateNamespaceResponse
return &resp, s.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
for _, path := range req.UpdateMask.Paths {
switch {
case strings.HasPrefix(path, "labels."):
key := strings.TrimPrefix(path, "labels.")
if err := store.SetLabel(ctx, req.Namespace.Name, key, req.Namespace.Labels[key]); err != nil {
return err
}
default:
return grpc.Errorf(codes.InvalidArgument, "cannot update %q field", path)
}
}
} else {
// clear out the existing labels and then set them to the incoming request.
// get current set of labels
labels, err := store.Labels(ctx, req.Namespace.Name)
if err != nil {
return mapGRPCError(err, req.Namespace.Name)
}
for k := range labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, ""); err != nil {
return err
}
}
for k, v := range req.Namespace.Labels {
if err := store.SetLabel(ctx, req.Namespace.Name, k, v); err != nil {
return err
}
}
}
return nil
})
}
func (s *Service) Delete(ctx context.Context, req *api.DeleteNamespaceRequest) (*empty.Empty, error) {
return &empty.Empty{}, s.withStoreUpdate(ctx, func(ctx context.Context, store namespaces.Store) error {
return mapGRPCError(store.Delete(ctx, req.Name), req.Name)
})
}
func (s *Service) withStore(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) func(tx *bolt.Tx) error {
return func(tx *bolt.Tx) error { return fn(ctx, metadata.NewNamespaceStore(tx)) }
}
func (s *Service) withStoreView(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) error {
return s.db.View(s.withStore(ctx, fn))
}
func (s *Service) withStoreUpdate(ctx context.Context, fn func(ctx context.Context, store namespaces.Store) error) error {
return s.db.Update(s.withStore(ctx, fn))
}