From af2718b01f375ad2334ec88ebafa0137c9122de1 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 5 Jun 2017 17:45:50 -0700 Subject: [PATCH] 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 --- api/services/namespaces/namespace.pb.go | 105 +++++++------ api/services/namespaces/namespace.proto | 8 +- checkpoint_test.go | 11 +- client.go | 15 +- client_test.go | 30 +++- cmd/containerd/builtins.go | 1 + cmd/containerd/main.go | 8 +- cmd/ctr/checkpoint.go | 6 +- cmd/ctr/delete.go | 6 +- cmd/ctr/events.go | 6 +- cmd/ctr/exec.go | 7 +- cmd/ctr/info.go | 9 +- cmd/ctr/kill.go | 11 +- cmd/ctr/list.go | 8 +- cmd/ctr/main.go | 14 +- cmd/ctr/namespaces.go | 201 ++++++++++++++++++++++++ cmd/ctr/pause.go | 6 +- cmd/ctr/ps.go | 10 +- cmd/ctr/resume.go | 6 +- cmd/ctr/run.go | 7 +- cmd/ctr/snapshot.go | 6 +- cmd/ctr/utils.go | 35 +++++ cmd/dist/active.go | 2 +- cmd/dist/apply.go | 2 +- cmd/dist/common.go | 26 +++ cmd/dist/delete.go | 2 +- cmd/dist/edit.go | 2 +- cmd/dist/fetch.go | 2 +- cmd/dist/fetchobject.go | 2 +- cmd/dist/get.go | 2 +- cmd/dist/images.go | 4 +- cmd/dist/ingest.go | 2 +- cmd/dist/list.go | 2 +- cmd/dist/main.go | 16 +- cmd/dist/pull.go | 13 +- cmd/dist/push.go | 2 +- cmd/dist/pushobject.go | 2 +- cmd/dist/rootfs.go | 4 +- container_test.go | 54 ++++--- metadata/buckets.go | 90 ++++++----- metadata/containers.go | 7 +- metadata/errors.go | 5 + metadata/images.go | 70 ++++++--- metadata/namespaces.go | 145 +++++++++++++++++ namespaces/context.go | 42 +++++ namespaces/context_test.go | 30 ++++ namespaces/grpc.go | 44 ++++++ namespaces/store.go | 21 +++ services/images/helpers.go | 3 + services/namespaces/client.go | 95 +++++++++++ services/namespaces/helpers.go | 36 +++++ services/namespaces/service.go | 164 +++++++++++++++++++ 52 files changed, 1184 insertions(+), 223 deletions(-) create mode 100644 cmd/ctr/namespaces.go create mode 100644 metadata/namespaces.go create mode 100644 namespaces/context.go create mode 100644 namespaces/context_test.go create mode 100644 namespaces/grpc.go create mode 100644 namespaces/store.go create mode 100644 services/namespaces/client.go create mode 100644 services/namespaces/helpers.go create mode 100644 services/namespaces/service.go diff --git a/api/services/namespaces/namespace.pb.go b/api/services/namespaces/namespace.pb.go index 650495c9bfd9..28a9e3db48e4 100644 --- a/api/services/namespaces/namespace.pb.go +++ b/api/services/namespaces/namespace.pb.go @@ -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, } diff --git a/api/services/namespaces/namespace.proto b/api/services/namespaces/namespace.proto index 4c0eab34036f..4927cff8527e 100644 --- a/api/services/namespaces/namespace.proto +++ b/api/services/namespaces/namespace.proto @@ -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; } diff --git a/checkpoint_test.go b/checkpoint_test.go index 3315ab15279c..df1491208245 100644 --- a/checkpoint_test.go +++ b/checkpoint_test.go @@ -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) diff --git a/client.go b/client.go index 7985e8f0881f..7d3c4ccff3fe 100644 --- a/client.go +++ b/client.go @@ -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) } diff --git a/client_test.go b/client_test.go index 5bb0e5b0184b..7375563d6c6b 100644 --- a/client_test.go +++ b/client_test.go @@ -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 diff --git a/cmd/containerd/builtins.go b/cmd/containerd/builtins.go index 9386b1c1887c..a21d4a5dbfe5 100644 --- a/cmd/containerd/builtins.go +++ b/cmd/containerd/builtins.go @@ -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" ) diff --git a/cmd/containerd/main.go b/cmd/containerd/main.go index 2f82e3f6039d..a76f1070b083 100644 --- a/cmd/containerd/main.go +++ b/cmd/containerd/main.go @@ -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) } diff --git a/cmd/ctr/checkpoint.go b/cmd/ctr/checkpoint.go index fc35cb335806..29590069a77b 100644 --- a/cmd/ctr/checkpoint.go +++ b/cmd/ctr/checkpoint.go @@ -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") } diff --git a/cmd/ctr/delete.go b/cmd/ctr/delete.go index a373c059c772..920695dfa408 100644 --- a/cmd/ctr/delete.go +++ b/cmd/ctr/delete.go @@ -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, }) diff --git a/cmd/ctr/events.go b/cmd/ctr/events.go index 6bd31bd1719a..001db8675602 100644 --- a/cmd/ctr/events.go +++ b/cmd/ctr/events.go @@ -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 } diff --git a/cmd/ctr/exec.go b/cmd/ctr/exec.go index 57a8a7c824b6..2df95db4c1be 100644 --- a/cmd/ctr/exec.go +++ b/cmd/ctr/exec.go @@ -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") } diff --git a/cmd/ctr/info.go b/cmd/ctr/info.go index 2cfba1151ec7..3dec3bccb392 100644 --- a/cmd/ctr/info.go +++ b/cmd/ctr/info.go @@ -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 } diff --git a/cmd/ctr/kill.go b/cmd/ctr/kill.go index afa12144e4a8..b2b9fb2a106e 100644 --- a/cmd/ctr/kill.go +++ b/cmd/ctr/kill.go @@ -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 } diff --git a/cmd/ctr/list.go b/cmd/ctr/list.go index 16e52c5dfad5..b8a5ab756829 100644 --- a/cmd/ctr/list.go +++ b/cmd/ctr/list.go @@ -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 } diff --git a/cmd/ctr/main.go b/cmd/ctr/main.go index 84bc711b0ab7..acd527fd0606 100644 --- a/cmd/ctr/main.go +++ b/cmd/ctr/main.go @@ -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, diff --git a/cmd/ctr/namespaces.go b/cmd/ctr/namespaces.go new file mode 100644 index 000000000000..283bfe464d97 --- /dev/null +++ b/cmd/ctr/namespaces.go @@ -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] [= 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] [=, ...]", + 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] [, ...]", + 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 + + }, +} diff --git a/cmd/ctr/pause.go b/cmd/ctr/pause.go index 42214239656c..e68e5bdad972 100644 --- a/cmd/ctr/pause.go +++ b/cmd/ctr/pause.go @@ -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 diff --git a/cmd/ctr/ps.go b/cmd/ctr/ps.go index c540d1e96a77..b6ff9f88cda1 100644 --- a/cmd/ctr/ps.go +++ b/cmd/ctr/ps.go @@ -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 } diff --git a/cmd/ctr/resume.go b/cmd/ctr/resume.go index e22d46d06c50..f0cc6210a09f 100644 --- a/cmd/ctr/resume.go +++ b/cmd/ctr/resume.go @@ -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 diff --git a/cmd/ctr/run.go b/cmd/ctr/run.go index eae380606891..e655ee760b98 100644 --- a/cmd/ctr/run.go +++ b/cmd/ctr/run.go @@ -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") } diff --git a/cmd/ctr/snapshot.go b/cmd/ctr/snapshot.go index 631f7d914cc9..825741fc0dc3 100644 --- a/cmd/ctr/snapshot.go +++ b/cmd/ctr/snapshot.go @@ -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 } diff --git a/cmd/ctr/utils.go b/cmd/ctr/utils.go index 3dfabeeffe90..01c429433cce 100644 --- a/cmd/ctr/utils.go +++ b/cmd/ctr/utils.go @@ -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 { diff --git a/cmd/dist/active.go b/cmd/dist/active.go index d339f6e18162..699c63af1146 100644 --- a/cmd/dist/active.go +++ b/cmd/dist/active.go @@ -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) diff --git a/cmd/dist/apply.go b/cmd/dist/apply.go index fe31dfa7e6b4..bbd702bf5f7b 100644 --- a/cmd/dist/apply.go +++ b/cmd/dist/apply.go @@ -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") diff --git a/cmd/dist/common.go b/cmd/dist/common.go index efd7df07653d..501a37ab9783 100644 --- a/cmd/dist/common.go +++ b/cmd/dist/common.go @@ -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 { diff --git a/cmd/dist/delete.go b/cmd/dist/delete.go index 9e586835b55a..d8cb76062d97 100644 --- a/cmd/dist/delete.go +++ b/cmd/dist/delete.go @@ -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) diff --git a/cmd/dist/edit.go b/cmd/dist/edit.go index 0a0ad039f8b2..365aa2d41878 100644 --- a/cmd/dist/edit.go +++ b/cmd/dist/edit.go @@ -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 != "" { diff --git a/cmd/dist/fetch.go b/cmd/dist/fetch.go index c2de902c9f1e..507f760e10ee 100644 --- a/cmd/dist/fetch.go +++ b/cmd/dist/fetch.go @@ -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) diff --git a/cmd/dist/fetchobject.go b/cmd/dist/fetchobject.go index 2f8a43aabeee..cf4cf8303af3 100644 --- a/cmd/dist/fetchobject.go +++ b/cmd/dist/fetchobject.go @@ -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) diff --git a/cmd/dist/get.go b/cmd/dist/get.go index b36b0e3cb878..909805cd782a 100644 --- a/cmd/dist/get.go +++ b/cmd/dist/get.go @@ -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()) diff --git a/cmd/dist/images.go b/cmd/dist/images.go index 53fbb3dce9e1..9018eb508095 100644 --- a/cmd/dist/images.go +++ b/cmd/dist/images.go @@ -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) diff --git a/cmd/dist/ingest.go b/cmd/dist/ingest.go index 03d16e0bad4a..5adffe326ef4 100644 --- a/cmd/dist/ingest.go +++ b/cmd/dist/ingest.go @@ -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 { diff --git a/cmd/dist/list.go b/cmd/dist/list.go index 9a73b012391c..60c784943a0e 100644 --- a/cmd/dist/list.go +++ b/cmd/dist/list.go @@ -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) diff --git a/cmd/dist/main.go b/cmd/dist/main.go index db5b2e767805..d5da0ee19df1 100644 --- a/cmd/dist/main.go +++ b/cmd/dist/main.go @@ -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) } diff --git a/cmd/dist/pull.go b/cmd/dist/pull.go index 74bf1a2d0388..948b873f4e12 100644 --- a/cmd/dist/pull.go +++ b/cmd/dist/pull.go @@ -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) diff --git a/cmd/dist/push.go b/cmd/dist/push.go index 6ce7afbe6f30..439b33521bb1 100644 --- a/cmd/dist/push.go +++ b/cmd/dist/push.go @@ -48,7 +48,7 @@ var pushCommand = cli.Command{ desc ocispec.Descriptor ) - ctx, cancel := appContext() + ctx, cancel := appContext(clicontext) defer cancel() client, err := getClient(clicontext) diff --git a/cmd/dist/pushobject.go b/cmd/dist/pushobject.go index f69778da9436..196036a8883e 100644 --- a/cmd/dist/pushobject.go +++ b/cmd/dist/pushobject.go @@ -26,7 +26,7 @@ var pushObjectCommand = cli.Command{ return err } - ctx, cancel := appContext() + ctx, cancel := appContext(clicontext) defer cancel() resolver, err := getResolver(ctx, clicontext) diff --git a/cmd/dist/rootfs.go b/cmd/dist/rootfs.go index d8711959f199..ea496f25d591 100644 --- a/cmd/dist/rootfs.go +++ b/cmd/dist/rootfs.go @@ -32,7 +32,7 @@ var rootfsUnpackCommand = cli.Command{ ArgsUsage: "[flags] ", 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] ", Flags: []cli.Flag{}, Action: func(clicontext *cli.Context) error { - ctx, cancel := appContext() + ctx, cancel := appContext(clicontext) defer cancel() if clicontext.NArg() != 2 { diff --git a/container_test.go b/container_test.go index 7f12e2ec2883..9245c1203d17 100644 --- a/container_test.go +++ b/container_test.go @@ -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) diff --git a/metadata/buckets.go b/metadata/buckets.go index 2da561cd4238..7eca8a2b82e1 100644 --- a/metadata/buckets.go +++ b/metadata/buckets.go @@ -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: 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)...) +} - return fn(bkt) +func imagesBucketPath(namespace string) [][]byte { + return [][]byte{bucketKeyVersion, []byte(namespace), bucketKeyObjectImages} } -func getImagesBucket(tx *bolt.Tx) *bolt.Bucket { - return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages) +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 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)) } diff --git a/metadata/containers.go b/metadata/containers.go index d1fe67f9d261..f1faaa0f342c 100644 --- a/metadata/containers.go +++ b/metadata/containers.go @@ -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 } diff --git a/metadata/errors.go b/metadata/errors.go index ab370a5ddce2..4607ec48e25d 100644 --- a/metadata/errors.go +++ b/metadata/errors.go @@ -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 +} diff --git a/metadata/images.go b/metadata/images.go index a922f7b666fe..43c703019881 100644 --- a/metadata/images.go +++ b/metadata/images.go @@ -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) + ) + + if err := readImage(&image, kbkt); err != nil { + return err + } - m = append(m, image) - return nil - }) + 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 diff --git a/metadata/namespaces.go b/metadata/namespaces.go new file mode 100644 index 000000000000..ffe898f67fec --- /dev/null +++ b/metadata/namespaces.go @@ -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 +} diff --git a/namespaces/context.go b/namespaces/context.go new file mode 100644 index 000000000000..fe5049e98372 --- /dev/null +++ b/namespaces/context.go @@ -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 +} diff --git a/namespaces/context_test.go b/namespaces/context_test.go new file mode 100644 index 000000000000..20e2b0927639 --- /dev/null +++ b/namespaces/context_test.go @@ -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) + } +} diff --git a/namespaces/grpc.go b/namespaces/grpc.go new file mode 100644 index 000000000000..c18fc933d133 --- /dev/null +++ b/namespaces/grpc.go @@ -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 +} diff --git a/namespaces/store.go b/namespaces/store.go new file mode 100644 index 000000000000..68ff714bb7fb --- /dev/null +++ b/namespaces/store.go @@ -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 +} diff --git a/services/images/helpers.go b/services/images/helpers.go index 52be324d9ac2..19ce9e18af39 100644 --- a/services/images/helpers.go +++ b/services/images/helpers.go @@ -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 diff --git a/services/namespaces/client.go b/services/namespaces/client.go new file mode 100644 index 000000000000..e92314dccde6 --- /dev/null +++ b/services/namespaces/client.go @@ -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 +} diff --git a/services/namespaces/helpers.go b/services/namespaces/helpers.go new file mode 100644 index 000000000000..8cb8f55ad8ce --- /dev/null +++ b/services/namespaces/helpers.go @@ -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 +} diff --git a/services/namespaces/service.go b/services/namespaces/service.go new file mode 100644 index 000000000000..4a5388e4d62b --- /dev/null +++ b/services/namespaces/service.go @@ -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)) +}