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 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 { type ListServiceContainersRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -2272,7 +2403,7 @@ type ListServiceContainersRequest struct {
func (x *ListServiceContainersRequest) Reset() { func (x *ListServiceContainersRequest) Reset() {
*x = ListServiceContainersRequest{} *x = ListServiceContainersRequest{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2285,7 +2416,7 @@ func (x *ListServiceContainersRequest) String() string {
func (*ListServiceContainersRequest) ProtoMessage() {} func (*ListServiceContainersRequest) ProtoMessage() {}
func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2298,7 +2429,7 @@ func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListServiceContainersRequest.ProtoReflect.Descriptor instead.
func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) { 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 { func (x *ListServiceContainersRequest) GetServiceId() string {
@@ -2328,7 +2459,7 @@ type ListServiceContainersResponse struct {
func (x *ListServiceContainersResponse) Reset() { func (x *ListServiceContainersResponse) Reset() {
*x = ListServiceContainersResponse{} *x = ListServiceContainersResponse{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2341,7 +2472,7 @@ func (x *ListServiceContainersResponse) String() string {
func (*ListServiceContainersResponse) ProtoMessage() {} func (*ListServiceContainersResponse) ProtoMessage() {}
func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2354,7 +2485,7 @@ func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListServiceContainersResponse.ProtoReflect.Descriptor instead.
func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) { 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 { func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers {
@@ -2378,7 +2509,7 @@ type MachineServiceContainers struct {
func (x *MachineServiceContainers) Reset() { func (x *MachineServiceContainers) Reset() {
*x = MachineServiceContainers{} *x = MachineServiceContainers{}
if protoimpl.UnsafeEnabled { 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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2391,7 +2522,7 @@ func (x *MachineServiceContainers) String() string {
func (*MachineServiceContainers) ProtoMessage() {} func (*MachineServiceContainers) ProtoMessage() {}
func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message { 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 { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2404,7 +2535,7 @@ func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message {
// Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead. // Deprecated: Use MachineServiceContainers.ProtoReflect.Descriptor instead.
func (*MachineServiceContainers) Descriptor() ([]byte, []int) { 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 { 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, 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, 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, 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, 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, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x18, 0x4d, 0x61,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74,
0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d,
0x5a, 0x0a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18,
0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76,
0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x68, 0x6f, 0x6f, 0x6b,
0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x18, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28,
0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x68, 0x6f, 0x6f, 0x6b, 0x43, 0x6f,
0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0x93, 0x0c, 0x0a, 0x06, 0x44, 0x6f, 0x63,
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e,
0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x68, 0x6f, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x6f, 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70,
0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x68, 0x6f, 0x6f, 0x6b, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xbd, 0x0b, 0x0a, 0x06, 0x44, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69,
0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
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,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 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, 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, 0x4a, 0x0a, 0x0d, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x32, 0x0a, 0x0d, 0x43,
0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x61,
0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x12,
0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x36, 0x0a, 0x09, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x15, 0x2e, 0x61,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x70, 0x69, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 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, 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, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x26,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
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 ( 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_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{ var file_api_pb_docker_proto_goTypes = []any{
(CreateServiceContainerRequest_ContainerType)(0), // 0: api.CreateServiceContainerRequest.ContainerType (CreateServiceContainerRequest_ContainerType)(0), // 0: api.CreateServiceContainerRequest.ContainerType
(*CreateContainerRequest)(nil), // 1: api.CreateContainerRequest (*CreateContainerRequest)(nil), // 1: api.CreateContainerRequest
@@ -2813,81 +2972,88 @@ var file_api_pb_docker_proto_goTypes = []any{
(*RemoveVolumeRequest)(nil), // 36: api.RemoveVolumeRequest (*RemoveVolumeRequest)(nil), // 36: api.RemoveVolumeRequest
(*CreateServiceContainerRequest)(nil), // 37: api.CreateServiceContainerRequest (*CreateServiceContainerRequest)(nil), // 37: api.CreateServiceContainerRequest
(*ServiceContainer)(nil), // 38: api.ServiceContainer (*ServiceContainer)(nil), // 38: api.ServiceContainer
(*ListServiceContainersRequest)(nil), // 39: api.ListServiceContainersRequest (*UpdateServiceContainerRequest)(nil), // 39: api.UpdateServiceContainerRequest
(*ListServiceContainersResponse)(nil), // 40: api.ListServiceContainersResponse (*Labels)(nil), // 40: api.Labels
(*MachineServiceContainers)(nil), // 41: api.MachineServiceContainers (*ListServiceContainersRequest)(nil), // 41: api.ListServiceContainersRequest
(*Metadata)(nil), // 42: api.Metadata (*ListServiceContainersResponse)(nil), // 42: api.ListServiceContainersResponse
(*LogsRequest)(nil), // 43: api.LogsRequest (*MachineServiceContainers)(nil), // 43: api.MachineServiceContainers
(*emptypb.Empty)(nil), // 44: google.protobuf.Empty nil, // 44: api.Labels.LabelsEntry
(*LogEntry)(nil), // 45: api.LogEntry (*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{ var file_api_pb_docker_proto_depIdxs = []int32{
9, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers 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 12, // 2: api.ExecContainerRequest.config:type_name -> api.ExecConfig
13, // 3: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent 13, // 3: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent
19, // 4: api.InspectImageResponse.messages:type_name -> api.Image 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 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 25, // 8: api.RemoveImageRequest.platforms:type_name -> api.ImagePlatform
27, // 9: api.RemoveImageResponse.messages:type_name -> api.MachineImageRemoval 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 28, // 11: api.MachineImageRemoval.results:type_name -> api.ImageRemoval
30, // 12: api.ListImagesResponse.messages:type_name -> api.MachineImages 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 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 0, // 16: api.CreateServiceContainerRequest.container_type:type_name -> api.CreateServiceContainerRequest.ContainerType
41, // 17: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers 40, // 17: api.UpdateServiceContainerRequest.service_labels:type_name -> api.Labels
42, // 18: api.MachineServiceContainers.metadata:type_name -> api.Metadata 44, // 18: api.Labels.labels:type_name -> api.Labels.LabelsEntry
38, // 19: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer 43, // 19: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers
38, // 20: api.MachineServiceContainers.hook_containers:type_name -> api.ServiceContainer 45, // 20: api.MachineServiceContainers.metadata:type_name -> api.Metadata
1, // 21: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest 38, // 21: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer
3, // 22: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest 38, // 22: api.MachineServiceContainers.hook_containers:type_name -> api.ServiceContainer
5, // 23: api.Docker.StartContainer:input_type -> api.StartContainerRequest 1, // 23: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest
6, // 24: api.Docker.StopContainer:input_type -> api.StopContainerRequest 3, // 24: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest
7, // 25: api.Docker.ListContainers:input_type -> api.ListContainersRequest 5, // 25: api.Docker.StartContainer:input_type -> api.StartContainerRequest
10, // 26: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest 6, // 26: api.Docker.StopContainer:input_type -> api.StopContainerRequest
11, // 27: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest 7, // 27: api.Docker.ListContainers:input_type -> api.ListContainersRequest
43, // 28: api.Docker.ContainerLogs:input_type -> api.LogsRequest 10, // 28: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest
15, // 29: api.Docker.PullImage:input_type -> api.PullImageRequest 11, // 29: api.Docker.ExecContainer:input_type -> api.ExecContainerRequest
17, // 30: api.Docker.InspectImage:input_type -> api.InspectImageRequest 46, // 30: api.Docker.ContainerLogs:input_type -> api.LogsRequest
20, // 31: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest 15, // 31: api.Docker.PullImage:input_type -> api.PullImageRequest
23, // 32: api.Docker.ListImages:input_type -> api.ListImagesRequest 17, // 32: api.Docker.InspectImage:input_type -> api.InspectImageRequest
24, // 33: api.Docker.RemoveImage:input_type -> api.RemoveImageRequest 20, // 33: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
31, // 34: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest 23, // 34: api.Docker.ListImages:input_type -> api.ListImagesRequest
33, // 35: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest 24, // 35: api.Docker.RemoveImage:input_type -> api.RemoveImageRequest
36, // 36: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest 31, // 36: api.Docker.CreateVolume:input_type -> api.CreateVolumeRequest
37, // 37: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest 33, // 37: api.Docker.ListVolumes:input_type -> api.ListVolumesRequest
3, // 38: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest 36, // 38: api.Docker.RemoveVolume:input_type -> api.RemoveVolumeRequest
39, // 39: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest 37, // 39: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
10, // 40: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest 3, // 40: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest
2, // 41: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse 39, // 41: api.Docker.UpdateServiceContainer:input_type -> api.UpdateServiceContainerRequest
4, // 42: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse 41, // 42: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest
44, // 43: api.Docker.StartContainer:output_type -> google.protobuf.Empty 10, // 43: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
44, // 44: api.Docker.StopContainer:output_type -> google.protobuf.Empty 2, // 44: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
8, // 45: api.Docker.ListContainers:output_type -> api.ListContainersResponse 4, // 45: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
44, // 46: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty 47, // 46: api.Docker.StartContainer:output_type -> google.protobuf.Empty
14, // 47: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse 47, // 47: api.Docker.StopContainer:output_type -> google.protobuf.Empty
45, // 48: api.Docker.ContainerLogs:output_type -> api.LogEntry 8, // 48: api.Docker.ListContainers:output_type -> api.ListContainersResponse
16, // 49: api.Docker.PullImage:output_type -> api.JSONMessage 47, // 49: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
18, // 50: api.Docker.InspectImage:output_type -> api.InspectImageResponse 14, // 50: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse
21, // 51: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse 48, // 51: api.Docker.ContainerLogs:output_type -> api.LogEntry
29, // 52: api.Docker.ListImages:output_type -> api.ListImagesResponse 16, // 52: api.Docker.PullImage:output_type -> api.JSONMessage
26, // 53: api.Docker.RemoveImage:output_type -> api.RemoveImageResponse 18, // 53: api.Docker.InspectImage:output_type -> api.InspectImageResponse
32, // 54: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse 21, // 54: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
34, // 55: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse 29, // 55: api.Docker.ListImages:output_type -> api.ListImagesResponse
44, // 56: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty 26, // 56: api.Docker.RemoveImage:output_type -> api.RemoveImageResponse
2, // 57: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse 32, // 57: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse
38, // 58: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer 34, // 58: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse
40, // 59: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse 47, // 59: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty
44, // 60: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty 2, // 60: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
41, // [41:61] is the sub-list for method output_type 38, // 61: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer
21, // [21:41] is the sub-list for method input_type 47, // 62: api.Docker.UpdateServiceContainer:output_type -> google.protobuf.Empty
21, // [21:21] is the sub-list for extension type_name 42, // 63: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse
21, // [21:21] is the sub-list for extension extendee 47, // 64: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty
0, // [0:21] is the sub-list for field type_name 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() } 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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: case 0:
return &v.state return &v.state
case 1: 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 { 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 { switch v := v.(*MachineServiceContainers); i {
case 0: case 0:
return &v.state return &v.state
@@ -3401,13 +3591,14 @@ func file_api_pb_docker_proto_init() {
(*ExecContainerResponse_Stderr)(nil), (*ExecContainerResponse_Stderr)(nil),
(*ExecContainerResponse_ExitCode)(nil), (*ExecContainerResponse_ExitCode)(nil),
} }
file_api_pb_docker_proto_msgTypes[38].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_pb_docker_proto_rawDesc, RawDescriptor: file_api_pb_docker_proto_rawDesc,
NumEnums: 1, NumEnums: 1,
NumMessages: 41, NumMessages: 44,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+19
View File
@@ -32,6 +32,8 @@ service Docker {
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse); rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer); 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 ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty); rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
} }
@@ -281,6 +283,23 @@ message ServiceContainer {
bytes service_spec = 2; 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 { message ListServiceContainersRequest {
string service_id = 1; string service_id = 1;
// JSON serialised container.ListOptions. // JSON serialised container.ListOptions.
+40
View File
@@ -38,6 +38,7 @@ const (
Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume" Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer"
Docker_UpdateServiceContainer_FullMethodName = "/api.Docker/UpdateServiceContainer"
Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" 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) RemoveVolume(ctx context.Context, in *RemoveVolumeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, 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) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error)
RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, 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 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) { func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListServiceContainersResponse) out := new(ListServiceContainersResponse)
@@ -323,6 +336,8 @@ type DockerServer interface {
RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error) RemoveVolume(context.Context, *RemoveVolumeRequest) (*emptypb.Empty, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, 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) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDockerServer() mustEmbedUnimplementedDockerServer()
@@ -389,6 +404,9 @@ func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *Create
func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) { func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) {
return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented") 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) { func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented") 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) 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) { func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListServiceContainersRequest) in := new(ListServiceContainersRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -818,6 +854,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "InspectServiceContainer", MethodName: "InspectServiceContainer",
Handler: _Docker_InspectServiceContainer_Handler, Handler: _Docker_InspectServiceContainer_Handler,
}, },
{
MethodName: "UpdateServiceContainer",
Handler: _Docker_UpdateServiceContainer_Handler,
},
{ {
MethodName: "ListServiceContainers", MethodName: "ListServiceContainers",
Handler: _Docker_ListServiceContainers_Handler, 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"], "container_name", e.Actor.Attributes["name"],
"action", e.Action) "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 { if err := c.syncContainersToStore(ctx); err != nil {
return fmt.Errorf("sync containers to cluster store: %w", err) 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{ config := &container.Config{
Cmd: spec.Container.Command, Cmd: spec.Container.Command,
Env: envVars.ToSlice(), Env: envVars.ToSlice(),
Entrypoint: spec.Container.Entrypoint, Entrypoint: spec.Container.Entrypoint,
Hostname: containerName, Hostname: containerName,
Image: spec.Container.Image, Image: spec.Container.Image,
Labels: map[string]string{ Labels: labels,
api.LabelServiceID: req.ServiceId, User: spec.Container.User,
api.LabelServiceName: spec.Name, Tty: spec.Container.Tty,
api.LabelServiceMode: spec.Mode, OpenStdin: spec.Container.OpenStdin,
api.LabelManaged: "",
},
User: spec.Container.User,
Tty: spec.Container.Tty,
OpenStdin: spec.Container.OpenStdin,
} }
if spec.Mode == "" { if spec.Mode == "" {
config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated 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. // 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 { if spec.PreDeploy != nil && req.ContainerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
config.Labels = map[string]string{ // Keep the user-defined labels but replace the service mode and ports labels with the hook label.
api.LabelServiceID: req.ServiceId, delete(config.Labels, api.LabelServiceMode)
api.LabelServiceName: spec.Name, delete(config.Labels, api.LabelServicePorts)
api.LabelHook: api.LabelHookPreDeploy, config.Labels[api.LabelHook] = api.LabelHookPreDeploy
api.LabelManaged: "",
}
config.Healthcheck = &container.HealthConfig{ config.Healthcheck = &container.HealthConfig{
Test: []string{"NONE"}, Test: []string{"NONE"},
} }
@@ -1179,6 +1182,36 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta
return resp, nil 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. // logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream.
const logsHeartbeatInterval = 200 * time.Millisecond const logsHeartbeatInterval = 200 * time.Millisecond
+105
View File
@@ -15,6 +15,7 @@ import (
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@@ -34,6 +35,9 @@ type Service struct {
Client client.APIClient Client client.APIClient
// db is a connection to the machine database. // db is a connection to the machine database.
db *sqlx.DB 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. // NewService creates a new Docker service instance.
@@ -41,9 +45,26 @@ func NewService(client client.APIClient, db *sqlx.DB) *Service {
return &Service{ return &Service{
Client: client, Client: client,
db: db, 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 // 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. // becomes reachable again after a disconnect, which typically indicates a restart.
func (s *Service) WatchDaemonRestart(ctx context.Context) <-chan struct{} { 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 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 // ListServiceContainersResult holds the result of listing service containers, split into regular
// service containers and one-off hook containers. // service containers and one-off hook containers.
type ListServiceContainersResult struct { type ListServiceContainersResult struct {
+1
View File
@@ -30,6 +30,7 @@ type ContainerClient interface {
StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error
StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error
RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error
UpdateContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts UpdateContainerOptions) error
WaitContainerHealthy( WaitContainerHealthy(
ctx context.Context, serviceNameOrID, containerNameOrID string, opts WaitContainerHealthyOptions, ctx context.Context, serviceNameOrID, containerNameOrID string, opts WaitContainerHealthyOptions,
) error ) error
+33
View File
@@ -19,6 +19,11 @@ const (
// in internal/machine/docker/manager.go. // in internal/machine/docker/manager.go.
DockerNetworkName = "uncloud" 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 marks system containers as managed by the Uncloud daemon.
LabelDaemonManaged = "uncloudd.managed" LabelDaemonManaged = "uncloudd.managed"
@@ -175,6 +180,19 @@ type CreateContainerResponse struct {
Name string 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 { type ServiceContainer struct {
Container Container
ServiceSpec ServiceSpec ServiceSpec ServiceSpec
@@ -195,6 +213,21 @@ func (c *ServiceContainer) ServiceName() string {
return c.Config.Labels[LabelServiceName] 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. // ServiceMode returns the replication mode of the service this container belongs to.
func (c *ServiceContainer) ServiceMode() string { func (c *ServiceContainer) ServiceMode() string {
return c.ServiceSpec.Mode return c.ServiceSpec.Mode
+33
View File
@@ -1,7 +1,11 @@
package api package api
import ( import (
"errors"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
) )
const ( const (
@@ -27,6 +31,35 @@ type ContainerResources struct {
Ulimits map[string]Ulimit 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. // DeviceMapping represents a device mapping between host and container.
type DeviceMapping struct { type DeviceMapping struct {
// HostPath is the path to the device on the host. // 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 Configs []ConfigSpec
// Container defines the desired state of each container in the service. // Container defines the desired state of each container in the service.
Container ContainerSpec 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 is the replication mode of the service. Default is ServiceModeReplicated if empty.
Mode string Mode string
Name string Name string
@@ -157,6 +160,9 @@ func (s *ServiceSpec) Validate() error {
if err := s.Container.Validate(); err != nil { if err := s.Container.Validate(); err != nil {
return err return err
} }
if err := ValidateLabels(s.Labels); err != nil {
return fmt.Errorf("invalid service labels: %w", err)
}
switch s.Mode { switch s.Mode {
case "", ServiceModeGlobal, ServiceModeReplicated: case "", ServiceModeGlobal, ServiceModeReplicated:
@@ -243,6 +249,7 @@ func (s *ServiceSpec) Clone() ServiceSpec {
spec.Caddy = &caddyCopy spec.Caddy = &caddyCopy
} }
spec.Container = s.Container.Clone() spec.Container = s.Container.Clone()
spec.Labels = maps.Clone(s.Labels)
spec.PreDeploy = s.PreDeploy.Clone() spec.PreDeploy = s.PreDeploy.Clone()
if s.Ports != nil { if s.Ports != nil {
@@ -279,6 +286,10 @@ type ContainerSpec struct {
Image string Image string
// Run a custom init inside the container. If nil, use the daemon's configured settings. // Run a custom init inside the container. If nil, use the daemon's configured settings.
Init *bool 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 overrides the default logging driver for the container. Each Docker daemon can have its own default.
LogDriver *LogDriver LogDriver *LogDriver
// PidMode sets the PID namespace mode for the container. Currently only "" or "host" is supported. // 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 { if _, err := reference.ParseDockerRef(s.Image); err != nil {
return fmt.Errorf("invalid image '%s': %w", s.Image, err) 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 { for _, m := range s.VolumeMounts {
if err := m.Validate(); err != nil { if err := m.Validate(); err != nil {
@@ -390,6 +404,7 @@ func (s *ContainerSpec) Clone() ContainerSpec {
hc.Test = slices.Clone(s.Healthcheck.Test) hc.Test = slices.Clone(s.Healthcheck.Test)
spec.Healthcheck = &hc spec.Healthcheck = &hc
} }
spec.Labels = maps.Clone(s.Labels)
if s.LogDriver != nil { if s.LogDriver != nil {
logDriver := *s.LogDriver logDriver := *s.LogDriver
if s.LogDriver.Options != nil { if s.LogDriver.Options != nil {
@@ -428,6 +443,20 @@ func (s *ContainerSpec) Clone() ContainerSpec {
return spec 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 type EnvVars map[string]string
// ToSlice converts the environment variables to a slice of strings in the format "key=value". // 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) { func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -267,8 +308,9 @@ func TestContainerSpec_Clone(t *testing.T) {
"FOO": "bar", "FOO": "bar",
"BAZ": "qux", "BAZ": "qux",
}, },
Image: "nginx:latest", Image: "nginx:latest",
Init: new(true), Init: new(true),
Labels: map[string]string{"com.example.team": "web"},
LogDriver: &LogDriver{ LogDriver: &LogDriver{
Name: "json-file", Name: "json-file",
Options: map[string]string{ Options: map[string]string{
@@ -313,6 +355,7 @@ func TestContainerSpec_Clone(t *testing.T) {
original.Command[0] = stringModified original.Command[0] = stringModified
original.Entrypoint[0] = stringModified original.Entrypoint[0] = stringModified
original.Env["FOO"] = stringModified original.Env["FOO"] = stringModified
original.Labels["com.example.team"] = stringModified
original.LogDriver.Options["max-size"] = stringModified original.LogDriver.Options["max-size"] = stringModified
original.Volumes[0] = stringModified original.Volumes[0] = stringModified
original.VolumeMounts[0].ContainerPath = 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, "NET_ADMIN", cloned.CapAdd[0])
assert.Equal(t, "ALL", cloned.CapDrop[0]) assert.Equal(t, "ALL", cloned.CapDrop[0])
assert.Equal(t, "sh", cloned.Command[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, "/bin/bash", cloned.Entrypoint[0])
assert.Equal(t, "bar", cloned.Env["FOO"]) assert.Equal(t, "bar", cloned.Env["FOO"])
assert.Equal(t, "qux", cloned.Env["BAZ"]) 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. // formatSummary counts all operations across the plan and renders the summary footer.
func (p *Plan) formatSummary() string { func (p *Plan) formatSummary() string {
var createCount, startFirstCount, stopFirstCount, removeCount int var createCount, startFirstCount, stopFirstCount, updateCount, removeCount int
machines := make(map[string]struct{}) machines := make(map[string]struct{})
for _, op := range p.Volumes { for _, op := range p.Volumes {
@@ -76,6 +76,9 @@ func (p *Plan) formatSummary() string {
} else { } else {
startFirstCount++ startFirstCount++
} }
case *operation.UpdateContainerOperation:
machines[o.MachineID] = struct{}{}
updateCount++
case *operation.RemoveContainerOperation: case *operation.RemoveContainerOperation:
machines[o.MachineID] = struct{}{} machines[o.MachineID] = struct{}{}
removeCount++ removeCount++
@@ -99,6 +102,10 @@ func (p *Plan) formatSummary() string {
parts = append(parts, parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)")) 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 { if removeCount > 0 {
parts = append(parts, parts = append(parts,
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove")) 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, warnCount: 1,
warnContains: []string{"container_name"}, 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", name: "deploy restart_policy",
composeYAML: `services: composeYAML: `services:
+8 -6
View File
@@ -66,6 +66,10 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
Mode: api.ServiceModeReplicated, 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. // Map x-caddy extension to spec.Caddy if specified.
if caddy, ok := service.Extensions[CaddyExtensionKey].(Caddy); ok && caddy.Config != "" { if caddy, ok := service.Extensions[CaddyExtensionKey].(Caddy); ok && caddy.Config != "" {
spec.Caddy = &api.CaddySpec{ spec.Caddy = &api.CaddySpec{
@@ -124,6 +128,10 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
d := time.Duration(cfg.Monitor) d := time.Duration(cfg.Monitor)
spec.UpdateConfig.MonitorPeriod = &d 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? // 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 { if service.DNSSearch != nil {
errs = append(errs, err(service.Name, "dns_search")) errs = append(errs, err(service.Name, "dns_search"))
} }
if service.Labels != nil {
errs = append(errs, err(service.Name, "labels"))
}
if service.Links != nil { if service.Links != nil {
errs = append(errs, err(service.Name, "links")) errs = append(errs, err(service.Name, "links"))
} }
@@ -517,9 +522,6 @@ func validateServicesFeatures(project *types.Project) []error {
if service.Deploy.RollbackConfig != nil { if service.Deploy.RollbackConfig != nil {
errs = append(errs, err(service.Name, "deploy rollback_config")) 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. // we only allow the 'default' network, nothing else.
if x := service.Networks; x != nil { 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) { func TestServiceSpecFromCompose_UpdateConfig(t *testing.T) {
t.Parallel() t.Parallel()
+33
View File
@@ -343,6 +343,39 @@ func (cli *Client) RemoveContainer(
return nil 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. // ExecContainer executes a command in a container within the service.
// If containerNameOrID is empty, the first container in the service will be used. // If containerNameOrID is empty, the first container in the service will be used.
func (cli *Client) ExecContainer( func (cli *Client) ExecContainer(
+58 -13
View File
@@ -1,7 +1,7 @@
package deploy package deploy
import ( import (
"reflect" "maps"
"sort" "sort"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -12,7 +12,8 @@ import (
type ContainerSpecStatus string type ContainerSpecStatus string
const ( 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" ContainerNeedsUpdate ContainerSpecStatus = "needs-update"
ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate" 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 current.Container.Resources.ValidateUpdateInPlace(newResources) != nil {
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) {
return ContainerNeedsRecreate return ContainerNeedsRecreate
} }
@@ -100,8 +93,10 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta
return ContainerNeedsRecreate return ContainerNeedsRecreate
} }
// Remaining resources are mutable. // CPU and memory limits are updated on the running container. Service labels are only stored in the service spec
if !reflect.DeepEqual(current.Container.Resources, newResources) { // and not applied to Docker containers.
if !current.Container.Resources.Equals(newResources) ||
!cmp.Equal(current.Labels, new.Labels, cmpopts.EquateEmpty()) {
return ContainerNeedsUpdate return ContainerNeedsUpdate
} }
@@ -119,3 +114,53 @@ func sortConfigs(configs []api.ConfigSpec) {
return configs[i].Name < configs[j].Name 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)) 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) { func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
t.Parallel() t.Parallel()
@@ -127,7 +344,7 @@ func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
CPU: 1000000000, CPU: 1000000000,
}, },
new: api.ContainerResources{}, new: api.ContainerResources{},
want: ContainerNeedsUpdate, want: ContainerNeedsRecreate,
}, },
// Memory // Memory
@@ -189,19 +406,33 @@ func TestEvalContainerSpecChange_ContainerResources(t *testing.T) {
name: "unset memory", name: "unset memory",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024}, new: api.ContainerResources{MemoryReservation: 50 * 1024 * 1024},
want: ContainerNeedsUpdate, want: ContainerNeedsRecreate,
}, },
{ {
name: "unset reservation", name: "unset reservation",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{Memory: 100 * 1024 * 1024}, new: api.ContainerResources{Memory: 100 * 1024 * 1024},
want: ContainerNeedsUpdate, want: ContainerNeedsRecreate,
}, },
{ {
name: "unset memory and reservation", name: "unset memory and reservation",
current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024}, current: api.ContainerResources{Memory: 100 * 1024 * 1024, MemoryReservation: 50 * 1024 * 1024},
new: api.ContainerResources{}, 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 // 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. // FormatSummary counts operations in the service plan and renders a styled summary line.
func (sp *ServicePlan) FormatSummary() string { func (sp *ServicePlan) FormatSummary() string {
var createCount, startFirstCount, stopFirstCount, removeCount int var createCount, startFirstCount, stopFirstCount, updateCount, removeCount int
machines := make(map[string]struct{}) machines := make(map[string]struct{})
for _, op := range sp.Operations { for _, op := range sp.Operations {
@@ -188,6 +188,9 @@ func (sp *ServicePlan) FormatSummary() string {
} else { } else {
startFirstCount++ startFirstCount++
} }
case *operation.UpdateContainerOperation:
machines[o.MachineID] = struct{}{}
updateCount++
case *operation.RemoveContainerOperation: case *operation.RemoveContainerOperation:
machines[o.MachineID] = struct{}{} machines[o.MachineID] = struct{}{}
removeCount++ removeCount++
@@ -210,6 +213,10 @@ func (sp *ServicePlan) FormatSummary() string {
parts = append(parts, parts = append(parts,
tui.BoldYellow.Render(strconv.Itoa(stopFirstCount))+" "+tui.Yellow.Render("replace (stop-first)")) 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 { if removeCount > 0 {
parts = append(parts, parts = append(parts,
tui.BoldRed.Render(strconv.Itoa(removeCount))+" "+tui.Red.Render("remove")) 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. // Organise existing containers by machine.
containersOnMachine := make(map[string][]api.ServiceContainer) containersOnMachine := make(map[string][]api.ServiceContainer)
upToDateContainersOnMachine := make(map[string]int) reusableContainersOnMachine := make(map[string]int)
containerSpecStatuses := make(map[string]ContainerSpecStatus) containerSpecStatuses := make(map[string]ContainerSpecStatus)
if svc != nil { if svc != nil {
for _, c := range svc.Containers { for _, c := range svc.Containers {
@@ -105,40 +105,50 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec)
if s.ForceRecreate { if s.ForceRecreate {
status = ContainerNeedsRecreate status = ContainerNeedsRecreate
} else { } else {
status = EvalContainerSpecChange(c.Container.ServiceSpec, spec) status = EvalContainerChange(c.Container, spec)
} }
containerSpecStatuses[c.Container.ID] = status containerSpecStatuses[c.Container.ID] = status
if status == ContainerUpToDate { if status != ContainerNeedsRecreate {
upToDateContainersOnMachine[c.MachineID] += 1 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 { 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 return -1
} default:
if status, ok := containerSpecStatuses[c2.Container.ID]; ok && status == ContainerUpToDate {
return 1 return 1
} }
return 0
}) })
for _, c := range svc.Containers { for _, c := range svc.Containers {
containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c.Container) 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 // Sort machines such that machines with the most containers that are up to date or can be updated in place
// existing containers, and finally machines without containers. // are first, followed by machines with existing containers, and finally machines without containers.
slices.SortFunc(matchedMachines, func(m1, m2 *pb.MachineInfo) int { slices.SortFunc(matchedMachines, func(m1, m2 *pb.MachineInfo) int {
if upToDateContainersOnMachine[m1.Id] > 0 && upToDateContainersOnMachine[m2.Id] > 0 { if reusableContainersOnMachine[m1.Id] > 0 && reusableContainersOnMachine[m2.Id] > 0 {
return upToDateContainersOnMachine[m2.Id] - upToDateContainersOnMachine[m1.Id] return reusableContainersOnMachine[m2.Id] - reusableContainersOnMachine[m1.Id]
} }
if upToDateContainersOnMachine[m1.Id] > 0 { if reusableContainersOnMachine[m1.Id] > 0 {
return -1 return -1
} }
if upToDateContainersOnMachine[m2.Id] > 0 { if reusableContainersOnMachine[m2.Id] > 0 {
return 1 return 1
} }
return len(containersOnMachine[m2.Id]) - len(containersOnMachine[m1.Id]) 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 { if status == ContainerUpToDate {
continue 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. // 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. // 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 { for i, c := range containers {
if !c.Container.State.Running || c.Container.State.Paused { if !c.Container.State.Running || c.Container.State.Paused {
// Skip containers that are not running. // Skip containers that are not running.
@@ -312,12 +331,21 @@ func reconcileGlobalContainer(
if forceRecreate { if forceRecreate {
status = ContainerNeedsRecreate status = ContainerNeedsRecreate
} else { } else {
status = EvalContainerSpecChange(c.Container.ServiceSpec, spec) status = EvalContainerChange(c.Container, spec)
} }
if status == ContainerUpToDate { if status != ContainerNeedsRecreate {
// The container is already running with the same spec. // The container is already running with the desired spec or can be updated in place to it.
upToDate = true 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 { for j, old := range containers {
if i == j { if i == j {
continue continue
@@ -331,10 +359,8 @@ func reconcileGlobalContainer(
} }
break 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 return ops, nil
} }
+135 -1
View File
@@ -1,6 +1,8 @@
package deploy package deploy
import ( import (
"slices"
"strings"
"testing" "testing"
"github.com/docker/docker/api/types/container" "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/deploy/scheduler"
"github.com/psviderski/uncloud/pkg/client/operation" "github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestDetermineUpdateOrder(t *testing.T) { 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) { func TestPreDeployOperations(t *testing.T) {
hook := &api.PreDeployHook{ hook := &api.PreDeployHook{
Command: []string{"db", "migrate"}, 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", name: "plan has only RemoveContainerOperation",
plan: ServicePlan{ plan: ServicePlan{
@@ -614,7 +746,8 @@ func TestPreDeployOperations(t *testing.T) {
}, },
svc: &api.Service{ svc: &api.Service{
HookContainers: []api.MachineServiceContainer{ 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-1", Container: runningHook1},
{MachineID: "m-3", Container: runningHook2}, {MachineID: "m-3", Container: runningHook2},
}, },
@@ -660,6 +793,7 @@ func newServiceContainer(id string, state container.State) api.ServiceContainer
ID: id, ID: id,
State: &state, 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) 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. // stopOptions converts a stop grace period duration to Docker container stop options.
func stopOptions(gracePeriod *time.Duration) container.StopOptions { func stopOptions(gracePeriod *time.Duration) container.StopOptions {
if gracePeriod == nil { if gracePeriod == nil {
+7
View File
@@ -123,6 +123,13 @@ func (c *recordingClient) RemoveContainer(
return nil 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( func (c *recordingClient) CreatePreDeployHookContainer(
context.Context, string, api.ServiceSpec, string, context.Context, string, api.ServiceSpec, string,
) (api.CreateContainerResponse, error) { ) (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) { func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
desiredSpec := spec.SetDefaults() desiredSpec := spec.SetDefaults()
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, desiredSpec) status := deploy.EvalContainerChange(ctr, desiredSpec)
assert.Equal(t, deploy.ContainerUpToDate, status) assert.Equal(t, deploy.ContainerUpToDate, status)
spec = renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name) 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.Name, ctr.Config.Labels[api.LabelServiceName])
assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode]) assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode])
assert.Contains(t, ctr.Config.Labels, api.LabelManaged) 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. // Command and Entrypoint can only be compared if they are set in the spec.
// Otherwise, the container takes them from the image. // Otherwise, the container takes them from the image.
+82
View File
@@ -2,7 +2,9 @@ package e2e
import ( import (
"context" "context"
"fmt"
"testing" "testing"
"time"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
@@ -10,6 +12,7 @@ import (
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/compose" "github.com/psviderski/uncloud/pkg/client/compose"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/psviderski/uncloud/pkg/client/operation"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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") "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 // Catches regression: https://github.com/psviderski/uncloud/issues/176
t.Run("plan new deployment with volumes and recreate strategy", func(t *testing.T) { t.Run("plan new deployment with volumes and recreate strategy", func(t *testing.T) {
t.Parallel() t.Parallel()
+84
View File
@@ -1761,6 +1761,90 @@ func TestServiceLifecycle(t *testing.T) {
require.ErrorIs(t, err, api.ErrNotFound) 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.Run("1 replica", func(t *testing.T) {
t.Parallel() 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 | | Feature | Support Status | Notes |
|----------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| **Services** | | | | **Services** | | |
| `build` | ✅ Supported | Build context and Dockerfile | | `build` | ✅ Supported | Build context and Dockerfile |
| `cap_add` | ✅ Supported | Additional kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) | | `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 | | `cap_drop` | ✅ Supported | Which kernel [capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) to drop |
| `command` | ✅ Supported | Override container command | | `command` | ✅ Supported | Override container command |
| `configs` | ✅ Supported | File-based and inline configs | | `configs` | ✅ Supported | File-based and inline configs |
| `cpus` | ✅ Supported | CPU limit | | `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 | | `devices` | ✅ Supported | Device mappings |
| `dns` | ❌ Not supported | Built-in service discovery | | `dns` | ❌ Not supported | Built-in service discovery |
| `dns_search` | ❌ 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 | | `healthcheck` | ✅ Supported | Health check configuration |
| `image` | ✅ Supported | Container image specification | | `image` | ✅ Supported | Container image specification |
| `init` | ✅ Supported | Run init process in container | | `init` | ✅ Supported | Run init process in container |
| `labels` | ❌ Not supported | | | `labels` | ✅ Supported | Docker labels on containers |
| `links` | ❌ Not supported | Use service names for communication | | `links` | ❌ Not supported | Use service names for communication |
| `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver | | `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver |
| `mem_limit` | ✅ Supported | Memory limit | | `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 | | | `memswap_limit` | ❌ Not supported | |
| `networks` | ❌ Not supported | All containers share cluster network | | `networks` | ❌ Not supported | All containers share cluster network |
| `pid` | ✅ Supported | Set the PID namespace mode, `pid: host` only | | `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 | | `privileged` | ✅ Supported | Run containers in privileged mode |
| `pull_policy` | ✅ Supported | `always`, `missing`, `never` | | `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 | | | `security_opt` | ❌ Not supported | |
| `shm_size` | ✅ Supported | Shared memory size | | `shm_size` | ✅ Supported | Shared memory size |
| `stdin_open` | ✅ Supported | Allocate standard input and keep it open | | `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 | | `ulimits` | ✅ Supported | Resource limits |
| `user` | ✅ Supported | Set container user | | `user` | ✅ Supported | Set container user |
| `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | | `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs |
| **Deploy** | | | | **Deploy** | | |
| `labels` | ❌ Not supported | | | `labels` | ✅ Supported | Service labels, not applied to containers. Use `services.<name>.labels` to set Docker labels on containers |
| `mode` | ✅ Supported | Either `global` or `replicated` | | `mode` | ✅ Supported | Either `global` or `replicated` |
| `placement` | ❌ Not supported | Use [`x-machines`](2-extensions.md#x-machines) extension | | `placement` | ❌ Not supported | Use [`x-machines`](2-extensions.md#x-machines) extension |
| `replicas` | ✅ Supported | Number of container replicas | | `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` | | `restart_policy` | ❌ Not supported | Defaults to `unless-stopped` |
| `rollback_config` | ❌ Not supported | See [#151](https://github.com/psviderski/uncloud/issues/151) | | `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) | | `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
| **Volumes** | | | | **Volumes** | | |
| Named volumes | ✅ Supported | Docker volumes | | Named volumes | ✅ Supported | Docker volumes |
| Bind mounts | ✅ Supported | Host path binding with optional [runtime templates](../3-concepts/6-services/3-runtime-templates.md) | | Bind mounts | ✅ Supported | Host path binding with optional [runtime templates](../3-concepts/6-services/3-runtime-templates.md) |
| Tmpfs mounts | ✅ Supported | In-memory filesystems | | Tmpfs mounts | ✅ Supported | In-memory filesystems |
| Volume labels | ✅ Supported | Custom labels | | Volume labels | ✅ Supported | Custom labels |
| External volumes | ✅ Supported | Must exist before deployment | | 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 | | [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 | | File-based configs | ✅ Supported | Read from file |
| Inline configs | ✅ Supported | Defined in compose file | | Inline configs | ✅ Supported | Defined in compose file |
| External configs | ❌ Not supported | Not supported | | External configs | ❌ Not supported | Not supported |
| Short syntax | ❌ Not supported | Use long syntax only | | Short syntax | ❌ Not supported | Use long syntax only |
| **Extensions** | | | | **Extensions** | | |
| `x-context` | ✅ Uncloud-specific | Cluster context override | | `x-context` | ✅ Uncloud-specific | Cluster context override |
| `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | | `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration |
| `x-machines` | ✅ Uncloud-specific | Machine placement constraints | | `x-machines` | ✅ Uncloud-specific | Machine placement constraints |