diff --git a/cmd/uncloud/service/scale.go b/cmd/uncloud/service/scale.go index bd700ed3..80c55ebf 100644 --- a/cmd/uncloud/service/scale.go +++ b/cmd/uncloud/service/scale.go @@ -3,12 +3,13 @@ package service import ( "context" "fmt" + "strconv" + "github.com/charmbracelet/huh" "github.com/docker/compose/v2/pkg/progress" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/pkg/api" "github.com/spf13/cobra" - "strconv" ) type scaleOptions struct { @@ -81,12 +82,7 @@ func scale(ctx context.Context, uncli *cli.CLI, opts scaleOptions) error { // TODO: Check if all containers have the same spec. If not, prompt user to choose which one to scale. // This can happen if a service deployment failed midway and some containers were not updated. - // Derive the service spec from the first container. - spec, err := svc.Containers[0].Container.ServiceSpec() - if err != nil { - return fmt.Errorf("get service spec from container: %w", err) - } - + spec := svc.Containers[0].Container.ServiceSpec spec.Replicas = opts.replicas deployment := clusterClient.NewDeployment(spec, nil) plan, err := deployment.Plan(ctx) diff --git a/internal/machine/api/pb/docker.pb.go b/internal/machine/api/pb/docker.pb.go index af672504..eddfdfdc 100644 --- a/internal/machine/api/pb/docker.pb.go +++ b/internal/machine/api/pb/docker.pb.go @@ -26,13 +26,13 @@ type CreateContainerRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // JSON serialized container.Config. + // JSON serialised container.Config. Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - // JSON serialized container.HostConfig. + // JSON serialised container.HostConfig. HostConfig []byte `protobuf:"bytes,2,opt,name=host_config,json=hostConfig,proto3" json:"host_config,omitempty"` - // JSON serialized network.NetworkingConfig. + // JSON serialised network.NetworkingConfig. NetworkConfig []byte `protobuf:"bytes,3,opt,name=network_config,json=networkConfig,proto3" json:"network_config,omitempty"` - // JSON serialized ocispec.Platform. + // JSON serialised ocispec.Platform. Platform []byte `protobuf:"bytes,4,opt,name=platform,proto3" json:"platform,omitempty"` Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` } @@ -109,7 +109,7 @@ type CreateContainerResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // JSON serialized container.CreateResponse. + // JSON serialised container.CreateResponse. Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` } @@ -204,7 +204,7 @@ type InspectContainerResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // JSON serialized container.InspectResponse. + // JSON serialised container.InspectResponse. Response []byte `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"` } @@ -253,7 +253,7 @@ type StartContainerRequest struct { unknownFields protoimpl.UnknownFields Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // JSON serialized container.StartOptions. + // JSON serialised container.StartOptions. Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` } @@ -309,7 +309,7 @@ type StopContainerRequest struct { unknownFields protoimpl.UnknownFields Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // JSON serialized container.StopOptions. + // JSON serialised container.StopOptions. Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` } @@ -364,7 +364,7 @@ type ListContainersRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // JSON serialized container.ListOptions. + // JSON serialised container.ListOptions. Options []byte `protobuf:"bytes,1,opt,name=options,proto3" json:"options,omitempty"` } @@ -461,7 +461,7 @@ type MachineContainers struct { unknownFields protoimpl.UnknownFields Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // JSON serialized []container.ContainerJSON. + // JSON serialised []container.InspectResponse. Containers []byte `protobuf:"bytes,2,opt,name=containers,proto3" json:"containers,omitempty"` } @@ -517,7 +517,7 @@ type RemoveContainerRequest struct { unknownFields protoimpl.UnknownFields Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // JSON serialized container.RemoveOptions. + // JSON serialised container.RemoveOptions. Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` } @@ -573,7 +573,7 @@ type PullImageRequest struct { unknownFields protoimpl.UnknownFields Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"` - // JSON serialized image.PullOptions. + // JSON serialised image.PullOptions. Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` } @@ -628,7 +628,7 @@ type JSONMessage struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // JSON serialized jsonmessage.JSONMessage. + // JSON serialised jsonmessage.JSONMessage. Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` } @@ -772,7 +772,7 @@ type Image struct { unknownFields protoimpl.UnknownFields Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // JSON serialized types.ImageInspect. + // JSON serialised types.ImageInspect. Image []byte `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` } @@ -1046,6 +1046,223 @@ func (x *CreateServiceContainerRequest) GetContainerName() string { return "" } +type ServiceContainer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // JSON serialised container.InspectResponse. + Container []byte `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"` + // JSON serialised api.ServiceSpec. + ServiceSpec []byte `protobuf:"bytes,2,opt,name=service_spec,json=serviceSpec,proto3" json:"service_spec,omitempty"` +} + +func (x *ServiceContainer) Reset() { + *x = ServiceContainer{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServiceContainer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceContainer) ProtoMessage() {} + +func (x *ServiceContainer) ProtoReflect() protoreflect.Message { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[19] + 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 ServiceContainer.ProtoReflect.Descriptor instead. +func (*ServiceContainer) Descriptor() ([]byte, []int) { + return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{19} +} + +func (x *ServiceContainer) GetContainer() []byte { + if x != nil { + return x.Container + } + return nil +} + +func (x *ServiceContainer) GetServiceSpec() []byte { + if x != nil { + return x.ServiceSpec + } + return nil +} + +type ListServiceContainersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceId string `protobuf:"bytes,1,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + // JSON serialised container.ListOptions. + Options []byte `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` +} + +func (x *ListServiceContainersRequest) Reset() { + *x = ListServiceContainersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListServiceContainersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServiceContainersRequest) ProtoMessage() {} + +func (x *ListServiceContainersRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[20] + 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 ListServiceContainersRequest.ProtoReflect.Descriptor instead. +func (*ListServiceContainersRequest) Descriptor() ([]byte, []int) { + return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{20} +} + +func (x *ListServiceContainersRequest) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *ListServiceContainersRequest) GetOptions() []byte { + if x != nil { + return x.Options + } + return nil +} + +type ListServiceContainersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests + // to multiple machines. + Messages []*MachineServiceContainers `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` +} + +func (x *ListServiceContainersResponse) Reset() { + *x = ListServiceContainersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListServiceContainersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServiceContainersResponse) ProtoMessage() {} + +func (x *ListServiceContainersResponse) ProtoReflect() protoreflect.Message { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[21] + 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 ListServiceContainersResponse.ProtoReflect.Descriptor instead. +func (*ListServiceContainersResponse) Descriptor() ([]byte, []int) { + return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{21} +} + +func (x *ListServiceContainersResponse) GetMessages() []*MachineServiceContainers { + if x != nil { + return x.Messages + } + return nil +} + +type MachineServiceContainers struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Containers []*ServiceContainer `protobuf:"bytes,2,rep,name=containers,proto3" json:"containers,omitempty"` +} + +func (x *MachineServiceContainers) Reset() { + *x = MachineServiceContainers{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MachineServiceContainers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineServiceContainers) ProtoMessage() {} + +func (x *MachineServiceContainers) ProtoReflect() protoreflect.Message { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[22] + 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 MachineServiceContainers.ProtoReflect.Descriptor instead. +func (*MachineServiceContainers) Descriptor() ([]byte, []int) { + return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{22} +} + +func (x *MachineServiceContainers) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *MachineServiceContainers) GetContainers() []*ServiceContainer { + if x != nil { + return x.Containers + } + return nil +} + var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ @@ -1144,63 +1361,99 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x32, 0xc3, 0x06, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, - 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, - 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, - 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, - 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x61, 0x6d, 0x65, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x22, 0x57, 0x0a, 0x1c, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x65, 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, 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, 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, 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, 0x4d, 0x0a, 0x16, 0x52, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x73, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x7c, 0x0a, + 0x18, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x29, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x35, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x32, 0xf3, 0x07, 0x0a, 0x06, + 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, + 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, + 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, + 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 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, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x21, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, + 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, + 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -1215,7 +1468,7 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte { return file_internal_machine_api_pb_docker_proto_rawDescData } -var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_internal_machine_api_pb_docker_proto_goTypes = []any{ (*CreateContainerRequest)(nil), // 0: api.CreateContainerRequest (*CreateContainerResponse)(nil), // 1: api.CreateContainerResponse @@ -1236,43 +1489,54 @@ var file_internal_machine_api_pb_docker_proto_goTypes = []any{ (*InspectRemoteImageResponse)(nil), // 16: api.InspectRemoteImageResponse (*RemoteImage)(nil), // 17: api.RemoteImage (*CreateServiceContainerRequest)(nil), // 18: api.CreateServiceContainerRequest - (*Metadata)(nil), // 19: api.Metadata - (*emptypb.Empty)(nil), // 20: google.protobuf.Empty + (*ServiceContainer)(nil), // 19: api.ServiceContainer + (*ListServiceContainersRequest)(nil), // 20: api.ListServiceContainersRequest + (*ListServiceContainersResponse)(nil), // 21: api.ListServiceContainersResponse + (*MachineServiceContainers)(nil), // 22: api.MachineServiceContainers + (*Metadata)(nil), // 23: api.Metadata + (*emptypb.Empty)(nil), // 24: google.protobuf.Empty } var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{ 8, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers - 19, // 1: api.MachineContainers.metadata:type_name -> api.Metadata + 23, // 1: api.MachineContainers.metadata:type_name -> api.Metadata 14, // 2: api.InspectImageResponse.messages:type_name -> api.Image - 19, // 3: api.Image.metadata:type_name -> api.Metadata + 23, // 3: api.Image.metadata:type_name -> api.Metadata 17, // 4: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage - 19, // 5: api.RemoteImage.metadata:type_name -> api.Metadata - 0, // 6: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest - 2, // 7: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest - 4, // 8: api.Docker.StartContainer:input_type -> api.StartContainerRequest - 5, // 9: api.Docker.StopContainer:input_type -> api.StopContainerRequest - 6, // 10: api.Docker.ListContainers:input_type -> api.ListContainersRequest - 9, // 11: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest - 10, // 12: api.Docker.PullImage:input_type -> api.PullImageRequest - 12, // 13: api.Docker.InspectImage:input_type -> api.InspectImageRequest - 15, // 14: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest - 18, // 15: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest - 9, // 16: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest - 1, // 17: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse - 3, // 18: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse - 20, // 19: api.Docker.StartContainer:output_type -> google.protobuf.Empty - 20, // 20: api.Docker.StopContainer:output_type -> google.protobuf.Empty - 7, // 21: api.Docker.ListContainers:output_type -> api.ListContainersResponse - 20, // 22: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty - 11, // 23: api.Docker.PullImage:output_type -> api.JSONMessage - 13, // 24: api.Docker.InspectImage:output_type -> api.InspectImageResponse - 16, // 25: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse - 1, // 26: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse - 20, // 27: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty - 17, // [17:28] is the sub-list for method output_type - 6, // [6:17] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 23, // 5: api.RemoteImage.metadata:type_name -> api.Metadata + 22, // 6: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers + 23, // 7: api.MachineServiceContainers.metadata:type_name -> api.Metadata + 19, // 8: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer + 0, // 9: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest + 2, // 10: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest + 4, // 11: api.Docker.StartContainer:input_type -> api.StartContainerRequest + 5, // 12: api.Docker.StopContainer:input_type -> api.StopContainerRequest + 6, // 13: api.Docker.ListContainers:input_type -> api.ListContainersRequest + 9, // 14: api.Docker.RemoveContainer:input_type -> api.RemoveContainerRequest + 10, // 15: api.Docker.PullImage:input_type -> api.PullImageRequest + 12, // 16: api.Docker.InspectImage:input_type -> api.InspectImageRequest + 15, // 17: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest + 18, // 18: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest + 2, // 19: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest + 20, // 20: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest + 9, // 21: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest + 1, // 22: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse + 3, // 23: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse + 24, // 24: api.Docker.StartContainer:output_type -> google.protobuf.Empty + 24, // 25: api.Docker.StopContainer:output_type -> google.protobuf.Empty + 7, // 26: api.Docker.ListContainers:output_type -> api.ListContainersResponse + 24, // 27: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty + 11, // 28: api.Docker.PullImage:output_type -> api.JSONMessage + 13, // 29: api.Docker.InspectImage:output_type -> api.InspectImageResponse + 16, // 30: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse + 1, // 31: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse + 19, // 32: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer + 21, // 33: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse + 24, // 34: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty + 22, // [22:35] is the sub-list for method output_type + 9, // [9:22] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_internal_machine_api_pb_docker_proto_init() } @@ -1510,6 +1774,54 @@ func file_internal_machine_api_pb_docker_proto_init() { return nil } } + file_internal_machine_api_pb_docker_proto_msgTypes[19].Exporter = func(v any, i int) any { + switch v := v.(*ServiceContainer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_machine_api_pb_docker_proto_msgTypes[20].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_internal_machine_api_pb_docker_proto_msgTypes[21].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_internal_machine_api_pb_docker_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*MachineServiceContainers); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1517,7 +1829,7 @@ func file_internal_machine_api_pb_docker_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_machine_api_pb_docker_proto_rawDesc, NumEnums: 0, - NumMessages: 19, + NumMessages: 23, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/machine/api/pb/docker.proto b/internal/machine/api/pb/docker.proto index 9d468572..a423fc5a 100644 --- a/internal/machine/api/pb/docker.proto +++ b/internal/machine/api/pb/docker.proto @@ -21,23 +21,25 @@ service Docker { rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse); rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse); + rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer); + rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse); rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty); } message CreateContainerRequest { - // JSON serialized container.Config. + // JSON serialised container.Config. bytes config = 1; - // JSON serialized container.HostConfig. + // JSON serialised container.HostConfig. bytes host_config = 2; - // JSON serialized network.NetworkingConfig. + // JSON serialised network.NetworkingConfig. bytes network_config = 3; - // JSON serialized ocispec.Platform. + // JSON serialised ocispec.Platform. bytes platform = 4; string name = 5; } message CreateContainerResponse { - // JSON serialized container.CreateResponse. + // JSON serialised container.CreateResponse. bytes response = 1; } @@ -46,24 +48,24 @@ message InspectContainerRequest { } message InspectContainerResponse { - // JSON serialized container.InspectResponse. + // JSON serialised container.InspectResponse. bytes response = 1; } message StartContainerRequest { string id = 1; - // JSON serialized container.StartOptions. + // JSON serialised container.StartOptions. bytes options = 2; } message StopContainerRequest { string id = 1; - // JSON serialized container.StopOptions. + // JSON serialised container.StopOptions. bytes options = 2; } message ListContainersRequest { - // JSON serialized container.ListOptions. + // JSON serialised container.ListOptions. bytes options = 1; } @@ -74,24 +76,24 @@ message ListContainersResponse { message MachineContainers { Metadata metadata = 1; - // JSON serialized []container.ContainerJSON. + // JSON serialised []container.InspectResponse. bytes containers = 2; } message RemoveContainerRequest { string id = 1; - // JSON serialized container.RemoveOptions. + // JSON serialised container.RemoveOptions. bytes options = 2; } message PullImageRequest { string image = 1; - // JSON serialized image.PullOptions. + // JSON serialised image.PullOptions. bytes options = 2; } message JSONMessage { - // JSON serialized jsonmessage.JSONMessage. + // JSON serialised jsonmessage.JSONMessage. bytes message = 1; } @@ -106,7 +108,7 @@ message InspectImageResponse { message Image { Metadata metadata = 1; - // JSON serialized types.ImageInspect. + // JSON serialised types.ImageInspect. bytes image = 2; } @@ -133,3 +135,27 @@ message CreateServiceContainerRequest { bytes service_spec = 2; string container_name = 3; } + +message ServiceContainer { + // JSON serialised container.InspectResponse. + bytes container = 1; + // JSON serialised api.ServiceSpec. + bytes service_spec = 2; +} + +message ListServiceContainersRequest { + string service_id = 1; + // JSON serialised container.ListOptions. + bytes options = 2; +} + +message ListServiceContainersResponse { + // Must contain only one repeated messages field to allow broadcasting ListServiceContainers requests + // to multiple machines. + repeated MachineServiceContainers messages = 1; +} + +message MachineServiceContainers { + Metadata metadata = 1; + repeated ServiceContainer containers = 2; +} diff --git a/internal/machine/api/pb/docker_grpc.pb.go b/internal/machine/api/pb/docker_grpc.pb.go index 861fb33c..4435ae90 100644 --- a/internal/machine/api/pb/docker_grpc.pb.go +++ b/internal/machine/api/pb/docker_grpc.pb.go @@ -20,17 +20,19 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer" - Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer" - Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer" - Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer" - Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers" - Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer" - Docker_PullImage_FullMethodName = "/api.Docker/PullImage" - Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" - Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" - Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" - Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" + Docker_CreateContainer_FullMethodName = "/api.Docker/CreateContainer" + Docker_InspectContainer_FullMethodName = "/api.Docker/InspectContainer" + Docker_StartContainer_FullMethodName = "/api.Docker/StartContainer" + Docker_StopContainer_FullMethodName = "/api.Docker/StopContainer" + Docker_ListContainers_FullMethodName = "/api.Docker/ListContainers" + Docker_RemoveContainer_FullMethodName = "/api.Docker/RemoveContainer" + Docker_PullImage_FullMethodName = "/api.Docker/PullImage" + Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" + Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" + Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" + Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" + Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" + Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" ) // DockerClient is the client API for Docker service. @@ -49,6 +51,8 @@ type DockerClient interface { // Docker auth credentials if necessary. InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) + InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) + ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } @@ -169,6 +173,26 @@ func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateSer return out, nil } +func (c *dockerClient) InspectServiceContainer(ctx context.Context, in *InspectContainerRequest, opts ...grpc.CallOption) (*ServiceContainer, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ServiceContainer) + err := c.cc.Invoke(ctx, Docker_InspectServiceContainer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dockerClient) ListServiceContainers(ctx context.Context, in *ListServiceContainersRequest, opts ...grpc.CallOption) (*ListServiceContainersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListServiceContainersResponse) + err := c.cc.Invoke(ctx, Docker_ListServiceContainers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) @@ -195,6 +219,8 @@ type DockerServer interface { // Docker auth credentials if necessary. InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) + InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) + ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) mustEmbedUnimplementedDockerServer() } @@ -236,6 +262,12 @@ func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRem func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented") } +func (UnimplementedDockerServer) InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) { + return nil, status.Errorf(codes.Unimplemented, "method InspectServiceContainer not implemented") +} +func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListServiceContainers not implemented") +} func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented") } @@ -433,6 +465,42 @@ func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Docker_InspectServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InspectContainerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DockerServer).InspectServiceContainer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Docker_InspectServiceContainer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DockerServer).InspectServiceContainer(ctx, req.(*InspectContainerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Docker_ListServiceContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListServiceContainersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DockerServer).ListServiceContainers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Docker_ListServiceContainers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DockerServer).ListServiceContainers(ctx, req.(*ListServiceContainersRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RemoveContainerRequest) if err := dec(in); err != nil { @@ -494,6 +562,14 @@ var Docker_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateServiceContainer", Handler: _Docker_CreateServiceContainer_Handler, }, + { + MethodName: "InspectServiceContainer", + Handler: _Docker_InspectServiceContainer_Handler, + }, + { + MethodName: "ListServiceContainers", + Handler: _Docker_ListServiceContainers_Handler, + }, { MethodName: "RemoveServiceContainer", Handler: _Docker_RemoveServiceContainer_Handler, diff --git a/internal/machine/caddyfile/config.go b/internal/machine/caddyfile/config.go index 82c94d61..d73e7645 100644 --- a/internal/machine/caddyfile/config.go +++ b/internal/machine/caddyfile/config.go @@ -4,21 +4,22 @@ import ( "encoding/json" "errors" "fmt" - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" - "github.com/psviderski/uncloud/internal/machine/docker" - "github.com/psviderski/uncloud/pkg/api" "log/slog" "maps" "net" "net/http" "slices" "strconv" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" + "github.com/psviderski/uncloud/internal/machine/docker" + "github.com/psviderski/uncloud/pkg/api" ) -func GenerateConfig(containers []api.Container, verifyResponse string) (*caddy.Config, error) { +func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) { // Maps hostnames to lists of upstreams (container IP:port pairs). httpHostUpstreams := make(map[string][]string) httpsHostUpstreams := make(map[string][]string) diff --git a/internal/machine/caddyfile/config_test.go b/internal/machine/caddyfile/config_test.go index eeed638d..273c3a5f 100644 --- a/internal/machine/caddyfile/config_test.go +++ b/internal/machine/caddyfile/config_test.go @@ -1,6 +1,9 @@ package caddyfile import ( + "strings" + "testing" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" @@ -8,20 +11,18 @@ import ( "github.com/psviderski/uncloud/pkg/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "strings" - "testing" ) func TestGenerateConfig(t *testing.T) { tests := []struct { name string - containers []api.Container + containers []api.ServiceContainer want string wantErr bool }{ { name: "empty containers", - containers: []api.Container{}, + containers: []api.ServiceContainer{}, want: `{ "servers": { "http": { @@ -45,7 +46,7 @@ func TestGenerateConfig(t *testing.T) { { name: "HTTP container", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "app.example.com:8080/http"), }, want: `{ @@ -79,7 +80,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "load balancing multiple containers", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "app.example.com:8080/http"), newContainer("10.210.0.3", "app.example.com:8080/http"), }, @@ -117,7 +118,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "HTTPS container", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "secure.example.com:8000/https"), }, want: `{ @@ -153,7 +154,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "mixed HTTP and HTTPS", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "app.example.com:8080/http", "web.example.com:8000/http"), @@ -227,7 +228,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "container without uncloud network ignored", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainerWithoutNetwork("ignored.example.com:8080/http"), }, want: `{ @@ -252,7 +253,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "container with invalid port ignored", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "invalid-port"), }, want: `{ @@ -277,7 +278,7 @@ func TestGenerateConfig(t *testing.T) { }, { name: "containers with unsupported protocols ignored", - containers: []api.Container{ + containers: []api.ServiceContainer{ newContainer("10.210.0.2", "5000/tcp"), newContainer("10.210.0.3", "5000/udp"), newContainer("10.210.0.4", "80:8080/tcp@host"), @@ -322,44 +323,40 @@ func TestGenerateConfig(t *testing.T) { } } -func newContainer(ip string, ports ...string) api.Container { +func newContainer(ip string, ports ...string) api.ServiceContainer { portsLabel := strings.Join(ports, ",") - return api.Container{ - ContainerJSON: types.ContainerJSON{ - ContainerJSONBase: &types.ContainerJSONBase{}, - NetworkSettings: &types.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - docker.NetworkName: { - IPAddress: ip, - }, - }, - }, - Config: &container.Config{ - Labels: map[string]string{ - api.LabelServicePorts: portsLabel, + return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{}, + NetworkSettings: &types.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + docker.NetworkName: { + IPAddress: ip, }, }, }, - } + Config: &container.Config{ + Labels: map[string]string{ + api.LabelServicePorts: portsLabel, + }, + }, + }}} } -func newContainerWithoutNetwork(ports ...string) api.Container { +func newContainerWithoutNetwork(ports ...string) api.ServiceContainer { portsLabel := strings.Join(ports, ",") - return api.Container{ - ContainerJSON: types.ContainerJSON{ - ContainerJSONBase: &types.ContainerJSONBase{}, - NetworkSettings: &types.NetworkSettings{ - Networks: map[string]*network.EndpointSettings{ - "other-network": { - IPAddress: "172.17.0.2", - }, - }, - }, - Config: &container.Config{ - Labels: map[string]string{ - api.LabelServicePorts: portsLabel, + return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{}, + NetworkSettings: &types.NetworkSettings{ + Networks: map[string]*network.EndpointSettings{ + "other-network": { + IPAddress: "172.17.0.2", }, }, }, - } + Config: &container.Config{ + Labels: map[string]string{ + api.LabelServicePorts: portsLabel, + }, + }, + }}} } diff --git a/internal/machine/caddyfile/controller.go b/internal/machine/caddyfile/controller.go index 8980ce76..e82f8c9d 100644 --- a/internal/machine/caddyfile/controller.go +++ b/internal/machine/caddyfile/controller.go @@ -4,12 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/psviderski/uncloud/internal/fs" - "github.com/psviderski/uncloud/internal/machine/store" - "github.com/psviderski/uncloud/pkg/api" "log/slog" "os" "path/filepath" + + "github.com/psviderski/uncloud/internal/fs" + "github.com/psviderski/uncloud/internal/machine/store" + "github.com/psviderski/uncloud/pkg/api" ) const ( @@ -89,15 +90,20 @@ func (c *Controller) Run(ctx context.Context) error { // filterAvailableContainers filters out containers from this machine that are likely unavailable. The availability // is determined by the cluster membership state of the machine that the container is running on. // TODO: implement machine membership check using Corrossion Admin client. -func (c *Controller) filterAvailableContainers(containerRecords []store.ContainerRecord) ([]api.Container, error) { - containers := make([]api.Container, len(containerRecords)) +func (c *Controller) filterAvailableContainers( + containerRecords []store.ContainerRecord, +) ([]api.ServiceContainer, error) { + containers := make([]api.ServiceContainer, len(containerRecords)) for i, cr := range containerRecords { - containers[i] = cr.Container + containers[i] = api.ServiceContainer{ + Container: cr.Container, + // TODO: restore ServiceSpec from the container record once it's saved in the store. + } } return containers, nil } -func (c *Controller) generateConfig(containers []api.Container) error { +func (c *Controller) generateConfig(containers []api.ServiceContainer) error { config, err := GenerateConfig(containers, c.verifyResponse) if err != nil { return err diff --git a/internal/machine/docker/client.go b/internal/machine/docker/client.go index 1f479c69..25939e06 100644 --- a/internal/machine/docker/client.go +++ b/internal/machine/docker/client.go @@ -369,6 +369,75 @@ func (c *Client) CreateServiceContainer( return resp, nil } +// InspectServiceContainer returns the container information and service specification that was used to create the +// container with the given ID. +func (c *Client) InspectServiceContainer(ctx context.Context, id string) (api.ServiceContainer, error) { + var resp api.ServiceContainer + + grpcResp, err := c.grpcClient.InspectServiceContainer(ctx, &pb.InspectContainerRequest{Id: id}) + if err != nil { + if status.Convert(err).Code() == codes.NotFound { + return resp, errdefs.NotFound(err) + } + return resp, err + } + + if err = json.Unmarshal(grpcResp.Container, &resp.Container); err != nil { + return resp, fmt.Errorf("unmarshal container: %w", err) + } + if err = json.Unmarshal(grpcResp.ServiceSpec, &resp.ServiceSpec); err != nil { + return resp, fmt.Errorf("unmarshal service spec: %w", err) + } + + return resp, nil +} + +type MachineServiceContainers struct { + Metadata *pb.Metadata + Containers []api.ServiceContainer +} + +// ListServiceContainers returns all containers on requested machines that belong to the service with the given +// name or ID. If serviceNameOrID is empty, all service containers are returned. +func (c *Client) ListServiceContainers( + ctx context.Context, serviceNameOrID string, opts container.ListOptions, +) ([]MachineServiceContainers, error) { + optsBytes, err := json.Marshal(opts) + if err != nil { + return nil, fmt.Errorf("marshal options: %w", err) + } + + resp, err := c.grpcClient.ListServiceContainers(ctx, &pb.ListServiceContainersRequest{ + ServiceId: serviceNameOrID, + Options: optsBytes, + }) + if err != nil { + return nil, err + } + + machineContainers := make([]MachineServiceContainers, len(resp.Messages)) + for i, msg := range resp.Messages { + machineContainers[i].Metadata = msg.Metadata + if msg.Metadata != nil && msg.Metadata.Error != "" { + continue + } + + containers := make([]api.ServiceContainer, len(msg.Containers)) + for j, sc := range msg.Containers { + if err = json.Unmarshal(sc.Container, &containers[j].Container); err != nil { + return nil, fmt.Errorf("unmarshal container: %w", err) + } + if err = json.Unmarshal(sc.ServiceSpec, &containers[j].ServiceSpec); err != nil { + return nil, fmt.Errorf("unmarshal service spec: %w", err) + } + } + + machineContainers[i].Containers = containers + } + + return machineContainers, nil +} + // RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID. // A service container is a container that has been created with CreateServiceContainer. func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts container.RemoveOptions) error { diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index f0798a39..e0349b07 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -2,6 +2,7 @@ package docker import ( "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -455,6 +456,125 @@ func (s *Server) CreateServiceContainer( return &pb.CreateContainerResponse{Response: respBytes}, nil } +// InspectServiceContainer returns the container information and service specification that was used to create the +// container with the given ID. +func (s *Server) InspectServiceContainer( + ctx context.Context, req *pb.InspectContainerRequest, +) (*pb.ServiceContainer, error) { + ctr, err := s.client.ContainerInspect(ctx, req.Id) + if err != nil { + if client.IsErrNotFound(err) { + return nil, status.Errorf(codes.NotFound, err.Error()) + } + return nil, status.Errorf(codes.Internal, err.Error()) + } + + ctrBytes, err := json.Marshal(ctr) + if err != nil { + return nil, status.Errorf(codes.Internal, "marshal response: %v", err) + } + + var specBytes []byte + err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, status.Errorf(codes.NotFound, "service spec not found for container: '%s'", ctr.ID) + } + return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v", + ctr.ID, err) + } + + return &pb.ServiceContainer{ + Container: ctrBytes, + ServiceSpec: specBytes, + }, nil +} + +// ListServiceContainers returns all containers that belong to the service with the given name or ID. +// If req.ServiceId is empty, all service containers are returned. +func (s *Server) ListServiceContainers( + ctx context.Context, req *pb.ListServiceContainersRequest, +) (*pb.ListServiceContainersResponse, error) { + var opts container.ListOptions + if len(req.Options) > 0 { + if err := json.Unmarshal(req.Options, &opts); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err) + } + + // Handle filters separately because they implement custom JSON unmarshalling. + var raw map[string]json.RawMessage + if err := json.Unmarshal(req.Options, &raw); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err) + } + + if filtersBytes, ok := raw["Filters"]; ok { + args, err := filters.FromJSON(string(filtersBytes)) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err) + } + opts.Filters = args + } else { + opts.Filters = filters.NewArgs() + } + } + // Only uncloud-managed containers that belong to some service. + opts.Filters.Add("label", api.LabelServiceID) + opts.Filters.Add("label", api.LabelManaged) + + containerSummaries, err := s.client.ContainerList(ctx, opts) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + containers := make([]*pb.ServiceContainer, 0, len(containerSummaries)) + for _, cs := range containerSummaries { + if req.ServiceId != "" && + cs.Labels[api.LabelServiceID] != req.ServiceId && cs.Labels[api.LabelServiceName] != req.ServiceId { + continue + } + + ctr, err := s.client.ContainerInspect(ctx, cs.ID) + if err != nil { + if client.IsErrNotFound(err) { + // The listed container may have been removed while we were inspecting other containers. + continue + } + return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err) + } + ctrBytes, err := json.Marshal(ctr) + if err != nil { + return nil, status.Errorf(codes.Internal, "marshal container: %v", err) + } + + var specBytes []byte + err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + // If this happens, there is a bug in the code, or someone manually removed the container from the DB, + // or created a managed container out of band. + slog.Error("Service container not found in machine database.", "id", ctr.ID) + // Just ignore such a container to not fail the list operation as it's not easily recoverable. + continue + } + return nil, status.Errorf(codes.Internal, "get service spec for container '%s' from machine database: %v", + ctr.ID, err) + } + + containers = append(containers, &pb.ServiceContainer{ + Container: ctrBytes, + ServiceSpec: specBytes, + }) + } + + return &pb.ListServiceContainersResponse{ + Messages: []*pb.MachineServiceContainers{ + { + Containers: containers, + }, + }, + }, nil +} + // RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID. // The difference between this method and RemoveContainer is that it also removes the container from the machine // database. diff --git a/internal/machine/machine.go b/internal/machine/machine.go index fc519f74..15cf5b2e 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -27,6 +27,7 @@ import ( machinedocker "github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/store" + "github.com/psviderski/uncloud/pkg/api" "github.com/siderolabs/grpc-proxy/proxy" "golang.org/x/sync/errgroup" "google.golang.org/grpc" @@ -751,10 +752,11 @@ func (m *Machine) InspectService( } } + ctr := api.ServiceContainer{Container: records[0].Container} svc := &pb.Service{ - Id: records[0].Container.ServiceID(), - Name: records[0].Container.ServiceName(), - Mode: records[0].Container.ServiceMode(), + Id: ctr.ServiceID(), + Name: ctr.ServiceName(), + Mode: ctr.ServiceMode(), Containers: containers, } return &pb.InspectServiceResponse{Service: svc}, nil diff --git a/pkg/api/client.go b/pkg/api/client.go index f6200cee..60f62009 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -3,10 +3,11 @@ package api import ( "context" "fmt" + "slices" + "github.com/docker/docker/api/types/container" "github.com/psviderski/uncloud/internal/machine/api/pb" "google.golang.org/grpc/metadata" - "slices" ) type Client interface { @@ -21,10 +22,10 @@ type ContainerClient interface { CreateContainer( ctx context.Context, serviceID string, spec ServiceSpec, machineID string, ) (container.CreateResponse, error) - InspectContainer(ctx context.Context, serviceID, containerID string) (MachineContainer, error) - RemoveContainer(ctx context.Context, serviceID, containerID string, opts container.RemoveOptions) error - StartContainer(ctx context.Context, serviceID, containerID string) error - StopContainer(ctx context.Context, serviceID, containerID string, opts container.StopOptions) error + InspectContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) (MachineServiceContainer, error) + RemoveContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions) error + StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error + StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error } type DNSClient interface { diff --git a/pkg/api/container.go b/pkg/api/container.go index 9a7cbad0..3ce15ff5 100644 --- a/pkg/api/container.go +++ b/pkg/api/container.go @@ -1,11 +1,13 @@ package api import ( + "encoding/json" "fmt" - "github.com/docker/docker/api/types" - "github.com/docker/go-units" "strings" "time" + + "github.com/docker/docker/api/types" + "github.com/docker/go-units" ) const ( @@ -21,73 +23,6 @@ type Container struct { types.ContainerJSON } -// NameWithoutSlash returns the container name without the leading slash. -// TODO: modify Name in original ContainerJSON structure when inspecting a Docker container and get rid of this method. -func (c *Container) NameWithoutSlash() string { - return c.Name[1:] -} - -// ServiceID returns the ID of the service this container belongs to. -func (c *Container) ServiceID() string { - return c.Config.Labels[LabelServiceID] -} - -// ServiceName returns the name of the service this container belongs to. -func (c *Container) ServiceName() string { - return c.Config.Labels[LabelServiceName] -} - -// ServiceMode returns the replication mode of the service this container belongs to. -func (c *Container) ServiceMode() string { - return c.Config.Labels[LabelServiceMode] -} - -// ServicePorts returns the ports this container publishes as part of its service. -func (c *Container) ServicePorts() ([]PortSpec, error) { - encoded, ok := c.Config.Labels[LabelServicePorts] - if !ok { - return nil, nil - } - if strings.TrimSpace(encoded) == "" { - return nil, nil - } - - publishPorts := strings.Split(encoded, ",") - ports := make([]PortSpec, len(publishPorts)) - for i, p := range publishPorts { - port, err := ParsePortSpec(strings.TrimSpace(p)) - if err != nil { - return nil, err - } - ports[i] = port - } - - return ports, nil -} - -// ServiceSpec constructs a service spec from the container's configuration. -func (c *Container) ServiceSpec() (ServiceSpec, error) { - ports, err := c.ServicePorts() - if err != nil { - return ServiceSpec{}, fmt.Errorf("get service ports: %w", err) - } - - // TODO: many properties on the container such as Config.Cmd or Config.Entrypoint are populated when the container - // is created. Figure out how to get a spec that is equal to the initial spec. - return ServiceSpec{ - Container: ContainerSpec{ - Command: c.Config.Cmd, - Entrypoint: c.Config.Entrypoint, - Image: c.Config.Image, - Init: c.HostConfig.Init, - Volumes: c.HostConfig.Binds, - }, - Mode: c.ServiceMode(), - Name: c.ServiceName(), - Ports: ports, - }, nil -} - // Healthy determines if the container is running and healthy. // A running container with no health check configured is considered healthy. func (c *Container) Healthy() bool { @@ -156,8 +91,71 @@ func (c *Container) HumanState() (string, error) { c.State.ExitCode, units.HumanDuration(time.Now().UTC().Sub(finishedAt))), nil } +func (c *Container) UnmarshalJSON(data []byte) error { + // A temporary type that's identical to Container but doesn't have the UnmarshalJSON method. + type ContainerAlias Container + + var temp ContainerAlias + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + *c = Container(temp) + c.Name = strings.TrimPrefix(c.Name, "/") + + return nil +} + +type ServiceContainer struct { + Container + ServiceSpec ServiceSpec +} + +type MachineContainer struct { + MachineID string + Container Container +} + +// ServiceID returns the ID of the service this container belongs to. +func (c *ServiceContainer) ServiceID() string { + return c.Config.Labels[LabelServiceID] +} + +// ServiceName returns the name of the service this container belongs to. +func (c *ServiceContainer) ServiceName() string { + return c.Config.Labels[LabelServiceName] +} + +// ServiceMode returns the replication mode of the service this container belongs to. +func (c *ServiceContainer) ServiceMode() string { + return c.Config.Labels[LabelServiceMode] +} + +// ServicePorts returns the ports this container publishes as part of its service. +func (c *ServiceContainer) ServicePorts() ([]PortSpec, error) { + encoded, ok := c.Config.Labels[LabelServicePorts] + if !ok { + return nil, nil + } + if strings.TrimSpace(encoded) == "" { + return nil, nil + } + + publishPorts := strings.Split(encoded, ",") + ports := make([]PortSpec, len(publishPorts)) + for i, p := range publishPorts { + port, err := ParsePortSpec(strings.TrimSpace(p)) + if err != nil { + return nil, err + } + ports[i] = port + } + + return ports, nil +} + // ConflictingServicePorts returns a list of service ports that conflict with the given ports. -func (c *Container) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) { +func (c *ServiceContainer) ConflictingServicePorts(ports []PortSpec) ([]PortSpec, error) { svcPorts, err := c.ServicePorts() if err != nil { return nil, fmt.Errorf("get service ports: %w", err) diff --git a/pkg/api/container_test.go b/pkg/api/container_test.go index b770979a..9dc0e21e 100644 --- a/pkg/api/container_test.go +++ b/pkg/api/container_test.go @@ -1,60 +1,15 @@ package api import ( + "net/netip" + "testing" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/netip" - "reflect" - "testing" ) -func TestContainer_ServiceSpec(t *testing.T) { - t.Parallel() - - init := true - ctr := &Container{ContainerJSON: types.ContainerJSON{ - ContainerJSONBase: &types.ContainerJSONBase{ - HostConfig: &container.HostConfig{ - Binds: []string{"/host/path:/container/path"}, - Init: &init, - }, - }, - Config: &container.Config{ - Cmd: []string{"/app/server"}, - Image: "app:latest", - Labels: map[string]string{ - LabelServiceID: "test-service-id", - LabelServiceName: "test-service-name", - LabelServicePorts: "app.example.com:8000/https", - }, - }, - }} - - expectedSpec := ServiceSpec{ - Container: ContainerSpec{ - Command: []string{"/app/server"}, - Image: "app:latest", - Init: &init, - Volumes: []string{"/host/path:/container/path"}, - }, - Name: "test-service-name", - Ports: []PortSpec{ - { - Hostname: "app.example.com", - ContainerPort: 8000, - Protocol: ProtocolHTTPS, - Mode: PortModeIngress, - }, - }, - } - - spec, err := ctr.ServiceSpec() - require.NoError(t, err) - assert.True(t, reflect.DeepEqual(spec, expectedSpec)) -} - func TestContainer_Healthy(t *testing.T) { t.Parallel() @@ -325,13 +280,13 @@ func TestContainer_ConflictingServicePorts(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctr := &Container{ContainerJSON: types.ContainerJSON{ + ctr := &ServiceContainer{Container: Container{ContainerJSON: types.ContainerJSON{ Config: &container.Config{ Labels: map[string]string{ LabelServicePorts: tt.containerPorts, }, }, - }} + }}} got, err := ctr.ConflictingServicePorts(tt.checkPorts) if tt.wantErr { diff --git a/pkg/api/service.go b/pkg/api/service.go index b098ccd9..7254dfda 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -5,12 +5,13 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/distribution/reference" - "github.com/psviderski/uncloud/internal/machine/api/pb" "maps" "reflect" "regexp" "slices" + + "github.com/distribution/reference" + "github.com/psviderski/uncloud/internal/machine/api/pb" ) const ( @@ -206,12 +207,12 @@ type Service struct { ID string Name string Mode string - Containers []MachineContainer + Containers []MachineServiceContainer } -type MachineContainer struct { +type MachineServiceContainer struct { MachineID string - Container Container + Container ServiceContainer } // Endpoints returns the exposed HTTP and HTTPS endpoints of the service. @@ -261,7 +262,7 @@ func (s *Service) Endpoints() []string { func ServiceFromProto(s *pb.Service) (Service, error) { var err error - containers := make([]MachineContainer, len(s.Containers)) + containers := make([]MachineServiceContainer, len(s.Containers)) for i, sc := range s.Containers { containers[i], err = machineContainerFromProto(sc) if err != nil { @@ -277,14 +278,14 @@ func ServiceFromProto(s *pb.Service) (Service, error) { }, nil } -func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) { +func machineContainerFromProto(sc *pb.Service_Container) (MachineServiceContainer, error) { var c Container if err := json.Unmarshal(sc.Container, &c); err != nil { - return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err) + return MachineServiceContainer{}, fmt.Errorf("unmarshal container: %w", err) } - return MachineContainer{ + return MachineServiceContainer{ MachineID: sc.MachineId, - Container: c, + Container: ServiceContainer{Container: c}, }, nil } diff --git a/pkg/client/client.go b/pkg/client/client.go index 1577dea9..d712d17a 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" + "os" + "github.com/docker/cli/cli/streams" "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/pkg/api" "google.golang.org/grpc" "google.golang.org/grpc/metadata" - "os" ) // Client is a client for the machine API. diff --git a/pkg/client/container.go b/pkg/client/container.go index 0c396c88..e9da408e 100644 --- a/pkg/client/container.go +++ b/pkg/client/container.go @@ -190,29 +190,26 @@ func toPullProgressEvent(jm jsonmessage.JSONMessage) *progress.Event { } // InspectContainer returns the information about the specified container within the service. -func (cli *Client) InspectContainer(ctx context.Context, serviceID, containerID string) (api.MachineContainer, error) { - var ctr api.MachineContainer - - svc, err := cli.InspectService(ctx, serviceID) +func (cli *Client) InspectContainer( + ctx context.Context, serviceNameOrID, containerNameOrID string, +) (api.MachineServiceContainer, error) { + svc, err := cli.InspectService(ctx, serviceNameOrID) if err != nil { - return ctr, fmt.Errorf("inspect service: %w", err) + return api.MachineServiceContainer{}, fmt.Errorf("inspect service: %w", err) } for _, c := range svc.Containers { - if c.Container.ID == containerID || c.Container.NameWithoutSlash() == containerID { - ctr = c + if c.Container.ID == containerNameOrID || c.Container.Name == containerNameOrID { + return c, nil } } - if ctr.MachineID == "" { - return ctr, api.ErrNotFound - } - return ctr, nil + return api.MachineServiceContainer{}, api.ErrNotFound } // StartContainer starts the specified container within the service. -func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID string) error { - ctr, err := cli.InspectContainer(ctx, serviceID, containerID) +func (cli *Client) StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error { + ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID) if err != nil { return err } @@ -224,7 +221,7 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st ctx = proxyToMachine(ctx, machine.Machine) pw := progress.ContextWriter(ctx) - eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name) + eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name) pw.Event(progress.StartingEvent(eventID)) if err = cli.Docker.StartContainer(ctx, ctr.Container.ID, container.StartOptions{}); err != nil { @@ -237,9 +234,9 @@ func (cli *Client) StartContainer(ctx context.Context, serviceID, containerID st // StopContainer stops the specified container within the service. func (cli *Client) StopContainer( - ctx context.Context, serviceID, containerID string, opts container.StopOptions, + ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions, ) error { - ctr, err := cli.InspectContainer(ctx, serviceID, containerID) + ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID) if err != nil { return err } @@ -251,7 +248,7 @@ func (cli *Client) StopContainer( ctx = proxyToMachine(ctx, machine.Machine) pw := progress.ContextWriter(ctx) - eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name) + eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name) pw.Event(progress.StoppingEvent(eventID)) if err = cli.Docker.StopContainer(ctx, ctr.Container.ID, opts); err != nil { @@ -264,9 +261,9 @@ func (cli *Client) StopContainer( // RemoveContainer removes the specified container within the service. func (cli *Client) RemoveContainer( - ctx context.Context, serviceID, containerNameOrID string, opts container.RemoveOptions, + ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.RemoveOptions, ) error { - ctr, err := cli.InspectContainer(ctx, serviceID, containerNameOrID) + ctr, err := cli.InspectContainer(ctx, serviceNameOrID, containerNameOrID) if err != nil { return err } @@ -278,7 +275,7 @@ func (cli *Client) RemoveContainer( ctx = proxyToMachine(ctx, machine.Machine) pw := progress.ContextWriter(ctx) - eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name) + eventID := fmt.Sprintf("Container %s on %s", ctr.Container.Name, machine.Machine.Name) pw.Event(progress.RemovingEvent(eventID)) if err = cli.Docker.RemoveServiceContainer(ctx, ctr.Container.ID, opts); err != nil { diff --git a/pkg/client/deploy/container.go b/pkg/client/deploy/container.go index 51853f92..4925c9c1 100644 --- a/pkg/client/deploy/container.go +++ b/pkg/client/deploy/container.go @@ -2,6 +2,7 @@ package deploy import ( "fmt" + "github.com/psviderski/uncloud/pkg/api" ) @@ -11,7 +12,8 @@ const ContainerUpToDate ContainerSpecStatus = "up-to-date" const ContainerNeedsUpdate ContainerSpecStatus = "needs-update" const ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate" -func CompareContainerToSpec(ctr api.Container, spec api.ServiceSpec) (ContainerSpecStatus, error) { +func CompareContainerToSpec(ctr api.ServiceContainer, spec api.ServiceSpec) (ContainerSpecStatus, error) { + // TODO: replace the hash comparison with a more detailed comparison of ctr.ServiceSpec and spec. specHash, err := spec.ImmutableHash() if err != nil { return "", fmt.Errorf("calculate immutable hash for service spec: %w", err) diff --git a/pkg/client/deploy/strategy.go b/pkg/client/deploy/strategy.go index 840bf7bf..016c2ff3 100644 --- a/pkg/client/deploy/strategy.go +++ b/pkg/client/deploy/strategy.go @@ -3,11 +3,12 @@ package deploy import ( "context" "fmt" + "math/rand/v2" + "slices" + "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/pkg/api" - "math/rand/v2" - "slices" ) // Strategy defines how a service should be deployed or updated. Different implementations can provide various @@ -89,7 +90,7 @@ func (s *RollingStrategy) planReplicated( }) // Organise existing containers by machine. - containersOnMachine := make(map[string][]api.Container) + containersOnMachine := make(map[string][]api.ServiceContainer) upToDateContainersOnMachine := make(map[string]int) containerSpecStatuses := make(map[string]ContainerSpecStatus) if svc != nil { @@ -111,7 +112,7 @@ func (s *RollingStrategy) planReplicated( } // Sort containers such that running containers with the desired spec are first. - slices.SortFunc(svc.Containers, func(c1, c2 api.MachineContainer) int { + slices.SortFunc(svc.Containers, func(c1, c2 api.MachineServiceContainer) int { if status, ok := containerSpecStatuses[c1.Container.ID]; ok && status == ContainerUpToDate { return -1 } @@ -222,7 +223,7 @@ func (s *RollingStrategy) planGlobal( // Map machineID to service containers on that machine. For the global mode, there should be at most one // container per machine but we use a slice to handle multiple containers that may exist due to a bug // or interruption in the previous deployment. - containersOnMachine := make(map[string][]api.MachineContainer) + containersOnMachine := make(map[string][]api.MachineServiceContainer) if svc != nil { for _, c := range svc.Containers { containersOnMachine[c.MachineID] = append(containersOnMachine[c.MachineID], c) @@ -271,7 +272,7 @@ func (s *RollingStrategy) planGlobal( // It ensures exactly one container with the desired spec is running on the machine by creating a new container and // removing old ones. If there is a host port conflict, it stops the old container before starting a new one. func reconcileGlobalContainer( - containers []api.MachineContainer, spec api.ServiceSpec, serviceID, machineID string, + containers []api.MachineServiceContainer, spec api.ServiceSpec, serviceID, machineID string, ) ([]Operation, error) { var ops []Operation diff --git a/pkg/client/resolver.go b/pkg/client/resolver.go index 464c8172..e1135269 100644 --- a/pkg/client/resolver.go +++ b/pkg/client/resolver.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "github.com/psviderski/uncloud/pkg/api" ) @@ -46,7 +47,7 @@ func (cli *Client) ServiceOperationNameResolver(ctx context.Context, svc api.Ser } containerNames := make(map[string]string, len(svc.Containers)) for _, c := range svc.Containers { - containerNames[c.Container.ID] = c.Container.NameWithoutSlash() + containerNames[c.Container.ID] = c.Container.Name } return NewNameResolver(machineNames, containerNames), nil diff --git a/pkg/client/service.go b/pkg/client/service.go index eb7226e8..30cc728c 100644 --- a/pkg/client/service.go +++ b/pkg/client/service.go @@ -4,17 +4,18 @@ import ( "context" "errors" "fmt" + "os" + "slices" + "sync" + "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" "github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client/deploy" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "slices" - "sync" ) type RunServiceResponse struct { @@ -64,8 +65,8 @@ func (cli *Client) RunService( } // InspectService returns detailed information about a service and its containers. -// The id parameter can be either a service ID or name. -func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, error) { +// The nameOrID parameter can be either a service name or ID. +func (cli *Client) InspectService(ctx context.Context, nameOrID string) (api.Service, error) { var svc api.Service machines, err := cli.ListMachines(ctx) @@ -87,22 +88,16 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, } listCtx := metadata.NewOutgoingContext(ctx, md) - // List only uncloud-managed containers that belong to some service. - opts := container.ListOptions{ - All: true, - Filters: filters.NewArgs( - filters.Arg("label", api.LabelServiceID), - filters.Arg("label", api.LabelManaged), - ), - } - machineContainers, err := cli.Docker.ListContainers(listCtx, opts) + // List all service containers including stopped ones. + opts := container.ListOptions{All: true} + machineContainers, err := cli.Docker.ListServiceContainers(listCtx, nameOrID, opts) if err != nil { return svc, fmt.Errorf("list containers: %w", err) } // Collect all containers on all machines that belong to the specified service. foundByID := false - var containers []api.MachineContainer + var containers []api.MachineServiceContainer for _, mc := range machineContainers { // Metadata can be nil if the request was broadcasted to only one machine. if mc.Metadata == nil && len(machineContainers) > 1 { @@ -130,15 +125,14 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, } } - for _, c := range mc.Containers { - ctr := api.Container{ContainerJSON: c} - if ctr.ServiceID() == id || ctr.ServiceName() == id { - containers = append(containers, api.MachineContainer{ + for _, ctr := range mc.Containers { + if ctr.ServiceID() == nameOrID || ctr.ServiceName() == nameOrID { + containers = append(containers, api.MachineServiceContainer{ MachineID: machineID, Container: ctr, }) - if ctr.ServiceID() == id { + if ctr.ServiceID() == nameOrID { foundByID = true } } @@ -153,15 +147,15 @@ func (cli *Client) InspectService(ctx context.Context, id string) (api.Service, // may not prevent this), or a service name might match another service's ID. In these cases, matching by ID takes // priority over matching by name. if foundByID { - containers = slices.DeleteFunc(containers, func(mc api.MachineContainer) bool { - return mc.Container.ServiceID() != id + containers = slices.DeleteFunc(containers, func(mc api.MachineServiceContainer) bool { + return mc.Container.ServiceID() != nameOrID }) } else { // Matched only by name but there could be multiple services with the same name. serviceID := containers[0].Container.ServiceID() for _, mc := range containers[1:] { if mc.Container.ServiceID() != serviceID { - return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", id) + return svc, fmt.Errorf("multiple services found with name '%s', use the service ID instead", nameOrID) } } } @@ -273,15 +267,9 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) { } listCtx := metadata.NewOutgoingContext(ctx, md) - // List only uncloud-managed containers that belong to some service. - opts := container.ListOptions{ - All: true, - Filters: filters.NewArgs( - filters.Arg("label", api.LabelServiceID), - filters.Arg("label", api.LabelManaged), - ), - } - machineContainers, err := cli.Docker.ListContainers(listCtx, opts) + // List all containers including stopped ones. + opts := container.ListOptions{All: true} + machineContainers, err := cli.Docker.ListServiceContainers(listCtx, "", opts) if err != nil { return nil, fmt.Errorf("list containers: %w", err) } @@ -292,13 +280,12 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) { for _, mc := range machineContainers { if mc.Metadata != nil && mc.Metadata.Error != "" { // TODO: return failed machines in the response. - fmt.Printf("WARNING: failed to list containers on machine '%s': %s\n", + fmt.Fprintf(os.Stderr, "WARNING: failed to list containers on machine '%s': %s\n", mc.Metadata.Machine, mc.Metadata.Error) continue } - for _, c := range mc.Containers { - ctr := api.Container{ContainerJSON: c} + for _, ctr := range mc.Containers { if _, ok := servicesByID[ctr.ServiceID()]; ok { continue } diff --git a/test/e2e/assert.go b/test/e2e/assert.go index 9ea9f34c..828b6525 100644 --- a/test/e2e/assert.go +++ b/test/e2e/assert.go @@ -1,12 +1,13 @@ package e2e import ( + "testing" + mapset "github.com/deckarep/golang-set/v2" "github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client/deploy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpec) { @@ -24,15 +25,15 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe } } -func assertContainerMatchesSpec(t *testing.T, ctr api.Container, spec api.ServiceSpec) { +func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) { status, err := deploy.CompareContainerToSpec(ctr, spec) require.NoError(t, err) assert.Equal(t, deploy.ContainerUpToDate, status) } // serviceContainersByMachine returns a map of machine ID to service containers on that machine. -func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.Container { - containers := make(map[string][]api.Container) +func serviceContainersByMachine(t *testing.T, svc api.Service) map[string][]api.ServiceContainer { + containers := make(map[string][]api.ServiceContainer) for _, c := range svc.Containers { containers[c.MachineID] = append(containers[c.MachineID], c.Container) } diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go index 7f673479..6b912c97 100644 --- a/test/e2e/service_test.go +++ b/test/e2e/service_test.go @@ -3,6 +3,10 @@ package e2e import ( "context" "errors" + "net/netip" + "strings" + "testing" + "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" "github.com/psviderski/uncloud/internal/machine/api/pb" @@ -14,9 +18,6 @@ import ( "github.com/psviderski/uncloud/pkg/client/deploy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/netip" - "strings" - "testing" ) func newServiceID() string { @@ -246,7 +247,7 @@ func TestDeployment(t *testing.T) { assert.NotEqual(t, oldContainerID, ctr.Container.ID, "Container on machine %s should have been updated", machine.Machine.Name) - svcSpec, err := ctr.Container.ServiceSpec() + svcSpec := ctr.Container.ServiceSpec require.NoError(t, err) assert.NotNil(t, svcSpec.Container.Init) assert.True(t, *svcSpec.Container.Init, @@ -279,7 +280,7 @@ func TestDeployment(t *testing.T) { // Verify all containers are updated with a published port. for _, ctr := range svc.Containers { - svcSpec, err := ctr.Container.ServiceSpec() + svcSpec := ctr.Container.ServiceSpec require.NoError(t, err) assert.Nil(t, svcSpec.Container.Init, "Container on machine %s should have init disabled", ctr.MachineID) @@ -382,7 +383,7 @@ func TestDeployment(t *testing.T) { assert.Len(t, svc.Containers, 2) // Existing container ctr0 on machine #0 should be left unchanged. - var ctr2 api.MachineContainer + var ctr2 api.MachineServiceContainer if ctr0.Container.ID == svc.Containers[0].Container.ID { ctr2 = svc.Containers[1] } else { @@ -652,7 +653,7 @@ func TestServiceLifecycle(t *testing.T) { require.NoError(t, err) ctr := mc.Container - assert.True(t, strings.HasPrefix(ctr.Name, "/container-spec-defaults-")) + assert.True(t, strings.HasPrefix(ctr.Name, "container-spec-defaults-")) assert.Equal(t, "portainer/pause:latest", ctr.Config.Image) // Verify default settings. @@ -727,7 +728,7 @@ func TestServiceLifecycle(t *testing.T) { require.NoError(t, err) ctr := mc.Container - assert.True(t, strings.HasPrefix(ctr.Name, "/container-spec-full-")) + assert.True(t, strings.HasPrefix(ctr.Name, "container-spec-full-")) assert.Equal(t, "portainer/pause:latest", ctr.Config.Image) assert.EqualValues(t, spec.Container.Command, ctr.Config.Cmd)