mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 04:34:17 +00:00
feat(labels): support service and container labels, update labels and cpu/memory in-place
This commit is contained in:
+381
-190
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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"])
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user