feat(labels): support service and container labels, update labels and cpu/memory in-place

This commit is contained in:
Pasha Sviderski
2026-11-02 14:57:33 +10:00
parent b214d23f21
commit 81f30aac26
30 changed files with 1727 additions and 286 deletions
+381 -190
View File
@@ -2259,6 +2259,137 @@ func (x *ServiceContainer) GetServiceSpec() []byte {
return nil
}
type UpdateServiceContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Fields that are not set are left unchanged. CPU and memory limits can be changed but not removed.
// CPU limit in nanocores.
Cpu *int64 `protobuf:"varint,2,opt,name=cpu,proto3,oneof" json:"cpu,omitempty"`
// Memory limit in bytes.
Memory *int64 `protobuf:"varint,3,opt,name=memory,proto3,oneof" json:"memory,omitempty"`
// Memory reservation in bytes.
MemoryReservation *int64 `protobuf:"varint,4,opt,name=memory_reservation,json=memoryReservation,proto3,oneof" json:"memory_reservation,omitempty"`
// Service labels that replace all current service labels.
ServiceLabels *Labels `protobuf:"bytes,5,opt,name=service_labels,json=serviceLabels,proto3" json:"service_labels,omitempty"`
}
func (x *UpdateServiceContainerRequest) Reset() {
*x = UpdateServiceContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_docker_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *UpdateServiceContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateServiceContainerRequest) ProtoMessage() {}
func (x *UpdateServiceContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_docker_proto_msgTypes[38]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateServiceContainerRequest.ProtoReflect.Descriptor instead.
func (*UpdateServiceContainerRequest) Descriptor() ([]byte, []int) {
return file_api_pb_docker_proto_rawDescGZIP(), []int{38}
}
func (x *UpdateServiceContainerRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *UpdateServiceContainerRequest) GetCpu() int64 {
if x != nil && x.Cpu != nil {
return *x.Cpu
}
return 0
}
func (x *UpdateServiceContainerRequest) GetMemory() int64 {
if x != nil && x.Memory != nil {
return *x.Memory
}
return 0
}
func (x *UpdateServiceContainerRequest) GetMemoryReservation() int64 {
if x != nil && x.MemoryReservation != nil {
return *x.MemoryReservation
}
return 0
}
func (x *UpdateServiceContainerRequest) GetServiceLabels() *Labels {
if x != nil {
return x.ServiceLabels
}
return nil
}
type Labels struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Labels map[string]string `protobuf:"bytes,1,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *Labels) Reset() {
*x = Labels{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_docker_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Labels) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Labels) ProtoMessage() {}
func (x *Labels) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_docker_proto_msgTypes[39]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Labels.ProtoReflect.Descriptor instead.
func (*Labels) Descriptor() ([]byte, []int) {
return file_api_pb_docker_proto_rawDescGZIP(), []int{39}
}
func (x *Labels) GetLabels() map[string]string {
if x != nil {
return x.Labels
}
return nil
}
type ListServiceContainersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2272,7 +2403,7 @@ type ListServiceContainersRequest struct {
func (x *ListServiceContainersRequest) Reset() {
*x = ListServiceContainersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_docker_proto_msgTypes[38]
mi := &file_api_pb_docker_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2285,7 +2416,7 @@ func (x *ListServiceContainersRequest) String() string {
func (*ListServiceContainersRequest) ProtoMessage() {}
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_docker_proto_msgTypes[38]
mi := &file_api_pb_docker_proto_msgTypes[40]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2298,7 +2429,7 @@ func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) {
return file_api_pb_docker_proto_rawDescGZIP(), []int{38}
return file_api_pb_docker_proto_rawDescGZIP(), []int{40}
}
func (x *ListServiceContainersRequest) GetServiceId() string {
@@ -2328,7 +2459,7 @@ type ListServiceContainersResponse struct {
func (x *ListServiceContainersResponse) Reset() {
*x = ListServiceContainersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_docker_proto_msgTypes[39]
mi := &file_api_pb_docker_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2341,7 +2472,7 @@ func (x *ListServiceContainersResponse) String() string {
func (*ListServiceContainersResponse) ProtoMessage() {}
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_docker_proto_msgTypes[39]
mi := &file_api_pb_docker_proto_msgTypes[41]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2354,7 +2485,7 @@ func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) {
return file_api_pb_docker_proto_rawDescGZIP(), []int{39}
return file_api_pb_docker_proto_rawDescGZIP(), []int{41}
}
func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
@@ -2378,7 +2509,7 @@ type MachineServiceContainers struct {
func (x *MachineServiceContainers) Reset() {
*x = MachineServiceContainers{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_docker_proto_msgTypes[40]
mi := &file_api_pb_docker_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2391,7 +2522,7 @@ func (x *MachineServiceContainers) String() string {
func (*MachineServiceContainers) ProtoMessage() {}
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_docker_proto_msgTypes[40]
mi := &file_api_pb_docker_proto_msgTypes[42]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2404,7 +2535,7 @@ func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
func (*MachineServiceContainers) Descriptor() ([]byte, []int) {
return file_api_pb_docker_proto_rawDescGZIP(), []int{40}
return file_api_pb_docker_proto_rawDescGZIP(), []int{42}
}
func (x *MachineServiceContainers) GetMetadata() *Metadata {
@@ -2638,125 +2769,153 @@ var file_api_pb_docker_proto_rawDesc = []byte{
0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65,
0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0xf5, 0x01, 0x0a, 0x1d, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x03, 0x63, 0x70, 0x75, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x03, 0x63, 0x70, 0x75, 0x88, 0x01, 0x01, 0x12,
0x1b, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48,
0x01, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x32, 0x0a, 0x12,
0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x11, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01,
0x12, 0x32, 0x0a, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6c, 0x61, 0x62, 0x65,
0x6c, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x61, 0x62, 0x65, 0x6c, 0x73, 0x52, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4c, 0x61,
0x62, 0x65, 0x6c, 0x73, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x63, 0x70, 0x75, 0x42, 0x09, 0x0a, 0x07,
0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x6d, 0x65, 0x6d, 0x6f,
0x72, 0x79, 0x5f, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x74,
0x0a, 0x06, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2f, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65,
0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x61, 0x62, 0x65, 0x6c, 0x73, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72,
0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62,
0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x3a, 0x02, 0x38, 0x01, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x5a, 0x0a,
0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39,
0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22,
0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x18,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x68, 0x6f,
0x6f, 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x68, 0x6f, 0x6f, 0x6b,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xbd, 0x0b, 0x0a, 0x06, 0x44,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43,
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61,
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74,
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49,
0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d,
0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
0x79, 0x12, 0x4a, 0x0a, 0x0d, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x32, 0x0a,
0x0d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30,
0x01, 0x12, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55,
0x0a, 0x12, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61,
0x67, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d,
0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6d,
0x61, 0x67, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65,
0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c,
0x75, 0x6d, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x4c,
0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f,
0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a,
0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65,
0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x18, 0x4d, 0x61,
0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x68, 0x6f, 0x6f, 0x6b,
0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x68, 0x6f, 0x6f, 0x6b, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0x93, 0x0c, 0x0a, 0x06, 0x44, 0x6f, 0x63,
0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e,
0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c,
0x4a, 0x0a, 0x0d, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x32, 0x0a, 0x0d, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x12,
0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x53, 0x4f, 0x4e, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x30, 0x01, 0x12, 0x43, 0x0a, 0x0c, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x49,
0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x12,
0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61,
0x67, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74,
0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65,
0x73, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67,
0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6d, 0x61, 0x67,
0x65, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6d,
0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f,
0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d,
0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x4c, 0x69, 0x73,
0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x6f, 0x6c, 0x75,
0x6d, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x52,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5a, 0x0a,
0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72,
0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73,
0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65,
0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x54, 0x0a, 0x16, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12,
0x5e, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72,
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x4d, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x26,
0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76,
0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2772,7 +2931,7 @@ func file_api_pb_docker_proto_rawDescGZIP() []byte {
}
var file_api_pb_docker_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 41)
var file_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 44)
var file_api_pb_docker_proto_goTypes = []any{
(CreateServiceContainerRequest_ContainerType)(0), // 0: api.CreateServiceContainerRequest.ContainerType
(*CreateContainerRequest)(nil), // 1: api.CreateContainerRequest
@@ -2813,81 +2972,88 @@ var file_api_pb_docker_proto_goTypes = []any{
(*RemoveVolumeRequest)(nil), // 36: api.RemoveVolumeRequest
(*CreateServiceContainerRequest)(nil), // 37: api.CreateServiceContainerRequest
(*ServiceContainer)(nil), // 38: api.ServiceContainer
(*ListServiceContainersRequest)(nil), // 39: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 40: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 41: api.MachineServiceContainers
(*Metadata)(nil), // 42: api.Metadata
(*LogsRequest)(nil), // 43: api.LogsRequest
(*emptypb.Empty)(nil), // 44: google.protobuf.Empty
(*LogEntry)(nil), // 45: api.LogEntry
(*UpdateServiceContainerRequest)(nil), // 39: api.UpdateServiceContainerRequest
(*Labels)(nil), // 40: api.Labels
(*ListServiceContainersRequest)(nil), // 41: api.ListServiceContainersRequest
(*ListServiceContainersResponse)(nil), // 42: api.ListServiceContainersResponse
(*MachineServiceContainers)(nil), // 43: api.MachineServiceContainers
nil, // 44: api.Labels.LabelsEntry
(*Metadata)(nil), // 45: api.Metadata
(*LogsRequest)(nil), // 46: api.LogsRequest
(*emptypb.Empty)(nil), // 47: google.protobuf.Empty
(*LogEntry)(nil), // 48: api.LogEntry
}
var file_api_pb_docker_proto_depIdxs = []int32{
9, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers
42, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
45, // 1: api.MachineContainers.metadata:type_name -> api.Metadata
12, // 2: api.ExecContainerRequest.config:type_name -> api.ExecConfig
13, // 3: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent
19, // 4: api.InspectImageResponse.messages:type_name -> api.Image
42, // 5: api.Image.metadata:type_name -> api.Metadata
45, // 5: api.Image.metadata:type_name -> api.Metadata
22, // 6: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage
42, // 7: api.RemoteImage.metadata:type_name -> api.Metadata
45, // 7: api.RemoteImage.metadata:type_name -> api.Metadata
25, // 8: api.RemoveImageRequest.platforms:type_name -> api.ImagePlatform
27, // 9: api.RemoveImageResponse.messages:type_name -> api.MachineImageRemoval
42, // 10: api.MachineImageRemoval.metadata:type_name -> api.Metadata
45, // 10: api.MachineImageRemoval.metadata:type_name -> api.Metadata
28, // 11: api.MachineImageRemoval.results:type_name -> api.ImageRemoval
30, // 12: api.ListImagesResponse.messages:type_name -> api.MachineImages
42, // 13: api.MachineImages.metadata:type_name -> api.Metadata
45, // 13: api.MachineImages.metadata:type_name -> api.Metadata
35, // 14: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes
42, // 15: api.MachineVolumes.metadata:type_name -> api.Metadata
45, // 15: api.MachineVolumes.metadata:type_name -> api.Metadata
0, // 16: api.CreateServiceContainerRequest.container_type:type_name -> api.CreateServiceContainerRequest.ContainerType
41, // 17: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
42, // 18: api.MachineServiceContainers.metadata:type_name -> api.Metadata
38, // 19: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
38, // 20: api.MachineServiceContainers.hook_containers:type_name -> api.ServiceContainer
1, // 21: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
3, // 22: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
5, // 23: api.Docker.StartContainer:input_type -> api.StartContainerRequest
6, // 24: api.Docker.StopContainer:input_type -> api.StopContainerRequest
7, // 25: api.Docker.ListContainers:input_type -> api.ListContainersRequest
10, // 26: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
11, // 27: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
43, // 28: api.Docker.ContainerLogs:input_type -> api.LogsRequest
15, // 29: api.Docker.PullImage:input_type -> api.PullImageRequest
17, // 30: api.Docker.InspectImage:input_type -> api.InspectImageRequest
20, // 31: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
23, // 32: api.Docker.ListImages:input_type -> api.ListImagesRequest
24, // 33: api.Docker.RemoveImage:input_type -> api.RemoveImageRequest
31, // 34: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
33, // 35: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
36, // 36: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
37, // 37: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
3, // 38: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
39, // 39: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
10, // 40: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
2, // 41: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
4, // 42: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
44, // 43: api.Docker.StartContainer:output_type -> google.protobuf.Empty
44, // 44: api.Docker.StopContainer:output_type -> google.protobuf.Empty
8, // 45: api.Docker.ListContainers:output_type -> api.ListContainersResponse
44, // 46: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
14, // 47: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
45, // 48: api.Docker.ContainerLogs:output_type -> api.LogEntry
16, // 49: api.Docker.PullImage:output_type -> api.JSONMessage
18, // 50: api.Docker.InspectImage:output_type -> api.InspectImageResponse
21, // 51: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
29, // 52: api.Docker.ListImages:output_type -> api.ListImagesResponse
26, // 53: api.Docker.RemoveImage:output_type -> api.RemoveImageResponse
32, // 54: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
34, // 55: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
44, // 56: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
2, // 57: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
38, // 58: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
40, // 59: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
44, // 60: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
41, // [41:61] is the sub-list for method output_type
21, // [21:41] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
40, // 17: api.UpdateServiceContainerRequest.service_labels:type_name -> api.Labels
44, // 18: api.Labels.labels:type_name -> api.Labels.LabelsEntry
43, // 19: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
45, // 20: api.MachineServiceContainers.metadata:type_name -> api.Metadata
38, // 21: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
38, // 22: api.MachineServiceContainers.hook_containers:type_name -> api.ServiceContainer
1, // 23: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
3, // 24: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
5, // 25: api.Docker.StartContainer:input_type -> api.StartContainerRequest
6, // 26: api.Docker.StopContainer:input_type -> api.StopContainerRequest
7, // 27: api.Docker.ListContainers:input_type -> api.ListContainersRequest
10, // 28: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
11, // 29: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
46, // 30: api.Docker.ContainerLogs:input_type -> api.LogsRequest
15, // 31: api.Docker.PullImage:input_type -> api.PullImageRequest
17, // 32: api.Docker.InspectImage:input_type -> api.InspectImageRequest
20, // 33: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
23, // 34: api.Docker.ListImages:input_type -> api.ListImagesRequest
24, // 35: api.Docker.RemoveImage:input_type -> api.RemoveImageRequest
31, // 36: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
33, // 37: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
36, // 38: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
37, // 39: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
3, // 40: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
39, // 41: api.Docker.UpdateServiceContainer:input_type -> api.UpdateServiceContainerRequest
41, // 42: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
10, // 43: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
2, // 44: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
4, // 45: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
47, // 46: api.Docker.StartContainer:output_type -> google.protobuf.Empty
47, // 47: api.Docker.StopContainer:output_type -> google.protobuf.Empty
8, // 48: api.Docker.ListContainers:output_type -> api.ListContainersResponse
47, // 49: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
14, // 50: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
48, // 51: api.Docker.ContainerLogs:output_type -> api.LogEntry
16, // 52: api.Docker.PullImage:output_type -> api.JSONMessage
18, // 53: api.Docker.InspectImage:output_type -> api.InspectImageResponse
21, // 54: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
29, // 55: api.Docker.ListImages:output_type -> api.ListImagesResponse
26, // 56: api.Docker.RemoveImage:output_type -> api.RemoveImageResponse
32, // 57: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
34, // 58: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
47, // 59: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
2, // 60: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
38, // 61: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
47, // 62: api.Docker.UpdateServiceContainer:output_type -> google.protobuf.Empty
42, // 63: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
47, // 64: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
44, // [44:65] is the sub-list for method output_type
23, // [23:44] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
}
func init() { file_api_pb_docker_proto_init() }
@@ -3354,7 +3520,7 @@ func file_api_pb_docker_proto_init() {
}
}
file_api_pb_docker_proto_msgTypes[38].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersRequest); i {
switch v := v.(*UpdateServiceContainerRequest); i {
case 0:
return &v.state
case 1:
@@ -3366,7 +3532,7 @@ func file_api_pb_docker_proto_init() {
}
}
file_api_pb_docker_proto_msgTypes[39].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersResponse); i {
switch v := v.(*Labels); i {
case 0:
return &v.state
case 1:
@@ -3378,6 +3544,30 @@ func file_api_pb_docker_proto_init() {
}
}
file_api_pb_docker_proto_msgTypes[40].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_docker_proto_msgTypes[41].Exporter = func(v any, i int) any {
switch v := v.(*ListServiceContainersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_docker_proto_msgTypes[42].Exporter = func(v any, i int) any {
switch v := v.(*MachineServiceContainers); i {
case 0:
return &v.state
@@ -3401,13 +3591,14 @@ func file_api_pb_docker_proto_init() {
(*ExecContainerResponse_Stderr)(nil),
(*ExecContainerResponse_ExitCode)(nil),
}
file_api_pb_docker_proto_msgTypes[38].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_pb_docker_proto_rawDesc,
NumEnums: 1,
NumMessages: 41,
NumMessages: 44,
NumExtensions: 0,
NumServices: 1,
},
+19
View File
@@ -32,6 +32,8 @@ service Docker {
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer);
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
rpc UpdateServiceContainer(UpdateServiceContainerRequest) returns (google.protobuf.Empty);
rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
}
@@ -281,6 +283,23 @@ message ServiceContainer {
bytes service_spec = 2;
}
message UpdateServiceContainerRequest {
string id = 1;
// Fields that are not set are left unchanged. CPU and memory limits can be changed but not removed.
// CPU limit in nanocores.
optional int64 cpu = 2;
// Memory limit in bytes.
optional int64 memory = 3;
// Memory reservation in bytes.
optional int64 memory_reservation = 4;
// Service labels that replace all current service labels.
Labels service_labels = 5;
}
message Labels {
map<string, string> labels = 1;
}
message ListServiceContainersRequest {
string service_id = 1;
// JSON serialised container.ListOptions.
+40
View File
@@ -38,6 +38,7 @@ const (
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_UpdateServiceContainer_FullMethodName = "/api.Docker/UpdateServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
)
@@ -66,6 +67,8 @@ type DockerClient interface {
RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error)
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
UpdateServiceContainer(ctx context.Context, in *UpdateServiceContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
@@ -279,6 +282,16 @@ func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectC
return out, nil
}
func (c *dockerClient) UpdateServiceContainer(ctx context.Context, in *UpdateServiceContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Docker_UpdateServiceContainer_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListServiceContainersResponse)
@@ -323,6 +336,8 @@ type DockerServer interface {
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error)
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
UpdateServiceContainer(context.Context, *UpdateServiceContainerRequest) (*emptypb.Empty, error)
ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDockerServer()
@@ -389,6 +404,9 @@ func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *Create
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented")
}
func (UnimplementedDockerServer) UpdateServiceContainer(context.Context, *UpdateServiceContainerRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateServiceContainer not implemented")
}
func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented")
}
@@ -715,6 +733,24 @@ func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _Docker_UpdateServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateServiceContainerRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DockerServer).UpdateServiceContainer(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Docker_UpdateServiceContainer_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DockerServer).UpdateServiceContainer(ctx, req.(*UpdateServiceContainerRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListServiceContainersRequest)
if err := dec(in); err != nil {
@@ -818,6 +854,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectServiceContainer",
Handler: _Docker_InspectServiceContainer_Handler,
},
{
MethodName: "UpdateServiceContainer",
Handler: _Docker_UpdateServiceContainer_Handler,
},
{
MethodName: "ListServiceContainers",
Handler: _Docker_ListServiceContainers_Handler,
+5
View File
@@ -129,6 +129,11 @@ func (c *Controller) WatchAndSyncContainers(ctx context.Context) error {
"container_name", e.Actor.Attributes["name"],
"action", e.Action)
if err := c.syncContainersToStore(ctx); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
case <-c.service.ContainersChanged():
slog.Debug("Syncing containers to cluster store triggered by a container change in machine database.")
if err := c.syncContainersToStore(ctx); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err)
}
+48 -15
View File
@@ -594,21 +594,26 @@ func (s *Server) CreateServiceContainer(
}
}
// Apply user-defined labels first so that the system labels always take precedence.
labels := maps.Clone(spec.Container.Labels)
if labels == nil {
labels = make(map[string]string)
}
labels[api.LabelServiceID] = req.ServiceId
labels[api.LabelServiceName] = spec.Name
labels[api.LabelServiceMode] = spec.Mode
labels[api.LabelManaged] = ""
config := &container.Config{
Cmd: spec.Container.Command,
Env: envVars.ToSlice(),
Entrypoint: spec.Container.Entrypoint,
Hostname: containerName,
Image: spec.Container.Image,
Labels: map[string]string{
api.LabelServiceID: req.ServiceId,
api.LabelServiceName: spec.Name,
api.LabelServiceMode: spec.Mode,
api.LabelManaged: "",
},
User: spec.Container.User,
Tty: spec.Container.Tty,
OpenStdin: spec.Container.OpenStdin,
Labels: labels,
User: spec.Container.User,
Tty: spec.Container.Tty,
OpenStdin: spec.Container.OpenStdin,
}
if spec.Mode == "" {
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
@@ -729,12 +734,10 @@ func (s *Server) CreateServiceContainer(
// Strip the container configuration that doesn't make sense for the pre-deploy hook and apply its overrides.
if spec.PreDeploy != nil && req.ContainerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
config.Labels = map[string]string{
api.LabelServiceID: req.ServiceId,
api.LabelServiceName: spec.Name,
api.LabelHook: api.LabelHookPreDeploy,
api.LabelManaged: "",
}
// Keep the user-defined labels but replace the service mode and ports labels with the hook label.
delete(config.Labels, api.LabelServiceMode)
delete(config.Labels, api.LabelServicePorts)
config.Labels[api.LabelHook] = api.LabelHookPreDeploy
config.Healthcheck = &container.HealthConfig{
Test: []string{"NONE"},
}
@@ -1179,6 +1182,36 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
return resp, nil
}
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
func (s *Server) UpdateServiceContainer(
ctx context.Context, req *pb.UpdateServiceContainerRequest,
) (*emptypb.Empty, error) {
opts := api.UpdateContainerOptions{
CPU: req.Cpu,
Memory: req.Memory,
MemoryReservation: req.MemoryReservation,
}
if req.ServiceLabels != nil {
labels := req.ServiceLabels.Labels
opts.ServiceLabels = &labels
}
if err := s.service.UpdateServiceContainer(ctx, req.Id, opts); err != nil {
switch {
case errdefs.IsNotFound(err):
return nil, status.Error(codes.NotFound, err.Error())
case errdefs.IsInvalidArgument(err):
return nil, status.Error(codes.InvalidArgument, err.Error())
case errdefs.IsFailedPrecondition(err):
return nil, status.Error(codes.FailedPrecondition, err.Error())
default:
return nil, status.Error(codes.Internal, err.Error())
}
}
return &emptypb.Empty{}, nil
}
// logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream.
const logsHeartbeatInterval = 200 * time.Millisecond
+105
View File
@@ -15,6 +15,7 @@ import (
"time"
"github.com/cenkalti/backoff/v4"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
@@ -34,6 +35,9 @@ type Service struct {
Client client.APIClient
// db is a connection to the machine database.
db *sqlx.DB
// containersChanged signals that service containers changed in the machine database without a corresponding
// Docker event, for example, when a container spec is updated in place.
containersChanged chan struct{}
}
// NewService creates a new Docker service instance.
@@ -41,9 +45,26 @@ func NewService(client client.APIClient, db *sqlx.DB) *Service {
return &Service{
Client: client,
db: db,
// Buffering as a pending signal is enough for the consumer to pick up all changes made before it reacts.
containersChanged: make(chan struct{}, 1),
}
}
// NotifyContainersChanged signals that service containers changed in the machine database. It doesn't block
// if a signal is already pending.
func (s *Service) NotifyContainersChanged() {
select {
case s.containersChanged <- struct{}{}:
default:
}
}
// ContainersChanged returns a channel that receives a signal when service containers change in the machine database
// without a corresponding Docker event.
func (s *Service) ContainersChanged() <-chan struct{} {
return s.containersChanged
}
// WatchDaemonRestart watches the Docker engine and signals on the returned channel whenever the daemon
// becomes reachable again after a disconnect, which typically indicates a restart.
func (s *Service) WatchDaemonRestart(ctx context.Context) <-chan struct{} {
@@ -141,6 +162,90 @@ func (s *Service) InspectServiceContainer(ctx context.Context, nameOrID string)
return serviceCtr, nil
}
// UpdateServiceContainer updates the requested properties of a service container in place without recreating it.
// It applies the CPU and memory limits to the container and updates the service spec stored for the container.
func (s *Service) UpdateServiceContainer(ctx context.Context, id string, opts api.UpdateContainerOptions) error {
if opts.ServiceLabels != nil {
if err := api.ValidateLabels(*opts.ServiceLabels); err != nil {
return errdefs.ErrInvalidArgument.WithMessage(fmt.Sprintf("invalid service labels: %v", err))
}
}
ctr, err := s.InspectServiceContainer(ctx, id)
if err != nil {
return err
}
spec := ctr.ServiceSpec
resources := spec.Container.Resources
if opts.CPU != nil {
resources.CPU = *opts.CPU
}
if opts.Memory != nil {
resources.Memory = *opts.Memory
}
if opts.MemoryReservation != nil {
resources.MemoryReservation = *opts.MemoryReservation
}
if resources.CPU < 0 || resources.Memory < 0 || resources.MemoryReservation < 0 {
return errdefs.ErrInvalidArgument.WithMessage("CPU and memory limits must not be negative")
}
// Compare with the limits Docker applies to the container rather than the stored spec. They may differ if
// a previous update applied the limits but failed to store the spec, or the container was updated outside
// of Uncloud.
applied := ctr.AppliedResources()
if err = applied.ValidateUpdateInPlace(resources); err != nil {
return errdefs.ErrFailedPrecondition.WithMessage(
fmt.Sprintf("container '%s' can't be updated without recreating it: %v", ctr.ID, err))
}
// Only CPU and memory limits can differ after the in-place update check above.
if !applied.Equals(resources) {
updateResources := container.Resources{
NanoCPUs: resources.CPU,
Memory: resources.Memory,
MemoryReservation: resources.MemoryReservation,
}
// Docker defaults the swap limit to twice the memory limit when creating a container and rejects raising
// the memory limit above the current swap limit, so update both to keep the same semantics.
if resources.Memory > 0 {
updateResources.MemorySwap = 2 * resources.Memory
}
if _, err = s.Client.ContainerUpdate(ctx, ctr.ID, container.UpdateConfig{
Resources: updateResources,
}); err != nil {
return fmt.Errorf("update container resources: %w", err)
}
}
spec.Container.Resources = resources
if opts.ServiceLabels != nil {
spec.Labels = nil
if len(*opts.ServiceLabels) > 0 {
spec.Labels = *opts.ServiceLabels
}
}
specBytes, err := json.Marshal(spec)
if err != nil {
return fmt.Errorf("marshal service spec: %w", err)
}
res, err := s.db.ExecContext(ctx,
`UPDATE containers SET service_spec = $1, updated_at = datetime('subsecond') WHERE id = $2`,
string(specBytes), ctr.ID)
if err != nil {
return fmt.Errorf("update container in database: %w", err)
}
if n, err := res.RowsAffected(); err == nil && n == 0 {
return errdefs.ErrNotFound.WithMessage(fmt.Sprintf("container '%s' not found in machine database", ctr.ID))
}
// Propagate the updated spec to the cluster store without waiting for the next regular sync.
s.NotifyContainersChanged()
return nil
}
// ListServiceContainersResult holds the result of listing service containers, split into regular
// service containers and one-off hook containers.
type ListServiceContainersResult struct {
+1
View File
@@ -30,6 +30,7 @@ type ContainerClient interface {
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
UpdateContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts UpdateContainerOptions) error
WaitContainerHealthy(
ctx context.Context, serviceNameOrID, containerNameOrID string, opts WaitContainerHealthyOptions,
) error
+33
View File
@@ -19,6 +19,11 @@ const (
// in internal/machine/docker/manager.go.
DockerNetworkName = "uncloud"
// LabelReservedPrefix is the prefix of labels reserved for Uncloud that users can't set on services and containers.
LabelReservedPrefix = "uncloud."
// LabelDaemonReservedPrefix is the prefix of labels reserved for the Uncloud daemon system containers.
LabelDaemonReservedPrefix = "uncloudd."
// LabelDaemonManaged marks system containers as managed by the Uncloud daemon.
LabelDaemonManaged = "uncloudd.managed"
@@ -175,6 +180,19 @@ type CreateContainerResponse struct {
Name string
}
// UpdateContainerOptions defines the properties of a service container to update in place without recreating it.
// Nil fields are left unchanged.
type UpdateContainerOptions struct {
// CPU is the new CPU limit in nanocores. The limit can be changed but not removed.
CPU *int64
// Memory is the new memory limit in bytes. The limit can be changed but not removed.
Memory *int64
// MemoryReservation is the new memory reservation in bytes. The reservation can be changed but not removed.
MemoryReservation *int64
// ServiceLabels replaces all service labels. A pointer to an empty map removes all service labels.
ServiceLabels *map[string]string
}
type ServiceContainer struct {
Container
ServiceSpec ServiceSpec
@@ -195,6 +213,21 @@ func (c *ServiceContainer) ServiceName() string {
return c.Config.Labels[LabelServiceName]
}
// AppliedResources returns the resources from the service spec with the CPU and memory limits that Docker currently
// applies to the container. The applied limits may differ from the spec if an in-place update was interrupted or
// the container was updated outside of Uncloud.
func (c *ServiceContainer) AppliedResources() ContainerResources {
resources := c.ServiceSpec.Container.Resources
if c.ContainerJSONBase == nil || c.HostConfig == nil {
return resources
}
resources.CPU = c.HostConfig.NanoCPUs
resources.Memory = c.HostConfig.Memory
resources.MemoryReservation = c.HostConfig.MemoryReservation
return resources
}
// ServiceMode returns the replication mode of the service this container belongs to.
func (c *ServiceContainer) ServiceMode() string {
return c.ServiceSpec.Mode
+33
View File
@@ -1,7 +1,11 @@
package api
import (
"errors"
"github.com/docker/docker/api/types/container"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
const (
@@ -27,6 +31,35 @@ type ContainerResources struct {
Ulimits map[string]Ulimit
}
// Equals returns true if the resources are equal, treating nil and empty slices and maps as equal.
func (r ContainerResources) Equals(other ContainerResources) bool {
return cmp.Equal(r, other, cmpopts.EquateEmpty())
}
// ValidateUpdateInPlace returns an error explaining why a container created with these resources can't be updated
// to the new resources without recreating it. It returns nil if the container can be updated in place.
// Docker can only change the CPU and memory limits of an existing container and can't remove a limit once it's set.
func (r ContainerResources) ValidateUpdateInPlace(new ContainerResources) error {
// Docker treats zero values as unchanged when updating a container.
if r.CPU != 0 && new.CPU == 0 {
return errors.New("CPU limit can't be removed")
}
if r.Memory != 0 && new.Memory == 0 {
return errors.New("memory limit can't be removed")
}
if r.MemoryReservation != 0 && new.MemoryReservation == 0 {
return errors.New("memory reservation can't be removed")
}
r.CPU = new.CPU
r.Memory = new.Memory
r.MemoryReservation = new.MemoryReservation
if !r.Equals(new) {
return errors.New("only CPU and memory limits can be changed")
}
return nil
}
// DeviceMapping represents a device mapping between host and container.
type DeviceMapping struct {
// HostPath is the path to the device on the host.
+72
View File
@@ -0,0 +1,72 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestContainerResources_ValidateUpdateInPlace(t *testing.T) {
t.Parallel()
current := ContainerResources{
CPU: Core,
Memory: 512 * 1024 * 1024,
MemoryReservation: 256 * 1024 * 1024,
}
tests := []struct {
name string
new func(r *ContainerResources)
wantErr string
}{
{
name: "same resources",
new: func(*ContainerResources) {},
},
{
name: "change CPU and memory limits",
new: func(r *ContainerResources) {
r.CPU = 2 * Core
r.Memory = 1024 * 1024 * 1024
r.MemoryReservation = 128 * 1024 * 1024
},
},
{
name: "remove CPU limit",
new: func(r *ContainerResources) { r.CPU = 0 },
wantErr: "CPU limit can't be removed",
},
{
name: "remove memory limit",
new: func(r *ContainerResources) { r.Memory = 0 },
wantErr: "memory limit can't be removed",
},
{
name: "remove memory reservation",
new: func(r *ContainerResources) { r.MemoryReservation = 0 },
wantErr: "memory reservation can't be removed",
},
{
name: "change shared memory",
new: func(r *ContainerResources) { r.SharedMemory = 64 * 1024 * 1024 },
wantErr: "only CPU and memory limits can be changed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newResources := current
tt.new(&newResources)
err := current.ValidateUpdateInPlace(newResources)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr)
}
})
}
}
+29
View File
@@ -77,6 +77,9 @@ type ServiceSpec struct {
Configs []ConfigSpec
// Container defines the desired state of each container in the service.
Container ContainerSpec
// Labels are user-defined key-value metadata attached to the service. Unlike container labels, they're not
// applied to Docker containers. Keys must not use the reserved "uncloud." prefix.
Labels map[string]string
// Mode is the replication mode of the service. Default is ServiceModeReplicated if empty.
Mode string
Name string
@@ -157,6 +160,9 @@ func (s *ServiceSpec) Validate() error {
if err := s.Container.Validate(); err != nil {
return err
}
if err := ValidateLabels(s.Labels); err != nil {
return fmt.Errorf("invalid service labels: %w", err)
}
switch s.Mode {
case "", ServiceModeGlobal, ServiceModeReplicated:
@@ -243,6 +249,7 @@ func (s *ServiceSpec) Clone() ServiceSpec {
spec.Caddy = &caddyCopy
}
spec.Container = s.Container.Clone()
spec.Labels = maps.Clone(s.Labels)
spec.PreDeploy = s.PreDeploy.Clone()
if s.Ports != nil {
@@ -279,6 +286,10 @@ type ContainerSpec struct {
Image string
// Run a custom init inside the container. If nil, use the daemon's configured settings.
Init *bool
// Labels are user-defined key-value metadata applied as Docker labels to each container in the service.
// Docker labels are immutable so changing them requires recreating the containers.
// Keys must not use the reserved "uncloud." prefix.
Labels map[string]string
// LogDriver overrides the default logging driver for the container. Each Docker daemon can have its own default.
LogDriver *LogDriver
// PidMode sets the PID namespace mode for the container. Currently only "" or "host" is supported.
@@ -333,6 +344,9 @@ func (s *ContainerSpec) Validate() error {
if _, err := reference.ParseDockerRef(s.Image); err != nil {
return fmt.Errorf("invalid image '%s': %w", s.Image, err)
}
if err := ValidateLabels(s.Labels); err != nil {
return fmt.Errorf("invalid container labels: %w", err)
}
for _, m := range s.VolumeMounts {
if err := m.Validate(); err != nil {
@@ -390,6 +404,7 @@ func (s *ContainerSpec) Clone() ContainerSpec {
hc.Test = slices.Clone(s.Healthcheck.Test)
spec.Healthcheck = &hc
}
spec.Labels = maps.Clone(s.Labels)
if s.LogDriver != nil {
logDriver := *s.LogDriver
if s.LogDriver.Options != nil {
@@ -428,6 +443,20 @@ func (s *ContainerSpec) Clone() ContainerSpec {
return spec
}
// ValidateLabels checks that label keys are non-empty and don't use the reserved Uncloud prefixes.
func ValidateLabels(labels map[string]string) error {
for k := range labels {
if strings.TrimSpace(k) == "" {
return fmt.Errorf("label key must not be empty")
}
if strings.HasPrefix(k, LabelReservedPrefix) || strings.HasPrefix(k, LabelDaemonReservedPrefix) {
return fmt.Errorf("label '%s' cannot use the reserved prefix '%s' or '%s'",
k, LabelReservedPrefix, LabelDaemonReservedPrefix)
}
}
return nil
}
type EnvVars map[string]string
// ToSlice converts the environment variables to a slice of strings in the format "key=value".
+46 -2
View File
@@ -63,6 +63,47 @@ func TestServiceSpec_Validate_Name(t *testing.T) {
}
}
func TestServiceSpec_Validate_Labels(t *testing.T) {
t.Parallel()
tests := []struct {
name string
labels map[string]string
wantErr string
}{
{name: "nil labels"},
{name: "valid labels", labels: map[string]string{"com.example.team": "web", "tier": ""}},
{name: "prefix without dot", labels: map[string]string{"uncloudfoo": "bar"}},
{name: "empty key", labels: map[string]string{"": "bar"}, wantErr: "must not be empty"},
{name: "reserved prefix", labels: map[string]string{LabelManaged: ""}, wantErr: "reserved prefix"},
{name: "reserved daemon prefix", labels: map[string]string{"uncloudd.foo": "bar"}, wantErr: "reserved prefix"},
}
for _, tt := range tests {
t.Run(tt.name+"/service", func(t *testing.T) {
t.Parallel()
spec := ServiceSpec{Container: ContainerSpec{Image: "nginx:latest"}, Labels: tt.labels}
err := spec.Validate()
if tt.wantErr != "" {
require.ErrorContains(t, err, "invalid service labels")
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
t.Run(tt.name+"/container", func(t *testing.T) {
t.Parallel()
spec := ServiceSpec{Container: ContainerSpec{Image: "nginx:latest", Labels: tt.labels}}
err := spec.Validate()
if tt.wantErr != "" {
require.ErrorContains(t, err, "invalid container labels")
require.ErrorContains(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
tests := []struct {
name string
@@ -267,8 +308,9 @@ func TestContainerSpec_Clone(t *testing.T) {
"FOO": "bar",
"BAZ": "qux",
},
Image: "nginx:latest",
Init: new(true),
Image: "nginx:latest",
Init: new(true),
Labels: map[string]string{"com.example.team": "web"},
LogDriver: &LogDriver{
Name: "json-file",
Options: map[string]string{
@@ -313,6 +355,7 @@ func TestContainerSpec_Clone(t *testing.T) {
original.Command[0] = stringModified
original.Entrypoint[0] = stringModified
original.Env["FOO"] = stringModified
original.Labels["com.example.team"] = stringModified
original.LogDriver.Options["max-size"] = stringModified
original.Volumes[0] = stringModified
original.VolumeMounts[0].ContainerPath = stringModified
@@ -328,6 +371,7 @@ func TestContainerSpec_Clone(t *testing.T) {
assert.Equal(t, "NET_ADMIN", cloned.CapAdd[0])
assert.Equal(t, "ALL", cloned.CapDrop[0])
assert.Equal(t, "sh", cloned.Command[0])
assert.Equal(t, "web", cloned.Labels["com.example.team"])
assert.Equal(t, "/bin/bash", cloned.Entrypoint[0])
assert.Equal(t, "bar", cloned.Env["FOO"])
assert.Equal(t, "qux", cloned.Env["BAZ"])
+8 -1
View File
@@ -55,7 +55,7 @@ func (p *Plan) Format() string {
// formatSummary counts all operations across the plan and renders the summary footer.
func (p *Plan) formatSummary() string {
var createCount, startFirstCount, stopFirstCount, removeCount int
var createCount, startFirstCount, stopFirstCount, updateCount, removeCount int
machines := make(map[string]struct{})
for _, op := range p.Volumes {
@@ -76,6 +76,9 @@ func (p *Plan) formatSummary() string {
} else {
startFirstCount++
}
case *operation.UpdateContainerOperation:
machines[o.MachineID] = struct{}{}
updateCount++
case *operation.RemoveContainerOperation:
machines[o.MachineID] = struct{}{}
removeCount++
@@ -99,6 +102,10 @@ func (p *Plan) formatSummary() string {
parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)"))
}
if updateCount > 0 {
parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(updateCount))+" "+tui.Yellow.Render("update (in place)"))
}
if removeCount > 0 {
parts = append(parts,
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove"))
+30
View File
@@ -0,0 +1,30 @@
package compose
import (
"testing"
"github.com/charmbracelet/x/ansi"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert"
)
func TestPlan_FormatSummary(t *testing.T) {
t.Parallel()
web := &deploy.ServicePlan{}
web.Operations = []operation.Operation{
&operation.UpdateContainerOperation{MachineID: "m-1"},
&operation.RemoveContainerOperation{MachineID: "m-2"},
}
db := &deploy.ServicePlan{}
db.Operations = []operation.Operation{
&operation.UpdateContainerOperation{MachineID: "m-2"},
}
plan := &Plan{
Volumes: []*operation.CreateVolumeOperation{{MachineID: "m-3"}},
Services: []*deploy.ServicePlan{web, db},
}
assert.Equal(t, "1 create · 2 update (in place) · 1 remove · across 3 machines", ansi.Strip(plan.formatSummary()))
}
-14
View File
@@ -344,20 +344,6 @@ configs:
warnCount: 1,
warnContains: []string{"container_name"},
},
{
name: "deploy labels",
composeYAML: `services:
app:
image: myapp:latest
deploy:
mode: replicated
labels:
- foo
replicas: 2
`,
warnCount: 1,
warnContains: []string{"deploy labels"},
},
{
name: "deploy restart_policy",
composeYAML: `services:
+8 -6
View File
@@ -66,6 +66,10 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
Mode: api.ServiceModeReplicated,
}
if labels := mergeLabels(service.Labels, service.CustomLabels); len(labels) > 0 {
spec.Container.Labels = labels
}
// Map x-caddy extension to spec.Caddy if specified.
if caddy, ok := service.Extensions[CaddyExtensionKey].(Caddy); ok && caddy.Config != "" {
spec.Caddy = &api.CaddySpec{
@@ -124,6 +128,10 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
d := time.Duration(cfg.Monitor)
spec.UpdateConfig.MonitorPeriod = &d
}
if len(service.Deploy.Labels) > 0 {
spec.Labels = maps.Clone(service.Deploy.Labels)
}
}
// TODO: can service.tmpfs be handled as tmpfs volume mounts as well?
@@ -480,9 +488,6 @@ func validateServicesFeatures(project *types.Project) []error {
if service.DNSSearch != nil {
errs = append(errs, err(service.Name, "dns_search"))
}
if service.Labels != nil {
errs = append(errs, err(service.Name, "labels"))
}
if service.Links != nil {
errs = append(errs, err(service.Name, "links"))
}
@@ -517,9 +522,6 @@ func validateServicesFeatures(project *types.Project) []error {
if service.Deploy.RollbackConfig != nil {
errs = append(errs, err(service.Name, "deploy rollback_config"))
}
if len(service.Deploy.Labels) != 0 {
errs = append(errs, err(service.Name, "deploy labels"))
}
}
// we only allow the 'default' network, nothing else.
if x := service.Networks; x != nil {
+75
View File
@@ -875,6 +875,81 @@ services:
}
}
func TestServiceSpecFromCompose_Labels(t *testing.T) {
t.Parallel()
tests := []struct {
name string
composeYAML string
expectedContainerLabels map[string]string
expectedServiceLabels map[string]string
}{
{
name: "no labels",
composeYAML: `
services:
web:
image: nginx
`,
},
{
name: "container labels as list",
composeYAML: `
services:
web:
image: nginx
labels:
- com.example.team=web
- com.example.empty
`,
expectedContainerLabels: map[string]string{
"com.example.team": "web",
"com.example.empty": "",
},
},
{
name: "container and service labels as maps",
composeYAML: `
services:
web:
image: nginx
labels:
com.example.team: web
deploy:
labels:
tier: frontend
`,
expectedContainerLabels: map[string]string{"com.example.team": "web"},
expectedServiceLabels: map[string]string{"tier": "frontend"},
},
{
name: "service labels only",
composeYAML: `
services:
web:
image: nginx
deploy:
labels:
- tier=frontend
`,
expectedServiceLabels: map[string]string{"tier": "frontend"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
require.NoError(t, err)
spec, err := ServiceSpecFromCompose(project, "web")
require.NoError(t, err)
assert.Equal(t, tt.expectedContainerLabels, spec.Container.Labels)
assert.Equal(t, tt.expectedServiceLabels, spec.Labels)
})
}
}
func TestServiceSpecFromCompose_UpdateConfig(t *testing.T) {
t.Parallel()
+33
View File
@@ -343,6 +343,39 @@ func (cli *Client) RemoveContainer(
return nil
}
// UpdateContainer updates the requested properties of the container within the service in place without recreating it.
func (cli *Client) UpdateContainer(
ctx context.Context, serviceNameOrID, containerNameOrID string, opts api.UpdateContainerOptions,
) error {
op, err := cli.resolveContainerOperation(ctx, serviceNameOrID, containerNameOrID)
if err != nil {
return err
}
req := &pb.UpdateServiceContainerRequest{
Id: op.containerID,
Cpu: opts.CPU,
Memory: opts.Memory,
MemoryReservation: opts.MemoryReservation,
}
if opts.ServiceLabels != nil {
req.ServiceLabels = &pb.Labels{Labels: *opts.ServiceLabels}
}
pw := progress.ContextWriter(op.ctx)
pw.Event(progress.NewEvent(op.eventID, progress.Working, "Updating"))
if _, err = cli.Docker.GRPCClient.UpdateServiceContainer(op.ctx, req); err != nil {
pw.Event(progress.ErrorEvent(op.eventID))
if status.Code(err) == codes.NotFound {
return api.ErrNotFound
}
return err
}
pw.Event(progress.NewEvent(op.eventID, progress.Done, "Updated"))
return nil
}
// ExecContainer executes a command in a container within the service.
// If containerNameOrID is empty, the first container in the service will be used.
func (cli *Client) ExecContainer(
+58 -13
View File
@@ -1,7 +1,7 @@
package deploy
import (
"reflect"
"maps"
"sort"
"github.com/google/go-cmp/cmp"
@@ -12,7 +12,8 @@ import (
type ContainerSpecStatus string
const (
ContainerUpToDate ContainerSpecStatus = "up-to-date"
ContainerUpToDate ContainerSpecStatus = "up-to-date"
// ContainerNeedsUpdate means the container can be updated in place without recreating it.
ContainerNeedsUpdate ContainerSpecStatus = "needs-update"
ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate"
)
@@ -83,15 +84,7 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
}
}
// Device reservations and mappings are immutable, so we'll need to recreate if any have changed
if !reflect.DeepEqual(current.Container.Resources.DeviceReservations, newResources.DeviceReservations) {
return ContainerNeedsRecreate
}
if !reflect.DeepEqual(current.Container.Resources.Devices, newResources.Devices) {
return ContainerNeedsRecreate
}
// Ulimits are immutable, so we'll need to recreate if any have changed.
if !reflect.DeepEqual(current.Container.Resources.Ulimits, newResources.Ulimits) {
if current.Container.Resources.ValidateUpdateInPlace(newResources) != nil {
return ContainerNeedsRecreate
}
@@ -100,8 +93,10 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
return ContainerNeedsRecreate
}
// Remaining resources are mutable.
if !reflect.DeepEqual(current.Container.Resources, newResources) {
// CPU and memory limits are updated on the running container. Service labels are only stored in the service spec
// and not applied to Docker containers.
if !current.Container.Resources.Equals(newResources) ||
!cmp.Equal(current.Labels, new.Labels, cmpopts.EquateEmpty()) {
return ContainerNeedsUpdate
}
@@ -119,3 +114,53 @@ func sortConfigs(configs []api.ConfigSpec) {
return configs[i].Name < configs[j].Name
})
}
// EvalContainerChange evaluates whether the running container matches the new spec, can be updated in place to
// match it, or needs to be recreated. Unlike EvalContainerSpecChange, it also compares the new spec with the CPU and
// memory limits that Docker applies to the container, so the container is updated when they drifted from its spec.
func EvalContainerChange(current api.ServiceContainer, new api.ServiceSpec) ContainerSpecStatus {
status := EvalContainerSpecChange(current.ServiceSpec, new)
if status == ContainerNeedsRecreate {
return status
}
applied := current.AppliedResources()
newResources := new.Container.Resources
if applied.ValidateUpdateInPlace(newResources) != nil {
return ContainerNeedsRecreate
}
// Only CPU and memory limits can differ after the in-place update check above.
if !applied.Equals(newResources) {
return ContainerNeedsUpdate
}
return status
}
// ContainerUpdateOptions returns the container properties that can be updated in place and differ between the new
// spec and either the container spec or the limits Docker applies to the container. It should only be used when
// EvalContainerChange returns ContainerNeedsUpdate.
func ContainerUpdateOptions(current api.ServiceContainer, new api.ServiceSpec) api.UpdateContainerOptions {
var opts api.UpdateContainerOptions
stored, applied := current.ServiceSpec.Container.Resources, current.AppliedResources()
res := new.Container.Resources
if res.CPU != stored.CPU || res.CPU != applied.CPU {
opts.CPU = &res.CPU
}
if res.Memory != stored.Memory || res.Memory != applied.Memory {
opts.Memory = &res.Memory
}
if res.MemoryReservation != stored.MemoryReservation || res.MemoryReservation != applied.MemoryReservation {
opts.MemoryReservation = &res.MemoryReservation
}
if !cmp.Equal(current.ServiceSpec.Labels, new.Labels, cmpopts.EquateEmpty()) {
labels := maps.Clone(new.Labels)
if labels == nil {
labels = map[string]string{}
}
opts.ServiceLabels = &labels
}
return opts
}
+235 -4
View File
@@ -86,6 +86,223 @@ func TestEvalContainerSpecChange_ContainerTty(t *testing.T) {
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
}
func TestEvalContainerSpecChange_ContainerLabels(t *testing.T) {
t.Parallel()
currentSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
}
newSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
Labels: map[string]string{"com.example.team": "web"},
},
}
emptySpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
Labels: map[string]string{},
},
}
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpec))
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(newSpec, currentSpec))
assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(newSpec, newSpec))
assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(currentSpec, emptySpec))
}
func TestEvalContainerSpecChange_ServiceLabels(t *testing.T) {
t.Parallel()
currentSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
}
newSpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
Labels: map[string]string{"tier": "frontend"},
}
emptySpec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
},
Labels: map[string]string{},
}
assert.Equal(t, ContainerNeedsUpdate, EvalContainerSpecChange(currentSpec, newSpec))
assert.Equal(t, ContainerNeedsUpdate, EvalContainerSpecChange(newSpec, currentSpec))
assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(newSpec, newSpec))
assert.Equal(t, ContainerUpToDate, EvalContainerSpecChange(currentSpec, emptySpec))
// Changes that require recreating the container take precedence over the in-place update.
newSpecWithImage := newSpec.Clone()
newSpecWithImage.Container.Image = "nginx:alpine"
assert.Equal(t, ContainerNeedsRecreate, EvalContainerSpecChange(currentSpec, newSpecWithImage))
}
func TestEvalContainerChange(t *testing.T) {
t.Parallel()
spec := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
Resources: api.ContainerResources{CPU: api.Core, Memory: 256 * 1024 * 1024},
},
}
appliedSpec := container.Resources{NanoCPUs: api.Core, Memory: 256 * 1024 * 1024}
tests := []struct {
name string
hostConfig *container.HostConfig
change func(spec *api.ServiceSpec)
expected ContainerSpecStatus
}{
{
name: "applied limits match spec",
hostConfig: &container.HostConfig{Resources: appliedSpec},
change: func(*api.ServiceSpec) {},
expected: ContainerUpToDate,
},
{
name: "unknown host config falls back to spec limits",
change: func(*api.ServiceSpec) {},
expected: ContainerUpToDate,
},
{
name: "applied memory limit drifted from spec",
hostConfig: &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 512 * 1024 * 1024}},
change: func(*api.ServiceSpec) {},
expected: ContainerNeedsUpdate,
},
{
name: "applied limits already match new spec but stored spec doesn't",
hostConfig: &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 512 * 1024 * 1024}},
change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 512 * 1024 * 1024 },
expected: ContainerNeedsUpdate,
},
{
name: "applied limit not in spec can't be removed in place",
hostConfig: &container.HostConfig{Resources: container.Resources{
NanoCPUs: api.Core, Memory: 256 * 1024 * 1024, MemoryReservation: 128 * 1024 * 1024,
}},
change: func(*api.ServiceSpec) {},
expected: ContainerNeedsRecreate,
},
{
name: "spec change that requires recreating",
hostConfig: &container.HostConfig{Resources: appliedSpec},
change: func(spec *api.ServiceSpec) { spec.Container.Image = "nginx:alpine" },
expected: ContainerNeedsRecreate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newSpec := spec.Clone()
tt.change(&newSpec)
assert.Equal(t, tt.expected, EvalContainerChange(serviceContainer(spec, tt.hostConfig), newSpec))
})
}
}
func TestContainerUpdateOptions(t *testing.T) {
t.Parallel()
current := api.ServiceSpec{
Container: api.ContainerSpec{
Image: "nginx:latest",
Resources: api.ContainerResources{CPU: api.Core, Memory: 256 * 1024 * 1024},
},
Labels: map[string]string{"tier": "frontend"},
}
applied := &container.HostConfig{Resources: container.Resources{NanoCPUs: api.Core, Memory: 256 * 1024 * 1024}}
tests := []struct {
name string
hostConfig *container.HostConfig
change func(spec *api.ServiceSpec)
expected api.UpdateContainerOptions
}{
{
name: "no changes",
hostConfig: applied,
change: func(*api.ServiceSpec) {},
},
{
name: "CPU only",
hostConfig: applied,
change: func(spec *api.ServiceSpec) { spec.Container.Resources.CPU = 2 * api.Core },
expected: api.UpdateContainerOptions{CPU: new(int64(2 * api.Core))},
},
{
name: "memory and reservation",
hostConfig: applied,
change: func(spec *api.ServiceSpec) {
spec.Container.Resources.Memory = 512 * 1024 * 1024
spec.Container.Resources.MemoryReservation = 128 * 1024 * 1024
},
expected: api.UpdateContainerOptions{
Memory: new(int64(512 * 1024 * 1024)),
MemoryReservation: new(int64(128 * 1024 * 1024)),
},
},
{
name: "applied memory limit drifted from spec",
hostConfig: &container.HostConfig{Resources: container.Resources{
NanoCPUs: api.Core, Memory: 512 * 1024 * 1024,
}},
change: func(*api.ServiceSpec) {},
expected: api.UpdateContainerOptions{Memory: new(int64(256 * 1024 * 1024))},
},
{
name: "applied memory limit already matches new spec",
hostConfig: &container.HostConfig{Resources: container.Resources{
NanoCPUs: api.Core, Memory: 512 * 1024 * 1024,
}},
change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 512 * 1024 * 1024 },
expected: api.UpdateContainerOptions{Memory: new(int64(512 * 1024 * 1024))},
},
{
name: "change service labels",
hostConfig: applied,
change: func(spec *api.ServiceSpec) { spec.Labels = map[string]string{"tier": "backend"} },
expected: api.UpdateContainerOptions{ServiceLabels: &map[string]string{"tier": "backend"}},
},
{
name: "remove all service labels",
hostConfig: applied,
change: func(spec *api.ServiceSpec) { spec.Labels = nil },
expected: api.UpdateContainerOptions{ServiceLabels: &map[string]string{}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newSpec := current.Clone()
tt.change(&newSpec)
assert.Equal(t, tt.expected, ContainerUpdateOptions(serviceContainer(current, tt.hostConfig), newSpec))
})
}
}
func serviceContainer(spec api.ServiceSpec, hostConfig *container.HostConfig) api.ServiceContainer {
return api.ServiceContainer{
Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{HostConfig: hostConfig},
}},
ServiceSpec: spec,
}
}
func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
t.Parallel()
@@ -127,7 +344,7 @@ func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
CPU: 1000000000,
},
new: api.ContainerResources{},
want: ContainerNeedsUpdate,
want: ContainerNeedsRecreate,
},
// Memory
@@ -189,19 +406,33 @@ func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
name: "unset memory",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024},
want: ContainerNeedsUpdate,
want: ContainerNeedsRecreate,
},
{
name: "unset reservation",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{Memory: 100 * 1024 * 1024},
want: ContainerNeedsUpdate,
want: ContainerNeedsRecreate,
},
{
name: "unset memory and reservation",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{},
want: ContainerNeedsUpdate,
want: ContainerNeedsRecreate,
},
// Shared memory
{
name: "change shared memory",
current: api.ContainerResources{SharedMemory: 64 * 1024 * 1024},
new: api.ContainerResources{SharedMemory: 128 * 1024 * 1024},
want: ContainerNeedsRecreate,
},
{
name: "change memory and shared memory",
current: api.ContainerResources{Memory: 100 * 1024 * 1024},
new: api.ContainerResources{Memory: 200 * 1024 * 1024, SharedMemory: 64 * 1024 * 1024},
want: ContainerNeedsRecreate,
},
// Memory and CPU
+8 -1
View File
@@ -173,7 +173,7 @@ func (sp *ServicePlan) Format() string {
// FormatSummary counts operations in the service plan and renders a styled summary line.
func (sp *ServicePlan) FormatSummary() string {
var createCount, startFirstCount, stopFirstCount, removeCount int
var createCount, startFirstCount, stopFirstCount, updateCount, removeCount int
machines := make(map[string]struct{})
for _, op := range sp.Operations {
@@ -188,6 +188,9 @@ func (sp *ServicePlan) FormatSummary() string {
} else {
startFirstCount++
}
case *operation.UpdateContainerOperation:
machines[o.MachineID] = struct{}{}
updateCount++
case *operation.RemoveContainerOperation:
machines[o.MachineID] = struct{}{}
removeCount++
@@ -210,6 +213,10 @@ func (sp *ServicePlan) FormatSummary() string {
parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)"))
}
if updateCount > 0 {
parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(updateCount))+" "+tui.Yellow.Render("update (in place)"))
}
if removeCount > 0 {
parts = append(parts,
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove"))
+51
View File
@@ -0,0 +1,51 @@
package deploy
import (
"testing"
"github.com/charmbracelet/x/ansi"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert"
)
func TestServicePlan_FormatSummary(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ops []operation.Operation
expected string
}{
{
name: "in-place updates only",
ops: []operation.Operation{
&operation.UpdateContainerOperation{MachineID: "m-1"},
&operation.UpdateContainerOperation{MachineID: "m-2"},
},
expected: "2 update (in place) · across 2 machines",
},
{
name: "all operation types",
ops: []operation.Operation{
&operation.RunContainerOperation{MachineID: "m-1"},
&operation.ReplaceContainerOperation{MachineID: "m-1", Order: api.UpdateOrderStartFirst},
&operation.ReplaceContainerOperation{MachineID: "m-2", Order: api.UpdateOrderStopFirst},
&operation.UpdateContainerOperation{MachineID: "m-2"},
&operation.RemoveContainerOperation{MachineID: "m-3"},
},
expected: "1 create · 1 replace (start-first) · 1 replace (stop-first) · 1 update (in place) · 1 remove · " +
"across 3 machines",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
plan := ServicePlan{}
plan.Operations = tt.ops
assert.Equal(t, tt.expected, ansi.Strip(plan.FormatSummary()))
})
}
}
+50 -24
View File
@@ -92,7 +92,7 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
// Organise existing containers by machine.
containersOnMachine := make(map[string][]api.ServiceContainer)
upToDateContainersOnMachine := make(map[string]int)
reusableContainersOnMachine := make(map[string]int)
containerSpecStatuses := make(map[string]ContainerSpecStatus)
if svc != nil {
for _, c := range svc.Containers {
@@ -105,40 +105,50 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
if s.ForceRecreate {
status = ContainerNeedsRecreate
} else {
status = EvalContainerSpecChange(c.Container.ServiceSpec, spec)
status = EvalContainerChange(c.Container, spec)
}
containerSpecStatuses[c.Container.ID] = status
if status == ContainerUpToDate {
upToDateContainersOnMachine[c.MachineID] += 1
if status != ContainerNeedsRecreate {
reusableContainersOnMachine[c.MachineID] += 1
}
}
// Sort containers such that running containers with the desired spec are first.
// Sort containers such that running containers with the desired spec or that can be updated in place to
// the desired spec are first.
// TODO: consider ranking up-to-date containers above ones that need an in-place update (here, when sorting
// machines, and in reconcileGlobalContainer) to avoid updating a container while removing an up-to-date one.
isReusable := func(c api.MachineServiceContainer) bool {
// Not running containers have no status.
status, ok := containerSpecStatuses[c.Container.ID]
return ok && status != ContainerNeedsRecreate
}
slices.SortFunc(svc.Containers, func(c1, c2 api.MachineServiceContainer) int {
if status, ok := containerSpecStatuses[c1.Container.ID]; ok && status == ContainerUpToDate {
r1, r2 := isReusable(c1), isReusable(c2)
switch {
case r1 == r2:
return 0
case r1:
return -1
}
if status, ok := containerSpecStatuses[c2.Container.ID]; ok && status == ContainerUpToDate {
default:
return 1
}
return 0
})
for _, c := range svc.Containers {
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c.Container)
}
// Sort machines such that machines with the most up-to-date containers are first, followed by machines with
// existing containers, and finally machines without containers.
// Sort machines such that machines with the most containers that are up to date or can be updated in place
// are first, followed by machines with existing containers, and finally machines without containers.
slices.SortFunc(matchedMachines, func(m1, m2 *pb.MachineInfo) int {
if upToDateContainersOnMachine[m1.Id] > 0 && upToDateContainersOnMachine[m2.Id] > 0 {
return upToDateContainersOnMachine[m2.Id] - upToDateContainersOnMachine[m1.Id]
if reusableContainersOnMachine[m1.Id] > 0 && reusableContainersOnMachine[m2.Id] > 0 {
return reusableContainersOnMachine[m2.Id] - reusableContainersOnMachine[m1.Id]
}
if upToDateContainersOnMachine[m1.Id] > 0 {
if reusableContainersOnMachine[m1.Id] > 0 {
return -1
}
if upToDateContainersOnMachine[m2.Id] > 0 {
if reusableContainersOnMachine[m2.Id] > 0 {
return 1
}
return len(containersOnMachine[m2.Id]) - len(containersOnMachine[m1.Id])
@@ -170,7 +180,16 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
if status == ContainerUpToDate {
continue
}
// TODO: handle ContainerNeedsUpdate when update of mutable fields on a container is supported.
if status == ContainerNeedsUpdate {
plan.Operations = append(plan.Operations, &operation.UpdateContainerOperation{
ServiceID: plan.ServiceID,
MachineID: m.Id,
MachineName: m.Name,
Container: ctr,
Options: ContainerUpdateOptions(ctr, spec),
})
continue
}
}
// Replace the old container with a new one.
@@ -301,7 +320,7 @@ func reconcileGlobalContainer(
}
// Check if there is a container with the same spec already running. If so, remove the rest.
upToDate := false
reused := false
for i, c := range containers {
if !c.Container.State.Running || c.Container.State.Paused {
// Skip containers that are not running.
@@ -312,12 +331,21 @@ func reconcileGlobalContainer(
if forceRecreate {
status = ContainerNeedsRecreate
} else {
status = EvalContainerSpecChange(c.Container.ServiceSpec, spec)
status = EvalContainerChange(c.Container, spec)
}
if status == ContainerUpToDate {
// The container is already running with the same spec.
upToDate = true
if status != ContainerNeedsRecreate {
// The container is already running with the desired spec or can be updated in place to it.
reused = true
if status == ContainerNeedsUpdate {
ops = append(ops, &operation.UpdateContainerOperation{
ServiceID: serviceID,
MachineID: machine.Id,
MachineName: machine.Name,
Container: c.Container,
Options: ContainerUpdateOptions(c.Container, spec),
})
}
for j, old := range containers {
if i == j {
continue
@@ -331,10 +359,8 @@ func reconcileGlobalContainer(
}
break
}
// TODO: handle ContainerNeedsUpdate when update of mutable fields on a container is supported.
// Make sure to update preDeployOperation accordingly.
}
if upToDate {
if reused {
return ops, nil
}
+135 -1
View File
@@ -1,6 +1,8 @@
package deploy
import (
"slices"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
@@ -11,6 +13,7 @@ import (
"github.com/psviderski/uncloud/pkg/client/deploy/scheduler"
"github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDetermineUpdateOrder(t *testing.T) {
@@ -424,6 +427,121 @@ func TestReconcileGlobalContainer(t *testing.T) {
}
}
func TestRollingStrategy_Plan_UpdateInPlace(t *testing.T) {
t.Parallel()
state := &scheduler.ClusterState{
Machines: []*scheduler.Machine{
{Info: &pb.MachineInfo{Id: "m-1", Name: "machine-1"}},
{Info: &pb.MachineInfo{Id: "m-2", Name: "machine-2"}},
},
}
newContainer := func(id string, spec api.ServiceSpec) api.ServiceContainer {
ctr := newServiceContainer(id, container.State{Running: true, Status: "running"})
ctr.ServiceSpec = spec
return ctr
}
tests := []struct {
name string
change func(spec *api.ServiceSpec)
// options are the expected in-place update options. Nil means the containers must be recreated.
options *api.UpdateContainerOptions
}{
{
name: "service labels",
change: func(spec *api.ServiceSpec) { spec.Labels = map[string]string{"tier": "backend"} },
options: &api.UpdateContainerOptions{ServiceLabels: &map[string]string{"tier": "backend"}},
},
{
name: "CPU and memory limits",
change: func(spec *api.ServiceSpec) {
spec.Container.Resources.CPU = 2 * api.Core
spec.Container.Resources.Memory = 512 * 1024 * 1024
},
options: &api.UpdateContainerOptions{
CPU: new(int64(2 * api.Core)),
Memory: new(int64(512 * 1024 * 1024)),
},
},
{
name: "remove memory limit",
change: func(spec *api.ServiceSpec) { spec.Container.Resources.Memory = 0 },
},
}
for _, tt := range tests {
for _, mode := range []string{api.ServiceModeReplicated, api.ServiceModeGlobal} {
t.Run(tt.name+"/"+mode, func(t *testing.T) {
currentSpec := api.ServiceSpec{
Name: "app",
Mode: mode,
Container: api.ContainerSpec{
Image: "nginx:latest",
Resources: api.ContainerResources{
CPU: api.Core,
Memory: 256 * 1024 * 1024,
},
},
Labels: map[string]string{"tier": "frontend"},
PreDeploy: &api.PreDeployHook{Command: []string{"db", "migrate"}},
}
if mode == api.ServiceModeReplicated {
currentSpec.Replicas = 2
}
newSpec := currentSpec.Clone()
tt.change(&newSpec)
ctr1 := newContainer("container-1", currentSpec)
ctr2 := newContainer("container-2", currentSpec)
svc := &api.Service{
ID: "svc-1",
Name: "app",
Mode: mode,
Containers: []api.MachineServiceContainer{
{MachineID: "m-1", MachineName: "machine-1", Container: ctr1},
{MachineID: "m-2", MachineName: "machine-2", Container: ctr2},
},
}
plan, err := (&RollingStrategy{}).Plan(state, svc, newSpec)
require.NoError(t, err)
if tt.options == nil {
require.NotEmpty(t, plan.Operations)
for _, op := range plan.Operations {
_, isUpdate := op.(*operation.UpdateContainerOperation)
assert.False(t, isUpdate, "Expected containers to be recreated, got %s", op)
}
return
}
// The replicated strategy shuffles machines so sort the operations to compare them deterministically.
ops := slices.Clone(plan.Operations)
slices.SortFunc(ops, func(a, b operation.Operation) int {
return strings.Compare(a.String(), b.String())
})
// The containers are updated in place and the pre-deploy hook is not run.
assertOperationsEqual(t, []operation.Operation{
&operation.UpdateContainerOperation{
ServiceID: "svc-1",
MachineID: "m-1",
MachineName: "machine-1",
Container: ctr1,
Options: *tt.options,
},
&operation.UpdateContainerOperation{
ServiceID: "svc-1",
MachineID: "m-2",
MachineName: "machine-2",
Container: ctr2,
Options: *tt.options,
},
}, ops)
})
}
}
}
func TestPreDeployOperations(t *testing.T) {
hook := &api.PreDeployHook{
Command: []string{"db", "migrate"},
@@ -502,6 +620,20 @@ func TestPreDeployOperations(t *testing.T) {
},
},
},
{
name: "plan has only UpdateContainerOperation",
plan: ServicePlan{
ServiceID: "svc-1",
ServiceName: "app",
Spec: api.ServiceSpec{PreDeploy: hook},
SequenceOperation: operation.SequenceOperation{
Operations: []operation.Operation{
&operation.UpdateContainerOperation{MachineID: "m-1", MachineName: "machine-1"},
},
},
},
expected: nil,
},
{
name: "plan has only RemoveContainerOperation",
plan: ServicePlan{
@@ -614,7 +746,8 @@ func TestPreDeployOperations(t *testing.T) {
},
svc: &api.Service{
HookContainers: []api.MachineServiceContainer{
{MachineID: "m-1", Container: newServiceContainer("stopped-hook", container.State{Status: "exited"})},
{MachineID: "m-1", Container: newServiceContainer("stopped-hook",
container.State{Status: "exited"})},
{MachineID: "m-1", Container: runningHook1},
{MachineID: "m-3", Container: runningHook2},
},
@@ -660,6 +793,7 @@ func newServiceContainer(id string, state container.State) api.ServiceContainer
ID: id,
State: &state,
},
Config: &container.Config{},
},
}}
}
+33
View File
@@ -285,6 +285,39 @@ func (o *ReplaceContainerOperation) String() string {
o.MachineID, o.ServiceID, o.OldContainer.ID, o.Order)
}
// UpdateContainerOperation updates the requested properties of a running container in place without recreating it.
type UpdateContainerOperation struct {
ServiceID string
MachineID string
// MachineName is used for formatting the operation as part of the deployment plan.
MachineName string
Container api.ServiceContainer
Options api.UpdateContainerOptions
}
func (o *UpdateContainerOperation) Execute(ctx context.Context, cli Client) error {
if err := cli.UpdateContainer(ctx, o.ServiceID, o.Container.ID, o.Options); err != nil {
return fmt.Errorf("update container: %w", err)
}
return nil
}
func (o *UpdateContainerOperation) Format() string {
displayName := o.Container.ServiceSpec.Name + tui.Faint.Render("/") + o.Container.ShortID()
return tui.BoldYellow.Render("~") + " " +
tui.Faint.Render("update container") + " " +
displayName + " " +
tui.Faint.Render("on") + " " +
o.MachineName + " " +
tui.Faint.Render("(in place)")
}
func (o *UpdateContainerOperation) String() string {
return fmt.Sprintf("UpdateContainerOperation[machine_id=%s service_id=%s container_id=%s]",
o.MachineID, o.ServiceID, o.Container.ID)
}
// stopOptions converts a stop grace period duration to Docker container stop options.
func stopOptions(gracePeriod *time.Duration) container.StopOptions {
if gracePeriod == nil {
+7
View File
@@ -123,6 +123,13 @@ func (c *recordingClient) RemoveContainer(
return nil
}
func (c *recordingClient) UpdateContainer(
_ context.Context, _, containerID string, _ api.UpdateContainerOptions,
) error {
c.calls = append(c.calls, "update:"+containerID)
return nil
}
func (c *recordingClient) CreatePreDeployHookContainer(
context.Context, string, api.ServiceSpec, string,
) (api.CreateContainerResponse, error) {
+4 -1
View File
@@ -39,7 +39,7 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
desiredSpec := spec.SetDefaults()
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, desiredSpec)
status := deploy.EvalContainerChange(ctr, desiredSpec)
assert.Equal(t, deploy.ContainerUpToDate, status)
spec = renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
@@ -48,6 +48,9 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode])
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
for k, v := range spec.Container.Labels {
assert.Equal(t, v, ctr.Config.Labels[k], "Expected container label '%s'", k)
}
// Command and Entrypoint can only be compared if they are set in the spec.
// Otherwise, the container takes them from the image.
+82
View File
@@ -2,7 +2,9 @@ package e2e
import (
"context"
"fmt"
"testing"
"time"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume"
@@ -10,6 +12,7 @@ import (
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -548,6 +551,85 @@ func TestComposeDeployment(t *testing.T) {
"Service containers should be on machines 1 and 3 from comma-separated list")
})
t.Run("labels and in-place update of service labels and resources", func(t *testing.T) {
t.Parallel()
name := "test-compose-labels"
t.Cleanup(func() {
removeServices(t, cli, name)
})
composeYAML := func(tier, memLimit string) string {
return fmt.Sprintf(`
services:
%s:
image: portainer/pause:3.9
mem_limit: %s
labels:
com.example.team: web
deploy:
replicas: 2
labels:
tier: %s
`, name, memLimit, tier)
}
project, err := compose.LoadProjectFromContent(ctx, composeYAML("frontend", "64m"))
require.NoError(t, err)
deployment, err := compose.NewDeployment(ctx, cli, project)
require.NoError(t, err)
require.NoError(t, deployment.Run(ctx))
svc, err := cli.InspectService(ctx, name)
require.NoError(t, err)
expectedSpec := api.ServiceSpec{
Name: name,
Mode: api.ServiceModeReplicated,
Container: api.ContainerSpec{
Image: "portainer/pause:3.9",
Labels: map[string]string{"com.example.team": "web"},
Resources: api.ContainerResources{Memory: 64 * 1024 * 1024},
},
Labels: map[string]string{"tier": "frontend"},
Replicas: 2,
}
assertServiceMatchesSpec(t, svc, expectedSpec)
for _, mc := range svc.Containers {
assert.NotContains(t, mc.Container.Config.Labels, "tier",
"Service labels must not be applied to containers")
}
containers := serviceContainerIDs(svc)
// Changing the service labels and memory limit should update the containers in place.
project, err = compose.LoadProjectFromContent(ctx, composeYAML("backend", "128m"))
require.NoError(t, err)
deployment, err = compose.NewDeployment(ctx, cli, project)
require.NoError(t, err)
plan, err := deployment.Plan(ctx)
require.NoError(t, err)
require.Len(t, plan.Services, 1)
require.Len(t, plan.Services[0].Operations, 2)
for _, op := range plan.Services[0].Operations {
assert.IsType(t, &operation.UpdateContainerOperation{}, op)
}
require.NoError(t, deployment.Run(ctx))
// The updated spec propagates to the cluster store asynchronously.
expectedSpec.Labels = map[string]string{"tier": "backend"}
expectedSpec.Container.Resources.Memory = 128 * 1024 * 1024
assert.EventuallyWithT(t, func(c *assert.CollectT) {
svc, err = cli.InspectService(ctx, name)
require.NoError(c, err)
for _, mc := range svc.Containers {
assert.Equal(c, expectedSpec.Labels, mc.Container.ServiceSpec.Labels)
assert.Equal(c, expectedSpec.Container.Resources.Memory, mc.Container.HostConfig.Memory)
}
}, 10*time.Second, 200*time.Millisecond)
assertServiceMatchesSpec(t, svc, expectedSpec)
assert.Equal(t, containers, serviceContainerIDs(svc), "Expected containers to be updated in place")
})
// Catches regression: https://github.com/psviderski/uncloud/issues/176
t.Run("plan new deployment with volumes and recreate strategy", func(t *testing.T) {
t.Parallel()
+84
View File
@@ -1761,6 +1761,90 @@ func TestServiceLifecycle(t *testing.T) {
require.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("update container in place", func(t *testing.T) {
t.Parallel()
serviceID := newServiceID()
spec := api.ServiceSpec{
Name: "container-update",
Container: api.ContainerSpec{
Image: "portainer/pause:latest",
Resources: api.ContainerResources{
CPU: 100 * api.MilliCore,
Memory: 20 * units.MiB,
},
},
Labels: map[string]string{"tier": "frontend"},
}
ctr, err := cli.CreateContainer(ctx, serviceID, spec, c.Machines[0].Name)
require.NoError(t, err)
t.Cleanup(func() {
err := cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{Force: true})
if !errors.Is(err, api.ErrNotFound) {
require.NoError(t, err)
}
})
require.NoError(t, cli.StartContainer(ctx, serviceID, ctr.ID))
// Update the CPU and memory limits and replace the service labels.
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
CPU: new(int64(200 * api.MilliCore)),
Memory: new(int64(40 * units.MiB)),
MemoryReservation: new(int64(10 * units.MiB)),
ServiceLabels: &map[string]string{"tier": "backend"},
})
require.NoError(t, err)
spec.Container.Resources.CPU = 200 * api.MilliCore
spec.Container.Resources.Memory = 40 * units.MiB
spec.Container.Resources.MemoryReservation = 10 * units.MiB
spec.Labels = map[string]string{"tier": "backend"}
mc, err := cli.InspectContainer(ctx, serviceID, ctr.ID)
require.NoError(t, err)
assert.Equal(t, ctr.ID, mc.Container.ID, "Expected container to be updated in place")
assert.True(t, mc.Container.State.Running, "Expected container to keep running")
assertContainerMatchesSpec(t, mc.Container, spec)
assert.Equal(t, spec.Labels, mc.Container.ServiceSpec.Labels)
assert.EqualValues(t, 80*units.MiB, mc.Container.HostConfig.MemorySwap,
"Expected swap limit to be twice the memory limit")
// Remove all service labels and leave the limits unchanged.
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
ServiceLabels: &map[string]string{},
})
require.NoError(t, err)
spec.Labels = nil
mc, err = cli.InspectContainer(ctx, serviceID, ctr.ID)
require.NoError(t, err)
assertContainerMatchesSpec(t, mc.Container, spec)
assert.Nil(t, mc.Container.ServiceSpec.Labels)
// A limit can't be removed without recreating the container.
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{Memory: new(int64(0))})
require.ErrorContains(t, err, "memory limit can't be removed")
mc, err = cli.InspectContainer(ctx, serviceID, ctr.ID)
require.NoError(t, err)
assertContainerMatchesSpec(t, mc.Container, spec)
// Reserved service labels are rejected.
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
ServiceLabels: &map[string]string{api.LabelManaged: ""},
})
require.ErrorContains(t, err, "reserved prefix")
require.NoError(t, cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{Force: true}))
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
ServiceLabels: &map[string]string{"tier": "frontend"},
})
require.ErrorIs(t, err, api.ErrNotFound)
})
t.Run("1 replica", func(t *testing.T) {
t.Parallel()
@@ -10,16 +10,16 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
:::
| Feature | Support Status | Notes |
|----------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| **Services** | | |
| Feature | Support Status | Notes |
|----------------------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| **Services** | | |
| `build` | ✅ Supported | Build context and Dockerfile |
| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) |
| `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop |
| `command` | ✅ Supported | Override container command |
| `configs` | ✅ Supported | File-based and inline configs |
| `cpus` | ✅ Supported | CPU limit |
| `depends_on` | ⚠️ Limited | Deployed in dependency order. Use [pre-deploy hooks](../4-guides/1-deployments/5-pre-deploy-hooks.md) for `service_completed_successfully` |
| `depends_on` | ⚠️ Limited | Deployed in dependency order. Use [pre-deploy hooks](../4-guides/1-deployments/5-pre-deploy-hooks.md) for `service_completed_successfully` |
| `devices` | ✅ Supported | Device mappings |
| `dns` | ❌ Not supported | Built-in service discovery |
| `dns_search` | ❌ Not supported | Built-in service discovery |
@@ -30,7 +30,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
| `healthcheck` | ✅ Supported | Health check configuration |
| `image` | ✅ Supported | Container image specification |
| `init` | ✅ Supported | Run init process in container |
| `labels` | ❌ Not supported | |
| `labels` | ✅ Supported | Docker labels on containers |
| `links` | ❌ Not supported | Use service names for communication |
| `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver |
| `mem_limit` | ✅ Supported | Memory limit |
@@ -39,10 +39,10 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
| `memswap_limit` | ❌ Not supported | |
| `networks` | ❌ Not supported | All containers share cluster network |
| `pid` | ✅ Supported | Set the PID namespace mode, `pid: host` only |
| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](2-extensions.md#x-ports) for HTTP/HTTPS |
| `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](2-extensions.md#x-ports) for HTTP/HTTPS |
| `privileged` | ✅ Supported | Run containers in privileged mode |
| `pull_policy` | ✅ Supported | `always`, `missing`, `never` |
| `secrets` | ⚠️ Limited | Reference secrets in `environment`, see [Secrets](../3-concepts/8-secrets.md). File mounts not supported |
| `secrets` | ⚠️ Limited | Reference secrets in `environment`, see [Secrets](../3-concepts/8-secrets.md). File mounts not supported |
| `security_opt` | ❌ Not supported | |
| `shm_size` | ✅ Supported | Shared memory size |
| `stdin_open` | ✅ Supported | Allocate standard input and keep it open |
@@ -53,28 +53,28 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
| `ulimits` | ✅ Supported | Resource limits |
| `user` | ✅ Supported | Set container user |
| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs |
| **Deploy** | | |
| `labels` | ❌ Not supported | |
| **Deploy** | | |
| `labels` | ✅ Supported | Service labels, not applied to containers. Use `services.<name>.labels` to set Docker labels on containers |
| `mode` | ✅ Supported | Either `global` or `replicated` |
| `placement` | ❌ Not supported | Use [`x-machines`](2-extensions.md#x-machines) extension |
| `replicas` | ✅ Supported | Number of container replicas |
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
| `resources` | ⚠️ Limited | CPU, memory limits and device reservations |
| `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
| `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) |
| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
| **Volumes** | | |
| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
| **Volumes** | | |
| Named volumes | ✅ Supported | Docker volumes |
| Bind mounts | ✅ Supported | Host path binding with optional [runtime templates](../3-concepts/6-services/3-runtime-templates.md) |
| Tmpfs mounts | ✅ Supported | In-memory filesystems |
| Volume labels | ✅ Supported | Custom labels |
| External volumes | ✅ Supported | Must exist before deployment |
| [Volume drivers][volume-drivers] | ✅ Supported | `local` (supports [NFS][volume-nfs], [CIFS/Samba][volume-cifs]) and manually installed third-party drivers |
| **Configs** | | |
| **Configs** | | |
| File-based configs | ✅ Supported | Read from file |
| Inline configs | ✅ Supported | Defined in compose file |
| External configs | ❌ Not supported | Not supported |
| Short syntax | ❌ Not supported | Use long syntax only |
| **Extensions** | | |
| **Extensions** | | |
| `x-context` | ✅ Uncloud-specific | Cluster context override |
| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration |
| `x-machines` | ✅ Uncloud-specific | Machine placement constraints |