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:
parent
25cc7614ae
commit
af2718b01f
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
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)
|
||||
|
13
client.go
13
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) {
|
||||
@ -80,7 +74,6 @@ type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
|
||||
runtime string
|
||||
namespace 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)
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,6 +31,12 @@ 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())
|
||||
@ -36,7 +44,10 @@ func TestMain(m *testing.M) {
|
||||
var (
|
||||
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
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -41,8 +41,10 @@ var checkpointCommand = cli.Command{
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
id = context.String("id")
|
||||
ctx = gocontext.Background()
|
||||
ctx, cancel = appContext(context)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
if id == "" {
|
||||
return errors.New("container id must be provided")
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
@ -31,8 +30,10 @@ var execCommand = cli.Command{
|
||||
Action: func(context *cli.Context) error {
|
||||
var (
|
||||
id = context.String("id")
|
||||
ctx = gocontext.Background()
|
||||
ctx, cancel = appContext(context)
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
if id == "" {
|
||||
return errors.New("container id must be provided")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
201
cmd/ctr/namespaces.go
Normal 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
|
||||
|
||||
},
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
ctx, cancel = appContext(context)
|
||||
id = context.String("id")
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
if id == "" {
|
||||
return errors.New("container id must be provided")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
2
cmd/dist/active.go
vendored
@ -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
2
cmd/dist/apply.go
vendored
@ -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
26
cmd/dist/common.go
vendored
@ -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
2
cmd/dist/delete.go
vendored
@ -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
2
cmd/dist/edit.go
vendored
@ -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
2
cmd/dist/fetch.go
vendored
@ -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)
|
||||
|
2
cmd/dist/fetchobject.go
vendored
2
cmd/dist/fetchobject.go
vendored
@ -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
2
cmd/dist/get.go
vendored
@ -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
4
cmd/dist/images.go
vendored
@ -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
2
cmd/dist/ingest.go
vendored
@ -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
2
cmd/dist/list.go
vendored
@ -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
16
cmd/dist/main.go
vendored
@ -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
13
cmd/dist/pull.go
vendored
@ -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
2
cmd/dist/push.go
vendored
@ -48,7 +48,7 @@ var pushCommand = cli.Command{
|
||||
desc ocispec.Descriptor
|
||||
)
|
||||
|
||||
ctx, cancel := appContext()
|
||||
ctx, cancel := appContext(clicontext)
|
||||
defer cancel()
|
||||
|
||||
client, err := getClient(clicontext)
|
||||
|
2
cmd/dist/pushobject.go
vendored
2
cmd/dist/pushobject.go
vendored
@ -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
4
cmd/dist/rootfs.go
vendored
@ -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 {
|
||||
|
@ -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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
ctx, cancel = testContext()
|
||||
id = "ContainerAttach"
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
image, err := client.GetImage(ctx, testImage)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
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
|
||||
return readImage(&image, bkt)
|
||||
}); err != nil {
|
||||
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,9 +84,17 @@ 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 {
|
||||
bkt := getImagesBucket(s.tx, namespace)
|
||||
if bkt == nil {
|
||||
return nil, nil // empty store
|
||||
}
|
||||
|
||||
if err := bkt.ForEach(func(k, v []byte) error {
|
||||
var (
|
||||
image = images.Image{
|
||||
Name: string(k),
|
||||
@ -80,7 +108,6 @@ func (s *imageStore) List(ctx context.Context) ([]images.Image, error) {
|
||||
|
||||
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
145
metadata/namespaces.go
Normal 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
42
namespaces/context.go
Normal 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
|
||||
}
|
30
namespaces/context_test.go
Normal file
30
namespaces/context_test.go
Normal 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
44
namespaces/grpc.go
Normal 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
21
namespaces/store.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
95
services/namespaces/client.go
Normal file
95
services/namespaces/client.go
Normal 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
|
||||
}
|
36
services/namespaces/helpers.go
Normal file
36
services/namespaces/helpers.go
Normal 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
|
||||
}
|
164
services/namespaces/service.go
Normal file
164
services/namespaces/service.go
Normal 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))
|
||||
}
|
Loading…
Reference in New Issue
Block a user