diff --git a/cmd/uncloud/service/inspect.go b/cmd/uncloud/service/inspect.go index c87252a3..bdb4be17 100644 --- a/cmd/uncloud/service/inspect.go +++ b/cmd/uncloud/service/inspect.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "maps" "os" "slices" "text/tabwriter" @@ -59,6 +60,24 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error { fmt.Printf("Service ID: %s\n", svc.ID) fmt.Printf("Name: %s\n", svc.Name) fmt.Printf("Mode: %s\n", svc.Mode) + + // Display labels from the service spec. Use the first container's spec as representative. + if len(svc.Containers) > 0 { + spec := svc.Containers[0].Container.ServiceSpec + if len(spec.Labels) > 0 { + fmt.Println("Labels:") + for _, key := range slices.Sorted(maps.Keys(spec.Labels)) { + fmt.Printf(" %s=%s\n", key, spec.Labels[key]) + } + } + if len(spec.DeployLabels) > 0 { + fmt.Println("Deploy Labels:") + for _, key := range slices.Sorted(maps.Keys(spec.DeployLabels)) { + fmt.Printf(" %s=%s\n", key, spec.DeployLabels[key]) + } + } + } + fmt.Println() // Parse created times for sorting and display. diff --git a/cmd/uncloud/service/run.go b/cmd/uncloud/service/run.go index f45258cd..9f33cf11 100644 --- a/cmd/uncloud/service/run.go +++ b/cmd/uncloud/service/run.go @@ -24,6 +24,7 @@ type runOptions struct { entrypointChanged bool env []string image string + labels []string machines []string memory dockeropts.MemBytes mode string @@ -110,6 +111,9 @@ func NewRunCommand(groupID string) *cobra.Command { " -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+ " -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+ " -v /host/path:/container/path:ro Bind mount a host directory or file as read-only") + cmd.Flags().StringSliceVarP(&opts.labels, "label", "l", nil, + "Set a label on service containers. Can be specified multiple times.\n"+ + "Format: key=value") return cmd } @@ -199,6 +203,11 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) { return spec, err } + labels, err := parseLabels(opts.labels) + if err != nil { + return spec, err + } + placement := api.Placement{ Machines: cli.ExpandCommaSeparatedValues(opts.machines), } @@ -217,6 +226,7 @@ func prepareServiceSpec(opts runOptions) (api.ServiceSpec, error) { User: opts.user, VolumeMounts: mounts, }, + Labels: labels, Mode: opts.mode, Name: opts.name, Placement: placement, @@ -275,6 +285,24 @@ func parseEnv(env []string) (api.EnvVars, error) { return envVars, nil } +// parseLabels parses label flags in the format "key=value". +func parseLabels(labels []string) (map[string]string, error) { + if len(labels) == 0 { + return nil, nil + } + + result := make(map[string]string, len(labels)) + for _, l := range labels { + key, value, hasValue := strings.Cut(l, "=") + if key == "" || !hasValue { + return nil, fmt.Errorf("invalid label format '%s': must be key=value", l) + } + result[key] = value + } + + return result, nil +} + // parseVolumeFlags parses volume flag values in Docker CLI format and returns VolumeSpecs and VolumeMounts. // It handles both named volumes (volume_name:/container/path[:ro|volume-nocopy]) // and bind mounts (/host/path:/container/path[:ro]). diff --git a/internal/machine/api/pb/docker.pb.go b/internal/machine/api/pb/docker.pb.go index 2905e680..7d3cb5ae 100644 --- a/internal/machine/api/pb/docker.pb.go +++ b/internal/machine/api/pb/docker.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.34.2 -// protoc v5.27.3 +// protoc v5.29.3 // source: internal/machine/api/pb/docker.proto package pb @@ -2247,6 +2247,62 @@ func (x *MachineServiceContainers) GetContainers() []*ServiceContainer { return nil } +type UpdateServiceContainerSpecRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContainerId string `protobuf:"bytes,1,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + // JSON serialised api.ServiceSpec. + ServiceSpec []byte `protobuf:"bytes,2,opt,name=service_spec,json=serviceSpec,proto3" json:"service_spec,omitempty"` +} + +func (x *UpdateServiceContainerSpecRequest) Reset() { + *x = UpdateServiceContainerSpecRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_machine_api_pb_docker_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateServiceContainerSpecRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateServiceContainerSpecRequest) ProtoMessage() {} + +func (x *UpdateServiceContainerSpecRequest) ProtoReflect() protoreflect.Message { + mi := &file_internal_machine_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 UpdateServiceContainerSpecRequest.ProtoReflect.Descriptor instead. +func (*UpdateServiceContainerSpecRequest) Descriptor() ([]byte, []int) { + return file_internal_machine_api_pb_docker_proto_rawDescGZIP(), []int{38} +} + +func (x *UpdateServiceContainerSpecRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *UpdateServiceContainerSpecRequest) GetServiceSpec() []byte { + if x != nil { + return x.ServiceSpec + } + return nil +} + var File_internal_machine_api_pb_docker_proto protoreflect.FileDescriptor var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ @@ -2460,100 +2516,113 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ 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, 0x8d, 0x0b, 0x0a, 0x06, 0x44, 0x6f, 0x63, - 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, - 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, - 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, - 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x4a, 0x0a, 0x0d, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x61, 0x70, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x22, 0x69, 0x0a, 0x21, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, + 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, + 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, 0x32, 0xeb, 0x0b, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, + 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, + 0x0e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, + 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x42, 0x0a, 0x0d, 0x53, 0x74, 0x6f, 0x70, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, + 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4a, 0x0a, 0x0d, 0x45, 0x78, + 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0d, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x19, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 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, 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, 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, + 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, 0x44, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 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, + 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, 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, + 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5a, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x12, 0x22, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, + 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x15, 0x4c, 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, 0x12, 0x5c, 0x0a, 0x1a, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x70, 0x65, + 0x63, 0x12, 0x26, 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, 0x53, 0x70, + 0x65, 0x63, 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 ( @@ -2569,68 +2638,69 @@ func file_internal_machine_api_pb_docker_proto_rawDescGZIP() []byte { } var file_internal_machine_api_pb_docker_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 38) +var file_internal_machine_api_pb_docker_proto_msgTypes = make([]protoimpl.MessageInfo, 39) var file_internal_machine_api_pb_docker_proto_goTypes = []any{ - (ContainerLogEntry_StreamType)(0), // 0: api.ContainerLogEntry.StreamType - (*CreateContainerRequest)(nil), // 1: api.CreateContainerRequest - (*CreateContainerResponse)(nil), // 2: api.CreateContainerResponse - (*InspectContainerRequest)(nil), // 3: api.InspectContainerRequest - (*InspectContainerResponse)(nil), // 4: api.InspectContainerResponse - (*StartContainerRequest)(nil), // 5: api.StartContainerRequest - (*StopContainerRequest)(nil), // 6: api.StopContainerRequest - (*ListContainersRequest)(nil), // 7: api.ListContainersRequest - (*ListContainersResponse)(nil), // 8: api.ListContainersResponse - (*MachineContainers)(nil), // 9: api.MachineContainers - (*RemoveContainerRequest)(nil), // 10: api.RemoveContainerRequest - (*ExecContainerRequest)(nil), // 11: api.ExecContainerRequest - (*ExecConfig)(nil), // 12: api.ExecConfig - (*ResizeEvent)(nil), // 13: api.ResizeEvent - (*ExecContainerResponse)(nil), // 14: api.ExecContainerResponse - (*ContainerLogsRequest)(nil), // 15: api.ContainerLogsRequest - (*ContainerLogEntry)(nil), // 16: api.ContainerLogEntry - (*PullImageRequest)(nil), // 17: api.PullImageRequest - (*JSONMessage)(nil), // 18: api.JSONMessage - (*InspectImageRequest)(nil), // 19: api.InspectImageRequest - (*InspectImageResponse)(nil), // 20: api.InspectImageResponse - (*Image)(nil), // 21: api.Image - (*InspectRemoteImageRequest)(nil), // 22: api.InspectRemoteImageRequest - (*InspectRemoteImageResponse)(nil), // 23: api.InspectRemoteImageResponse - (*RemoteImage)(nil), // 24: api.RemoteImage - (*ListImagesRequest)(nil), // 25: api.ListImagesRequest - (*ListImagesResponse)(nil), // 26: api.ListImagesResponse - (*MachineImages)(nil), // 27: api.MachineImages - (*CreateVolumeRequest)(nil), // 28: api.CreateVolumeRequest - (*CreateVolumeResponse)(nil), // 29: api.CreateVolumeResponse - (*ListVolumesRequest)(nil), // 30: api.ListVolumesRequest - (*ListVolumesResponse)(nil), // 31: api.ListVolumesResponse - (*MachineVolumes)(nil), // 32: api.MachineVolumes - (*RemoveVolumeRequest)(nil), // 33: api.RemoveVolumeRequest - (*CreateServiceContainerRequest)(nil), // 34: api.CreateServiceContainerRequest - (*ServiceContainer)(nil), // 35: api.ServiceContainer - (*ListServiceContainersRequest)(nil), // 36: api.ListServiceContainersRequest - (*ListServiceContainersResponse)(nil), // 37: api.ListServiceContainersResponse - (*MachineServiceContainers)(nil), // 38: api.MachineServiceContainers - (*Metadata)(nil), // 39: api.Metadata - (*timestamppb.Timestamp)(nil), // 40: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 41: google.protobuf.Empty + (ContainerLogEntry_StreamType)(0), // 0: api.ContainerLogEntry.StreamType + (*CreateContainerRequest)(nil), // 1: api.CreateContainerRequest + (*CreateContainerResponse)(nil), // 2: api.CreateContainerResponse + (*InspectContainerRequest)(nil), // 3: api.InspectContainerRequest + (*InspectContainerResponse)(nil), // 4: api.InspectContainerResponse + (*StartContainerRequest)(nil), // 5: api.StartContainerRequest + (*StopContainerRequest)(nil), // 6: api.StopContainerRequest + (*ListContainersRequest)(nil), // 7: api.ListContainersRequest + (*ListContainersResponse)(nil), // 8: api.ListContainersResponse + (*MachineContainers)(nil), // 9: api.MachineContainers + (*RemoveContainerRequest)(nil), // 10: api.RemoveContainerRequest + (*ExecContainerRequest)(nil), // 11: api.ExecContainerRequest + (*ExecConfig)(nil), // 12: api.ExecConfig + (*ResizeEvent)(nil), // 13: api.ResizeEvent + (*ExecContainerResponse)(nil), // 14: api.ExecContainerResponse + (*ContainerLogsRequest)(nil), // 15: api.ContainerLogsRequest + (*ContainerLogEntry)(nil), // 16: api.ContainerLogEntry + (*PullImageRequest)(nil), // 17: api.PullImageRequest + (*JSONMessage)(nil), // 18: api.JSONMessage + (*InspectImageRequest)(nil), // 19: api.InspectImageRequest + (*InspectImageResponse)(nil), // 20: api.InspectImageResponse + (*Image)(nil), // 21: api.Image + (*InspectRemoteImageRequest)(nil), // 22: api.InspectRemoteImageRequest + (*InspectRemoteImageResponse)(nil), // 23: api.InspectRemoteImageResponse + (*RemoteImage)(nil), // 24: api.RemoteImage + (*ListImagesRequest)(nil), // 25: api.ListImagesRequest + (*ListImagesResponse)(nil), // 26: api.ListImagesResponse + (*MachineImages)(nil), // 27: api.MachineImages + (*CreateVolumeRequest)(nil), // 28: api.CreateVolumeRequest + (*CreateVolumeResponse)(nil), // 29: api.CreateVolumeResponse + (*ListVolumesRequest)(nil), // 30: api.ListVolumesRequest + (*ListVolumesResponse)(nil), // 31: api.ListVolumesResponse + (*MachineVolumes)(nil), // 32: api.MachineVolumes + (*RemoveVolumeRequest)(nil), // 33: api.RemoveVolumeRequest + (*CreateServiceContainerRequest)(nil), // 34: api.CreateServiceContainerRequest + (*ServiceContainer)(nil), // 35: api.ServiceContainer + (*ListServiceContainersRequest)(nil), // 36: api.ListServiceContainersRequest + (*ListServiceContainersResponse)(nil), // 37: api.ListServiceContainersResponse + (*MachineServiceContainers)(nil), // 38: api.MachineServiceContainers + (*UpdateServiceContainerSpecRequest)(nil), // 39: api.UpdateServiceContainerSpecRequest + (*Metadata)(nil), // 40: api.Metadata + (*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 42: google.protobuf.Empty } var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{ 9, // 0: api.ListContainersResponse.messages:type_name -> api.MachineContainers - 39, // 1: api.MachineContainers.metadata:type_name -> api.Metadata + 40, // 1: api.MachineContainers.metadata:type_name -> api.Metadata 12, // 2: api.ExecContainerRequest.config:type_name -> api.ExecConfig 13, // 3: api.ExecContainerRequest.resize:type_name -> api.ResizeEvent 0, // 4: api.ContainerLogEntry.stream:type_name -> api.ContainerLogEntry.StreamType - 40, // 5: api.ContainerLogEntry.timestamp:type_name -> google.protobuf.Timestamp + 41, // 5: api.ContainerLogEntry.timestamp:type_name -> google.protobuf.Timestamp 21, // 6: api.InspectImageResponse.messages:type_name -> api.Image - 39, // 7: api.Image.metadata:type_name -> api.Metadata + 40, // 7: api.Image.metadata:type_name -> api.Metadata 24, // 8: api.InspectRemoteImageResponse.messages:type_name -> api.RemoteImage - 39, // 9: api.RemoteImage.metadata:type_name -> api.Metadata + 40, // 9: api.RemoteImage.metadata:type_name -> api.Metadata 27, // 10: api.ListImagesResponse.messages:type_name -> api.MachineImages - 39, // 11: api.MachineImages.metadata:type_name -> api.Metadata + 40, // 11: api.MachineImages.metadata:type_name -> api.Metadata 32, // 12: api.ListVolumesResponse.messages:type_name -> api.MachineVolumes - 39, // 13: api.MachineVolumes.metadata:type_name -> api.Metadata + 40, // 13: api.MachineVolumes.metadata:type_name -> api.Metadata 38, // 14: api.ListServiceContainersResponse.messages:type_name -> api.MachineServiceContainers - 39, // 15: api.MachineServiceContainers.metadata:type_name -> api.Metadata + 40, // 15: api.MachineServiceContainers.metadata:type_name -> api.Metadata 35, // 16: api.MachineServiceContainers.containers:type_name -> api.ServiceContainer 1, // 17: api.Docker.CreateContainer:input_type -> api.CreateContainerRequest 3, // 18: api.Docker.InspectContainer:input_type -> api.InspectContainerRequest @@ -2651,27 +2721,29 @@ var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{ 3, // 33: api.Docker.InspectServiceContainer:input_type -> api.InspectContainerRequest 36, // 34: api.Docker.ListServiceContainers:input_type -> api.ListServiceContainersRequest 10, // 35: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest - 2, // 36: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse - 4, // 37: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse - 41, // 38: api.Docker.StartContainer:output_type -> google.protobuf.Empty - 41, // 39: api.Docker.StopContainer:output_type -> google.protobuf.Empty - 8, // 40: api.Docker.ListContainers:output_type -> api.ListContainersResponse - 41, // 41: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty - 14, // 42: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse - 16, // 43: api.Docker.ContainerLogs:output_type -> api.ContainerLogEntry - 18, // 44: api.Docker.PullImage:output_type -> api.JSONMessage - 20, // 45: api.Docker.InspectImage:output_type -> api.InspectImageResponse - 23, // 46: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse - 26, // 47: api.Docker.ListImages:output_type -> api.ListImagesResponse - 29, // 48: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse - 31, // 49: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse - 41, // 50: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty - 2, // 51: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse - 35, // 52: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer - 37, // 53: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse - 41, // 54: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty - 36, // [36:55] is the sub-list for method output_type - 17, // [17:36] is the sub-list for method input_type + 39, // 36: api.Docker.UpdateServiceContainerSpec:input_type -> api.UpdateServiceContainerSpecRequest + 2, // 37: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse + 4, // 38: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse + 42, // 39: api.Docker.StartContainer:output_type -> google.protobuf.Empty + 42, // 40: api.Docker.StopContainer:output_type -> google.protobuf.Empty + 8, // 41: api.Docker.ListContainers:output_type -> api.ListContainersResponse + 42, // 42: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty + 14, // 43: api.Docker.ExecContainer:output_type -> api.ExecContainerResponse + 16, // 44: api.Docker.ContainerLogs:output_type -> api.ContainerLogEntry + 18, // 45: api.Docker.PullImage:output_type -> api.JSONMessage + 20, // 46: api.Docker.InspectImage:output_type -> api.InspectImageResponse + 23, // 47: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse + 26, // 48: api.Docker.ListImages:output_type -> api.ListImagesResponse + 29, // 49: api.Docker.CreateVolume:output_type -> api.CreateVolumeResponse + 31, // 50: api.Docker.ListVolumes:output_type -> api.ListVolumesResponse + 42, // 51: api.Docker.RemoveVolume:output_type -> google.protobuf.Empty + 2, // 52: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse + 35, // 53: api.Docker.InspectServiceContainer:output_type -> api.ServiceContainer + 37, // 54: api.Docker.ListServiceContainers:output_type -> api.ListServiceContainersResponse + 42, // 55: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty + 42, // 56: api.Docker.UpdateServiceContainerSpec:output_type -> google.protobuf.Empty + 37, // [37:57] is the sub-list for method output_type + 17, // [17:37] is the sub-list for method input_type 17, // [17:17] is the sub-list for extension type_name 17, // [17:17] is the sub-list for extension extendee 0, // [0:17] is the sub-list for field type_name @@ -3140,6 +3212,18 @@ func file_internal_machine_api_pb_docker_proto_init() { return nil } } + file_internal_machine_api_pb_docker_proto_msgTypes[38].Exporter = func(v any, i int) any { + switch v := v.(*UpdateServiceContainerSpecRequest); 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[10].OneofWrappers = []any{ (*ExecContainerRequest_Config)(nil), @@ -3158,7 +3242,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: 1, - NumMessages: 38, + NumMessages: 39, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/machine/api/pb/docker.proto b/internal/machine/api/pb/docker.proto index 4f4370db..b8a02d24 100644 --- a/internal/machine/api/pb/docker.proto +++ b/internal/machine/api/pb/docker.proto @@ -2,12 +2,12 @@ syntax = "proto3"; package api; -option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; - import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "internal/machine/api/pb/common.proto"; +option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; + service Docker { rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse); rpc InspectContainer(InspectContainerRequest) returns (InspectContainerResponse); @@ -34,6 +34,7 @@ service Docker { rpc InspectServiceContainer(InspectContainerRequest) returns (ServiceContainer); rpc ListServiceContainers(ListServiceContainersRequest) returns (ListServiceContainersResponse); rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty); + rpc UpdateServiceContainerSpec(UpdateServiceContainerSpecRequest) returns (google.protobuf.Empty); } message CreateContainerRequest { @@ -276,3 +277,9 @@ message MachineServiceContainers { Metadata metadata = 1; repeated ServiceContainer containers = 2; } + +message UpdateServiceContainerSpecRequest { + string container_id = 1; + // JSON serialised api.ServiceSpec. + bytes service_spec = 2; +} diff --git a/internal/machine/api/pb/docker_grpc.pb.go b/internal/machine/api/pb/docker_grpc.pb.go index 4362b5b6..5ac0db93 100644 --- a/internal/machine/api/pb/docker_grpc.pb.go +++ b/internal/machine/api/pb/docker_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.27.3 +// - protoc v5.29.3 // source: internal/machine/api/pb/docker.proto package pb @@ -20,25 +20,26 @@ 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_ExecContainer_FullMethodName = "/api.Docker/ExecContainer" - Docker_ContainerLogs_FullMethodName = "/api.Docker/ContainerLogs" - Docker_PullImage_FullMethodName = "/api.Docker/PullImage" - Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" - Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" - Docker_ListImages_FullMethodName = "/api.Docker/ListImages" - Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume" - Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes" - Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume" - Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" - Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" - Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" - 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_ExecContainer_FullMethodName = "/api.Docker/ExecContainer" + Docker_ContainerLogs_FullMethodName = "/api.Docker/ContainerLogs" + Docker_PullImage_FullMethodName = "/api.Docker/PullImage" + Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" + Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" + Docker_ListImages_FullMethodName = "/api.Docker/ListImages" + Docker_CreateVolume_FullMethodName = "/api.Docker/CreateVolume" + Docker_ListVolumes_FullMethodName = "/api.Docker/ListVolumes" + Docker_RemoveVolume_FullMethodName = "/api.Docker/RemoveVolume" + Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" + Docker_InspectServiceContainer_FullMethodName = "/api.Docker/InspectServiceContainer" + Docker_ListServiceContainers_FullMethodName = "/api.Docker/ListServiceContainers" + Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" + Docker_UpdateServiceContainerSpec_FullMethodName = "/api.Docker/UpdateServiceContainerSpec" ) // DockerClient is the client API for Docker service. @@ -66,6 +67,9 @@ type DockerClient interface { 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) + // UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. + // Used for updating metadata like deploy labels that don't require container recreation. + UpdateServiceContainerSpec(ctx context.Context, in *UpdateServiceContainerSpecRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type dockerClient struct { @@ -287,6 +291,16 @@ func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveCon return out, nil } +func (c *dockerClient) UpdateServiceContainerSpec(ctx context.Context, in *UpdateServiceContainerSpecRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Docker_UpdateServiceContainerSpec_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // DockerServer is the server API for Docker service. // All implementations must embed UnimplementedDockerServer // for forward compatibility. @@ -312,6 +326,9 @@ type DockerServer interface { InspectServiceContainer(context.Context, *InspectContainerRequest) (*ServiceContainer, error) ListServiceContainers(context.Context, *ListServiceContainersRequest) (*ListServiceContainersResponse, error) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) + // UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. + // Used for updating metadata like deploy labels that don't require container recreation. + UpdateServiceContainerSpec(context.Context, *UpdateServiceContainerSpecRequest) (*emptypb.Empty, error) mustEmbedUnimplementedDockerServer() } @@ -379,6 +396,9 @@ func (UnimplementedDockerServer) ListServiceContainers(context.Context, *ListSer func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented") } +func (UnimplementedDockerServer) UpdateServiceContainerSpec(context.Context, *UpdateServiceContainerSpecRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateServiceContainerSpec not implemented") +} func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {} func (UnimplementedDockerServer) testEmbeddedByValue() {} @@ -717,6 +737,24 @@ func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Docker_UpdateServiceContainerSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateServiceContainerSpecRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DockerServer).UpdateServiceContainerSpec(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Docker_UpdateServiceContainerSpec_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DockerServer).UpdateServiceContainerSpec(ctx, req.(*UpdateServiceContainerSpecRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Docker_ServiceDesc is the grpc.ServiceDesc for Docker service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -788,6 +826,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{ MethodName: "RemoveServiceContainer", Handler: _Docker_RemoveServiceContainer_Handler, }, + { + MethodName: "UpdateServiceContainerSpec", + Handler: _Docker_UpdateServiceContainerSpec_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/machine/cluster.go b/internal/machine/cluster.go index a42493c2..5d844f9d 100644 --- a/internal/machine/cluster.go +++ b/internal/machine/cluster.go @@ -67,6 +67,7 @@ func newClusterController( dnsServer *dns.Server, dnsResolver *dns.ClusterResolver, unregistry *unregistry.Registry, + storeSync <-chan struct{}, ) (*clusterController, error) { slog.Info("Starting WireGuard network.") wgnet, err := network.NewWireGuardNetwork() @@ -82,7 +83,7 @@ func newClusterController( endpointChanges: endpointChanges, server: server, corroService: corroService, - dockerCtrl: docker.NewController(state.ID, dockerService, store), + dockerCtrl: docker.NewController(state.ID, dockerService, store, storeSync), dockerReady: dockerReady, clusterReady: clusterReady, caddyconfigCtrl: caddyfileCtrl, diff --git a/internal/machine/docker/client.go b/internal/machine/docker/client.go index 25dfe66f..627c38d6 100644 --- a/internal/machine/docker/client.go +++ b/internal/machine/docker/client.go @@ -536,3 +536,25 @@ func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts con } return err } + +// UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. +// Used for updating metadata like deploy labels that don't require container recreation. +func (c *Client) UpdateServiceContainerSpec(ctx context.Context, containerID string, spec api.ServiceSpec) error { + specBytes, err := json.Marshal(spec) + if err != nil { + return fmt.Errorf("marshal service spec: %w", err) + } + + _, err = c.GRPCClient.UpdateServiceContainerSpec(ctx, &pb.UpdateServiceContainerSpecRequest{ + ContainerId: containerID, + ServiceSpec: specBytes, + }) + if err != nil { + if status.Convert(err).Code() == codes.NotFound { + return errdefs.NotFound(err) + } + return err + } + + return nil +} diff --git a/internal/machine/docker/controller.go b/internal/machine/docker/controller.go index 1b5317d1..ece22852 100644 --- a/internal/machine/docker/controller.go +++ b/internal/machine/docker/controller.go @@ -30,14 +30,17 @@ type Controller struct { client *client.Client service *Service store *store.Store + // sync receives signals to trigger an immediate sync to the cluster store. + sync <-chan struct{} } -func NewController(machineID string, service *Service, store *store.Store) *Controller { +func NewController(machineID string, service *Service, store *store.Store, sync <-chan struct{}) *Controller { return &Controller{ machineID: machineID, client: service.Client, service: service, store: store, + sync: sync, } } @@ -137,6 +140,11 @@ func (c *Controller) WatchAndSyncContainers(ctx context.Context) error { if err := c.syncContainersToStore(ctx); err != nil { return fmt.Errorf("sync containers to cluster store: %w", err) } + case <-c.sync: + slog.Debug("Syncing containers to cluster store triggered by spec update.") + if err := c.syncContainersToStore(ctx); err != nil { + return fmt.Errorf("sync containers to cluster store: %w", err) + } case err := <-errCh: if errors.Is(err, context.Canceled) { return nil diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index 8f2437e1..22dd64de 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -68,6 +68,8 @@ type Server struct { networkReady func() bool // waitForNetworkReady is a function that waits for the Docker network to be ready for containers. waitForNetworkReady func(ctx context.Context) error + // storeSync signals the Controller to immediately sync containers to the cluster store. + storeSync chan<- struct{} } type ServerOptions struct { @@ -76,6 +78,9 @@ type ServerOptions struct { // API server but in this case we should probably fail until the cluster is initialised. NetworkReady func() bool WaitForNetworkReady func(ctx context.Context) error + // StoreSync signals the Controller to immediately sync containers to the cluster store. + // Used when spec updates need immediate cluster-wide visibility (e.g., deploy labels). + StoreSync chan<- struct{} } // NewServer creates a new Docker gRPC server with the provided Docker service. @@ -90,6 +95,7 @@ func NewServer(service *Service, db *sqlx.DB, internalDNSIP func() netip.Addr, m s.networkReady = opts.NetworkReady s.waitForNetworkReady = opts.WaitForNetworkReady + s.storeSync = opts.StoreSync return s } @@ -549,19 +555,24 @@ func (s *Server) CreateServiceContainer( } } + // Start with user labels (from service.labels in Compose), then overlay system labels (system takes precedence). + containerLabels := make(map[string]string) + for k, v := range spec.Labels { + containerLabels[k] = v + } + containerLabels[api.LabelServiceID] = req.ServiceId + containerLabels[api.LabelServiceName] = spec.Name + containerLabels[api.LabelServiceMode] = spec.Mode + containerLabels[api.LabelManaged] = "" + config := &container.Config{ Cmd: spec.Container.Command, Env: envVars.ToSlice(), Entrypoint: spec.Container.Entrypoint, Hostname: containerName, Image: spec.Container.Image, - Labels: map[string]string{ - api.LabelServiceID: req.ServiceId, - api.LabelServiceName: spec.Name, - api.LabelServiceMode: spec.Mode, - api.LabelManaged: "", - }, - User: spec.Container.User, + Labels: containerLabels, + User: spec.Container.User, } if spec.Mode == "" { config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated @@ -1018,6 +1029,57 @@ func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveConta return resp, nil } +// UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. +// Used for updating metadata like deploy labels that don't require container recreation. +func (s *Server) UpdateServiceContainerSpec( + ctx context.Context, req *pb.UpdateServiceContainerSpecRequest, +) (*emptypb.Empty, error) { + // Validate the spec. + var spec api.ServiceSpec + if err := json.Unmarshal(req.ServiceSpec, &spec); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "unmarshal service spec: %v", err) + } + + // Validate the provided spec. + spec = spec.SetDefaults() + if err := spec.Validate(); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid service spec: %v", err) + } + + // Serialize back for storage. + specBytes, err := json.Marshal(spec) + if err != nil { + return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err) + } + + // Update in local DB. + result, err := s.db.ExecContext(ctx, + `UPDATE containers SET service_spec = $1, updated_at = datetime('subsecond') WHERE id = $2`, + string(specBytes), req.ContainerId) + if err != nil { + return nil, status.Errorf(codes.Internal, "update container spec: %v", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, status.Errorf(codes.Internal, "get rows affected: %v", err) + } + if rowsAffected == 0 { + return nil, status.Errorf(codes.NotFound, "container not found: %s", req.ContainerId) + } + + // Trigger immediate sync to cluster store for cluster-wide visibility. + if s.storeSync != nil { + select { + case s.storeSync <- struct{}{}: + default: + // Channel full, sync already pending. + } + } + + return &emptypb.Empty{}, nil +} + // logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream. const logsHeartbeatInterval = 200 * time.Millisecond diff --git a/internal/machine/machine.go b/internal/machine/machine.go index 0f726dfc..0a73ef78 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -195,6 +195,9 @@ type Machine struct { // dockerService provides high-level operations for managing Docker containers. dockerService *machinedocker.Service dockerServer *machinedocker.Server + // storeSync is shared between the docker Server (sends) and Controller (receives) + // to trigger immediate container sync to the cluster store. + storeSync chan struct{} // localMachineServer is the gRPC server for the machine API listening on the local Unix socket. localMachineServer *grpc.Server @@ -285,6 +288,8 @@ func NewMachine(config *Config) (*Machine, error) { dockerService: dockerService, localProxyServer: localProxyServer, proxyDirector: proxyDirector, + // Buffered channel to avoid blocking when triggering sync. + storeSync: make(chan struct{}, 1), } // Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function. @@ -298,6 +303,7 @@ func NewMachine(config *Config) (*Machine, error) { m.dockerServer = machinedocker.NewServer(dockerService, db, internalDNSIP, machineID, machinedocker.ServerOptions{ NetworkReady: m.IsNetworkReady, WaitForNetworkReady: m.WaitForNetworkReady, + StoreSync: m.storeSync, }) caddyServer := caddyconfig.NewServer(caddyconfig.NewService(config.CaddyConfigDir)) m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer) @@ -489,6 +495,7 @@ func (m *Machine) Run(ctx context.Context) error { dnsServer, dnsResolver, unreg, + m.storeSync, ) m.mu.Unlock() if err != nil { diff --git a/pkg/api/client.go b/pkg/api/client.go index 8bd920dd..896232b5 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -26,6 +26,9 @@ type ContainerClient interface { StartContainer(ctx context.Context, serviceNameOrID, containerNameOrID string) error StopContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, opts container.StopOptions) error ExecContainer(ctx context.Context, serviceNameOrID, containerNameOrID string, config ExecOptions) (int, error) + // UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. + // Used for updating metadata like deploy labels that don't require container recreation. + UpdateServiceContainerSpec(ctx context.Context, machineID, containerID string, spec ServiceSpec) error } type DNSClient interface { diff --git a/pkg/api/service.go b/pkg/api/service.go index 84a772ef..a8613879 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -63,6 +63,12 @@ type ServiceSpec struct { Volumes []VolumeSpec // Configs is list of configuration objects that can be mounted into the container. Configs []ConfigSpec + // Labels are user-defined metadata applied to service containers (from service.labels in Compose). + // System labels (prefixed with "uncloud.") are managed separately and take precedence. + Labels map[string]string `json:",omitempty"` + // DeployLabels are service-level metadata (from deploy.labels in Compose). + // These can be updated without recreating containers. + DeployLabels map[string]string `json:",omitempty"` } // CaddyConfig returns the Caddy reverse proxy configuration for the service or an empty string if it's not defined. @@ -219,6 +225,13 @@ func (s *ServiceSpec) Clone() ServiceSpec { } } + if s.Labels != nil { + spec.Labels = maps.Clone(s.Labels) + } + if s.DeployLabels != nil { + spec.DeployLabels = maps.Clone(s.DeployLabels) + } + return spec } @@ -487,13 +500,13 @@ func ServiceFromProto(s *pb.Service) (Service, error) { } func machineContainerFromProto(sc *pb.Service_Container) (MachineServiceContainer, error) { - var c Container + var c ServiceContainer if err := json.Unmarshal(sc.Container, &c); err != nil { return MachineServiceContainer{}, fmt.Errorf("unmarshal container: %w", err) } return MachineServiceContainer{ MachineID: sc.MachineId, - Container: ServiceContainer{Container: c}, + Container: c, }, nil } diff --git a/pkg/api/service_test.go b/pkg/api/service_test.go index 3654780d..5a5e004e 100644 --- a/pkg/api/service_test.go +++ b/pkg/api/service_test.go @@ -206,6 +206,61 @@ func TestServiceSpec_Validate_CaddyAndPorts(t *testing.T) { } } +func TestServiceSpec_Clone_Labels(t *testing.T) { + original := ServiceSpec{ + Name: "test", + Container: ContainerSpec{ + Image: "nginx:latest", + }, + Labels: map[string]string{ + "app": "test", + "version": "1.0", + }, + DeployLabels: map[string]string{ + "deploy_id": "abc123", + "env": "prod", + }, + } + + cloned := original.Clone() + + // Verify the cloned values match the original + assert.Equal(t, original.Labels, cloned.Labels) + assert.Equal(t, original.DeployLabels, cloned.DeployLabels) + + // Modify the original to verify deep copy + stringModified := "modified" + original.Labels["app"] = stringModified + original.Labels["new_key"] = "new_value" + original.DeployLabels["deploy_id"] = stringModified + original.DeployLabels["new_deploy_key"] = "new_value" + + // Assert cloned values are unchanged + assert.Equal(t, "test", cloned.Labels["app"]) + assert.Equal(t, "1.0", cloned.Labels["version"]) + assert.NotContains(t, cloned.Labels, "new_key") + assert.Equal(t, "abc123", cloned.DeployLabels["deploy_id"]) + assert.Equal(t, "prod", cloned.DeployLabels["env"]) + assert.NotContains(t, cloned.DeployLabels, "new_deploy_key") +} + +func TestServiceSpec_Clone_NilLabels(t *testing.T) { + original := ServiceSpec{ + Name: "test", + Container: ContainerSpec{ + Image: "nginx:latest", + }, + Labels: nil, + DeployLabels: nil, + } + + cloned := original.Clone() + + // Verify nil labels remain nil after cloning + assert.Nil(t, cloned.Labels) + assert.Nil(t, cloned.DeployLabels) +} + func TestContainerSpec_Clone(t *testing.T) { mode := os.FileMode(0o644) original := ContainerSpec{ diff --git a/pkg/client/compose/service.go b/pkg/client/compose/service.go index 86f85f35..27674d51 100644 --- a/pkg/client/compose/service.go +++ b/pkg/client/compose/service.go @@ -41,6 +41,9 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser env[k] = *v } + // Extract service-level labels to apply to containers. + containerLabels := mergeLabels(service.Labels, service.CustomLabels) + spec := api.ServiceSpec{ Container: api.ContainerSpec{ CapAdd: service.CapAdd, @@ -56,8 +59,18 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser Sysctls: service.Sysctls, User: service.User, }, - Name: serviceName, - Mode: api.ServiceModeReplicated, + Name: serviceName, + Mode: api.ServiceModeReplicated, + Labels: containerLabels, + } + + // Extract deploy labels into ServiceSpec.DeployLabels (metadata only, not applied to containers). + // DeployLabels can be updated without recreating containers. + if service.Deploy != nil { + labels := mergeLabels(service.Deploy.Labels) + if len(labels) > 0 { + spec.DeployLabels = labels + } } // Map x-caddy extension to spec.Caddy if specified. diff --git a/pkg/client/compose/service_test.go b/pkg/client/compose/service_test.go index e74dfc4e..2fd7c428 100644 --- a/pkg/client/compose/service_test.go +++ b/pkg/client/compose/service_test.go @@ -174,6 +174,14 @@ func TestServiceSpecFromCompose(t *testing.T) { }, }, Replicas: 3, + Labels: map[string]string{ + "app": "test", + "version": "1.0", + }, + DeployLabels: map[string]string{ + "deploy_id": "abc123", + "env": "prod", + }, Volumes: []api.VolumeSpec{ { Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90", diff --git a/pkg/client/compose/testdata/compose-full-spec.yaml b/pkg/client/compose/testdata/compose-full-spec.yaml index c7fb89d7..e6208ce5 100644 --- a/pkg/client/compose/testdata/compose-full-spec.yaml +++ b/pkg/client/compose/testdata/compose-full-spec.yaml @@ -26,6 +26,13 @@ services: sysctls: - net.ipv4.ip_forward=1 user: nginx:nginx + labels: + app: test + version: "1.0" + deploy: + labels: + deploy_id: abc123 + env: prod volumes: - /etc/passwd:/host/etc/passwd:ro - data1:/data1 diff --git a/pkg/client/container.go b/pkg/client/container.go index 7c05c58e..a151b634 100644 --- a/pkg/client/container.go +++ b/pkg/client/container.go @@ -309,6 +309,24 @@ func (cli *Client) RemoveContainer( return nil } +// UpdateServiceContainerSpec updates the stored service spec for a container without recreating it. +// Used for updating metadata like deploy labels that don't require container recreation. +func (cli *Client) UpdateServiceContainerSpec( + ctx context.Context, machineID, containerID string, spec api.ServiceSpec, +) error { + machine, err := cli.InspectMachine(ctx, machineID) + if err != nil { + return fmt.Errorf("inspect machine '%s': %w", machineID, err) + } + ctx = proxyToMachine(ctx, machine.Machine) + + if err := cli.Docker.UpdateServiceContainerSpec(ctx, containerID, spec); err != nil { + return fmt.Errorf("update service container spec: %w", err) + } + + return nil +} + // ExecContainer executes a command in a container within the service. // If containerNameOrID is empty, the first container in the service will be used. func (cli *Client) ExecContainer( diff --git a/pkg/client/deploy/container.go b/pkg/client/deploy/container.go index 361740d1..4ec7beb4 100644 --- a/pkg/client/deploy/container.go +++ b/pkg/client/deploy/container.go @@ -1,6 +1,7 @@ package deploy import ( + "maps" "reflect" "sort" @@ -12,9 +13,10 @@ import ( type ContainerSpecStatus string const ( - ContainerUpToDate ContainerSpecStatus = "up-to-date" - ContainerNeedsUpdate ContainerSpecStatus = "needs-update" - ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate" + ContainerUpToDate ContainerSpecStatus = "up-to-date" + ContainerNeedsUpdate ContainerSpecStatus = "needs-update" + ContainerNeedsRecreate ContainerSpecStatus = "needs-recreate" + ContainerNeedsSpecUpdate ContainerSpecStatus = "needs-spec-update" ) func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) ContainerSpecStatus { @@ -83,6 +85,11 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta } } + // Labels require container recreation if changed. + if !maps.Equal(current.Labels, new.Labels) { + return ContainerNeedsRecreate + } + // Device reservations are immutable, so we'll need to recreate if any have changed if !reflect.DeepEqual(current.Container.Resources.DeviceReservations, newResources.DeviceReservations) { return ContainerNeedsRecreate @@ -98,6 +105,11 @@ func EvalContainerSpecChange(current api.ServiceSpec, new api.ServiceSpec) Conta return ContainerNeedsUpdate } + // Deploy labels only need spec update (no container recreation). + if !maps.Equal(current.DeployLabels, new.DeployLabels) { + return ContainerNeedsSpecUpdate + } + return ContainerUpToDate } diff --git a/pkg/client/deploy/container_test.go b/pkg/client/deploy/container_test.go index 355d63bc..8ddf0745 100644 --- a/pkg/client/deploy/container_test.go +++ b/pkg/client/deploy/container_test.go @@ -1886,3 +1886,130 @@ func TestEvalContainerSpecChange_Mixed(t *testing.T) { }) } } + +func TestEvalContainerSpecChange_LabelsAndDeployLabels(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + currentLabels map[string]string + newLabels map[string]string + currentDeploy map[string]string + newDeploy map[string]string + want ContainerSpecStatus + }{ + // Container labels (service.labels) tests + { + name: "labels: empty", + currentLabels: nil, + newLabels: nil, + want: ContainerUpToDate, + }, + { + name: "labels: identical", + currentLabels: map[string]string{"app": "test"}, + newLabels: map[string]string{"app": "test"}, + want: ContainerUpToDate, + }, + { + name: "labels: add", + currentLabels: nil, + newLabels: map[string]string{"app": "test"}, + want: ContainerNeedsRecreate, + }, + { + name: "labels: remove", + currentLabels: map[string]string{"app": "test"}, + newLabels: nil, + want: ContainerNeedsRecreate, + }, + { + name: "labels: change value", + currentLabels: map[string]string{"app": "test"}, + newLabels: map[string]string{"app": "modified"}, + want: ContainerNeedsRecreate, + }, + { + name: "labels: add additional", + currentLabels: map[string]string{"app": "test"}, + newLabels: map[string]string{"app": "test", "env": "prod"}, + want: ContainerNeedsRecreate, + }, + // Deploy labels (deploy.labels) tests + { + name: "deploy labels: empty", + currentDeploy: nil, + newDeploy: nil, + want: ContainerUpToDate, + }, + { + name: "deploy labels: identical", + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: map[string]string{"deploy_id": "abc123"}, + want: ContainerUpToDate, + }, + { + name: "deploy labels: add", + currentDeploy: nil, + newDeploy: map[string]string{"deploy_id": "abc123"}, + want: ContainerNeedsSpecUpdate, + }, + { + name: "deploy labels: remove", + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: nil, + want: ContainerNeedsSpecUpdate, + }, + { + name: "deploy labels: change value", + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: map[string]string{"deploy_id": "def456"}, + want: ContainerNeedsSpecUpdate, + }, + { + name: "deploy labels: add additional", + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: map[string]string{"deploy_id": "abc123", "version": "v2"}, + want: ContainerNeedsSpecUpdate, + }, + // Combined tests + { + name: "both: labels changed takes precedence over deploy labels", + currentLabels: map[string]string{"app": "test"}, + newLabels: map[string]string{"app": "modified"}, + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: map[string]string{"deploy_id": "def456"}, + want: ContainerNeedsRecreate, + }, + { + name: "both: only deploy labels changed", + currentLabels: map[string]string{"app": "test"}, + newLabels: map[string]string{"app": "test"}, + currentDeploy: map[string]string{"deploy_id": "abc123"}, + newDeploy: map[string]string{"deploy_id": "def456"}, + want: ContainerNeedsSpecUpdate, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + currentSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + Labels: tt.currentLabels, + DeployLabels: tt.currentDeploy, + } + newSpec := api.ServiceSpec{ + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + Labels: tt.newLabels, + DeployLabels: tt.newDeploy, + } + + result := EvalContainerSpecChange(currentSpec, newSpec) + assert.Equal(t, tt.want, result) + }) + } +} diff --git a/pkg/client/deploy/operation.go b/pkg/client/deploy/operation.go index 78a94e68..64e4e34c 100644 --- a/pkg/client/deploy/operation.go +++ b/pkg/client/deploy/operation.go @@ -158,6 +158,31 @@ func (o *CreateVolumeOperation) String() string { o.MachineID, o.VolumeSpec.DockerVolumeName()) } +// UpdateSpecOperation updates the stored service spec for a container without recreating it. +// Used for updating metadata like deploy labels that don't require container recreation. +type UpdateSpecOperation struct { + MachineID string + ContainerID string + NewSpec api.ServiceSpec +} + +func (o *UpdateSpecOperation) Execute(ctx context.Context, cli Client) error { + if err := cli.UpdateServiceContainerSpec(ctx, o.MachineID, o.ContainerID, o.NewSpec); err != nil { + return fmt.Errorf("update service container spec: %w", err) + } + return nil +} + +func (o *UpdateSpecOperation) Format(resolver NameResolver) string { + machineName := resolver.MachineName(o.MachineID) + return fmt.Sprintf("%s: Update spec [container=%s]", machineName, o.ContainerID[:12]) +} + +func (o *UpdateSpecOperation) String() string { + return fmt.Sprintf("UpdateSpecOperation[machine_id=%s container_id=%s]", + o.MachineID, o.ContainerID) +} + // SequenceOperation is a composite operation that executes a sequence of operations in order. type SequenceOperation struct { Operations []Operation diff --git a/pkg/client/deploy/strategy.go b/pkg/client/deploy/strategy.go index 09cd07aa..3fcf421c 100644 --- a/pkg/client/deploy/strategy.go +++ b/pkg/client/deploy/strategy.go @@ -156,7 +156,17 @@ func (s *RollingStrategy) planReplicated(svc *api.Service, spec api.ServiceSpec) containersOnMachine[m.Id] = containers[1:] if status, ok := containerSpecStatuses[ctr.ID]; ok { // Contains statuses for only running containers. - if status == ContainerUpToDate { + switch status { + case ContainerUpToDate: + continue + case ContainerNeedsSpecUpdate: + // Only the stored spec needs updating (e.g., deploy labels changed). + // No container recreation needed. + plan.Operations = append(plan.Operations, &UpdateSpecOperation{ + MachineID: m.Id, + ContainerID: ctr.ID, + NewSpec: spec, + }) continue } // TODO: handle ContainerNeedsUpdate when update of mutable fields on a container is supported. @@ -269,7 +279,10 @@ func reconcileGlobalContainer( } // Check if there is a container with the same spec already running. If so, remove the rest. + // Also handle containers that only need spec updates (e.g., deploy labels changed). upToDate := false + specUpdateContainer := (*api.MachineServiceContainer)(nil) + var specUpdateIdx int for i, c := range containers { if !c.Container.State.Running || c.Container.State.Paused { // Skip containers that are not running. @@ -297,12 +310,37 @@ func reconcileGlobalContainer( } break } + + if status == ContainerNeedsSpecUpdate && specUpdateContainer == nil { + // Track the first container that only needs spec update. + specUpdateContainer = &containers[i] + specUpdateIdx = i + } // TODO: handle ContainerNeedsUpdate when update of mutable fields on a container is supported. } if upToDate { return ops, nil } + // If we found a container that only needs spec update, update it and remove the rest. + if specUpdateContainer != nil { + ops = append(ops, &UpdateSpecOperation{ + MachineID: specUpdateContainer.MachineID, + ContainerID: specUpdateContainer.Container.ID, + NewSpec: spec, + }) + for j, old := range containers { + if j == specUpdateIdx { + continue + } + ops = append(ops, &RemoveContainerOperation{ + MachineID: old.MachineID, + Container: old.Container, + }) + } + return ops, nil + } + // The machine has containers but none of them match the new spec. // Stop the old running containers that have conflicting ports with the new spec before running a new one. for _, c := range containers { diff --git a/pkg/client/deploy/strategy_test.go b/pkg/client/deploy/strategy_test.go new file mode 100644 index 00000000..57edf948 --- /dev/null +++ b/pkg/client/deploy/strategy_test.go @@ -0,0 +1,533 @@ +package deploy + +import ( + "testing" + + "github.com/docker/docker/api/types/container" + "github.com/psviderski/uncloud/internal/machine/api/pb" + "github.com/psviderski/uncloud/pkg/api" + "github.com/psviderski/uncloud/pkg/client/deploy/scheduler" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Helper functions to create test data. + +func newMachine(id, name string) *scheduler.Machine { + return &scheduler.Machine{ + Info: &pb.MachineInfo{ + Id: id, + Name: name, + }, + } +} + +func newClusterState(machines ...*scheduler.Machine) *scheduler.ClusterState { + return &scheduler.ClusterState{ + Machines: machines, + } +} + +func newRunningContainer(id string, spec api.ServiceSpec) api.ServiceContainer { + return api.ServiceContainer{ + Container: api.Container{ + InspectResponse: container.InspectResponse{ + ContainerJSONBase: &container.ContainerJSONBase{ + ID: id, + State: &container.State{ + Running: true, + }, + }, + Config: &container.Config{ + Labels: map[string]string{}, + }, + }, + }, + ServiceSpec: spec, + } +} + +func newStoppedContainer(id string, spec api.ServiceSpec) api.ServiceContainer { + return api.ServiceContainer{ + Container: api.Container{ + InspectResponse: container.InspectResponse{ + ContainerJSONBase: &container.ContainerJSONBase{ + ID: id, + State: &container.State{ + Running: false, + }, + }, + Config: &container.Config{ + Labels: map[string]string{}, + }, + }, + }, + ServiceSpec: spec, + } +} + +func newService(id, name string, containers []api.MachineServiceContainer) *api.Service { + return &api.Service{ + ID: id, + Name: name, + Containers: containers, + } +} + +func baseSpec(name string) api.ServiceSpec { + return api.ServiceSpec{ + Name: name, + Mode: api.ServiceModeReplicated, + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + Replicas: 1, + } +} + +func globalSpec(name string) api.ServiceSpec { + return api.ServiceSpec{ + Name: name, + Mode: api.ServiceModeGlobal, + Container: api.ContainerSpec{ + Image: "nginx:latest", + }, + } +} + +func globalSpecWithDeployLabels(name string, labels map[string]string) api.ServiceSpec { + s := globalSpec(name) + s.DeployLabels = labels + return s +} + +func globalSpecWithLabels(name string, labels map[string]string) api.ServiceSpec { + s := globalSpec(name) + s.Labels = labels + return s +} + +// getOperationsOfType returns all operations of a specific type. +func getOperationsOfType[T Operation](ops []Operation) []T { + var result []T + for _, op := range ops { + if typed, ok := op.(T); ok { + result = append(result, typed) + } + } + return result +} + +// TestRollingStrategy_Replicated_UpToDate tests that no operations are generated +// when containers are already up-to-date. +func TestRollingStrategy_Replicated_UpToDate(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + spec := baseSpec("test-service") + spec.Replicas = 1 + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", spec), + }, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, spec) + + require.NoError(t, err) + assert.Empty(t, plan.Operations, "expected no operations when container is up-to-date") +} + +// TestRollingStrategy_Replicated_NeedsSpecUpdate tests that only UpdateSpecOperation +// is generated when deploy labels change (no container recreation). +func TestRollingStrategy_Replicated_NeedsSpecUpdate(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + currentSpec := baseSpec("test-service") + currentSpec.DeployLabels = map[string]string{"version": "v1"} + + newSpec := baseSpec("test-service") + newSpec.DeployLabels = map[string]string{"version": "v2"} + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", currentSpec), + }, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + require.Len(t, plan.Operations, 1, "expected exactly one operation") + + updateOp, ok := plan.Operations[0].(*UpdateSpecOperation) + require.True(t, ok, "expected UpdateSpecOperation, got %T", plan.Operations[0]) + assert.Equal(t, "m1", updateOp.MachineID) + assert.Equal(t, "c1", updateOp.ContainerID) + assert.Equal(t, newSpec, updateOp.NewSpec) +} + +// TestRollingStrategy_Replicated_NeedsRecreate tests that containers are recreated +// when immutable fields change (like Labels). +func TestRollingStrategy_Replicated_NeedsRecreate(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + currentSpec := baseSpec("test-service") + currentSpec.Labels = map[string]string{"app": "old"} + + newSpec := baseSpec("test-service") + newSpec.Labels = map[string]string{"app": "new"} + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", currentSpec), + }, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + assert.Len(t, plan.Operations, 2, "expected run + remove operations") + + // Should have one RunContainerOperation and one RemoveContainerOperation. + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + removeOps := getOperationsOfType[*RemoveContainerOperation](plan.Operations) + + assert.Len(t, runOps, 1, "expected one RunContainerOperation") + assert.Len(t, removeOps, 1, "expected one RemoveContainerOperation") +} + +// TestRollingStrategy_Replicated_ScaleUp tests scaling up adds new containers. +func TestRollingStrategy_Replicated_ScaleUp(t *testing.T) { + t.Parallel() + + machines := []*scheduler.Machine{ + newMachine("m1", "machine-1"), + newMachine("m2", "machine-2"), + } + state := newClusterState(machines...) + + spec := baseSpec("test-service") + spec.Replicas = 1 + + newSpec := baseSpec("test-service") + newSpec.Replicas = 3 + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", spec), + }, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + assert.Len(t, runOps, 2, "expected 2 new containers to be created") +} + +// TestRollingStrategy_Replicated_ScaleDown tests scaling down removes excess containers. +func TestRollingStrategy_Replicated_ScaleDown(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + spec := baseSpec("test-service") + spec.Replicas = 3 + + newSpec := baseSpec("test-service") + newSpec.Replicas = 1 + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", spec)}, + {MachineID: "m1", Container: newRunningContainer("c2", spec)}, + {MachineID: "m1", Container: newRunningContainer("c3", spec)}, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + + removeOps := getOperationsOfType[*RemoveContainerOperation](plan.Operations) + assert.Len(t, removeOps, 2, "expected 2 containers to be removed") +} + +// TestRollingStrategy_Replicated_RecreatePreferredOverSpecUpdate tests that when both +// Labels and DeployLabels change, recreate takes precedence. +func TestRollingStrategy_Replicated_RecreatePreferredOverSpecUpdate(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + currentSpec := baseSpec("test-service") + currentSpec.Labels = map[string]string{"app": "old"} + currentSpec.DeployLabels = map[string]string{"version": "v1"} + + newSpec := baseSpec("test-service") + newSpec.Labels = map[string]string{"app": "new"} + newSpec.DeployLabels = map[string]string{"version": "v2"} + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", currentSpec), + }, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + + // Should recreate, not just update spec. + updateOps := getOperationsOfType[*UpdateSpecOperation](plan.Operations) + assert.Empty(t, updateOps, "should not have UpdateSpecOperation when Labels change") + + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + removeOps := getOperationsOfType[*RemoveContainerOperation](plan.Operations) + assert.Len(t, runOps, 1, "expected one RunContainerOperation") + assert.Len(t, removeOps, 1, "expected one RemoveContainerOperation") +} + +// TestRollingStrategy_Replicated_ForceRecreate tests that ForceRecreate flag +// causes recreation even when spec hasn't changed. +func TestRollingStrategy_Replicated_ForceRecreate(t *testing.T) { + t.Parallel() + + machine := newMachine("m1", "machine-1") + state := newClusterState(machine) + + spec := baseSpec("test-service") + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + { + MachineID: "m1", + Container: newRunningContainer("c1", spec), + }, + }) + + strategy := &RollingStrategy{ForceRecreate: true} + plan, err := strategy.Plan(state, svc, spec) + + require.NoError(t, err) + + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + removeOps := getOperationsOfType[*RemoveContainerOperation](plan.Operations) + assert.Len(t, runOps, 1, "expected container to be recreated") + assert.Len(t, removeOps, 1, "expected old container to be removed") +} + +// TestReconcileGlobalContainer tests the reconcileGlobalContainer function directly. +func TestReconcileGlobalContainer(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + containers []api.MachineServiceContainer + newSpec api.ServiceSpec + forceRecreate bool + wantRunOps int + wantRemoveOps int + wantUpdateOps int + wantStopOps int + }{ + { + name: "no containers - create new", + containers: nil, + newSpec: globalSpec("test"), + wantRunOps: 1, + wantRemoveOps: 0, + wantUpdateOps: 0, + }, + { + name: "container up-to-date - no ops", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpec("test"))}, + }, + newSpec: globalSpec("test"), + wantRunOps: 0, + wantRemoveOps: 0, + wantUpdateOps: 0, + }, + { + name: "container needs spec update only", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpecWithDeployLabels("test", map[string]string{"version": "v1"}))}, + }, + newSpec: globalSpecWithDeployLabels("test", map[string]string{"version": "v2"}), + wantRunOps: 0, + wantRemoveOps: 0, + wantUpdateOps: 1, + }, + { + name: "container needs recreate - labels changed", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpecWithLabels("test", map[string]string{"app": "old"}))}, + }, + newSpec: globalSpecWithLabels("test", map[string]string{"app": "new"}), + wantRunOps: 1, + wantRemoveOps: 1, + wantUpdateOps: 0, + }, + { + name: "multiple containers - one up-to-date - remove extras", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpec("test"))}, + {MachineID: "m1", Container: newRunningContainer("c2", globalSpec("test"))}, + {MachineID: "m1", Container: newRunningContainer("c3", globalSpec("test"))}, + }, + newSpec: globalSpec("test"), + wantRunOps: 0, + wantRemoveOps: 2, // Remove the extras. + wantUpdateOps: 0, + }, + { + name: "multiple containers - one needs spec update - update and remove extras", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpecWithDeployLabels("test", map[string]string{"version": "v1"}))}, + {MachineID: "m1", Container: newRunningContainer("c2", globalSpecWithDeployLabels("test", map[string]string{"version": "v1"}))}, + }, + newSpec: globalSpecWithDeployLabels("test", map[string]string{"version": "v2"}), + wantRunOps: 0, + wantRemoveOps: 1, // Remove the extra. + wantUpdateOps: 1, // Update the first one. + }, + { + name: "multiple containers - none match - recreate", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpecWithLabels("test", map[string]string{"app": "old"}))}, + {MachineID: "m1", Container: newRunningContainer("c2", globalSpecWithLabels("test", map[string]string{"app": "old"}))}, + }, + newSpec: globalSpecWithLabels("test", map[string]string{"app": "new"}), + wantRunOps: 1, + wantRemoveOps: 2, + wantUpdateOps: 0, + }, + { + name: "stopped container ignored - create new", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newStoppedContainer("c1", globalSpec("test"))}, + }, + newSpec: globalSpec("test"), + wantRunOps: 1, + wantRemoveOps: 1, // Remove the stopped one. + wantUpdateOps: 0, + }, + { + name: "force recreate - recreate even if up-to-date", + containers: []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", globalSpec("test"))}, + }, + newSpec: globalSpec("test"), + forceRecreate: true, + wantRunOps: 1, + wantRemoveOps: 1, + wantUpdateOps: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ops, err := reconcileGlobalContainer( + tt.containers, + tt.newSpec, + "svc-1", + "m1", + tt.forceRecreate, + ) + + require.NoError(t, err) + + runOps := getOperationsOfType[*RunContainerOperation](ops) + removeOps := getOperationsOfType[*RemoveContainerOperation](ops) + updateOps := getOperationsOfType[*UpdateSpecOperation](ops) + stopOps := getOperationsOfType[*StopContainerOperation](ops) + + assert.Len(t, runOps, tt.wantRunOps, "RunContainerOperation count mismatch") + assert.Len(t, removeOps, tt.wantRemoveOps, "RemoveContainerOperation count mismatch") + assert.Len(t, updateOps, tt.wantUpdateOps, "UpdateSpecOperation count mismatch") + assert.Len(t, stopOps, tt.wantStopOps, "StopContainerOperation count mismatch") + }) + } +} + +// TestRollingStrategy_Global_NeedsSpecUpdate tests the full Plan flow for global services +// with spec updates. +func TestRollingStrategy_Global_NeedsSpecUpdate(t *testing.T) { + t.Parallel() + + machines := []*scheduler.Machine{ + newMachine("m1", "machine-1"), + newMachine("m2", "machine-2"), + } + state := newClusterState(machines...) + + currentSpec := globalSpec("test-service") + currentSpec.DeployLabels = map[string]string{"version": "v1"} + + newSpec := globalSpec("test-service") + newSpec.DeployLabels = map[string]string{"version": "v2"} + + svc := newService("svc-1", "test-service", []api.MachineServiceContainer{ + {MachineID: "m1", Container: newRunningContainer("c1", currentSpec)}, + {MachineID: "m2", Container: newRunningContainer("c2", currentSpec)}, + }) + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, svc, newSpec) + + require.NoError(t, err) + + updateOps := getOperationsOfType[*UpdateSpecOperation](plan.Operations) + assert.Len(t, updateOps, 2, "expected UpdateSpecOperation for each machine") + + // Verify no recreate operations. + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + removeOps := getOperationsOfType[*RemoveContainerOperation](plan.Operations) + assert.Empty(t, runOps, "should not have RunContainerOperation for spec-only update") + assert.Empty(t, removeOps, "should not have RemoveContainerOperation for spec-only update") +} + +// TestRollingStrategy_NewService tests deploying a brand new service. +func TestRollingStrategy_NewService(t *testing.T) { + t.Parallel() + + machines := []*scheduler.Machine{ + newMachine("m1", "machine-1"), + newMachine("m2", "machine-2"), + } + state := newClusterState(machines...) + + spec := baseSpec("test-service") + spec.Replicas = 2 + + strategy := &RollingStrategy{} + plan, err := strategy.Plan(state, nil, spec) // nil service = new deployment + + require.NoError(t, err) + assert.NotEmpty(t, plan.ServiceID, "should generate new service ID") + + runOps := getOperationsOfType[*RunContainerOperation](plan.Operations) + assert.Len(t, runOps, 2, "expected 2 containers for new service") +} diff --git a/test/e2e/compose_deploy_test.go b/test/e2e/compose_deploy_test.go index efbab9b5..c9ce5cfa 100644 --- a/test/e2e/compose_deploy_test.go +++ b/test/e2e/compose_deploy_test.go @@ -3,6 +3,7 @@ package e2e import ( "context" "testing" + "time" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/volume" @@ -558,4 +559,114 @@ volumes: assert.Len(t, plan.Operations, 2, "Expected 1 volume creation and 1 service to deploy") }) + + t.Run("redeploy with deploy labels updates spec without recreating container", func(t *testing.T) { + t.Parallel() + + name := "test-compose-deploy-labels" + t.Cleanup(func() { + removeServices(t, cli, name) + }) + + // Initial deployment without deploy labels, pinned to machine-1. + initialYAML := ` +services: + test-compose-deploy-labels: + image: portainer/pause:3.9 + environment: + VAR: value + x-machines: machine-1 +` + project, err := compose.LoadProjectFromContent(ctx, initialYAML) + require.NoError(t, err) + + deployment, err := compose.NewDeployment(ctx, cli, project) + require.NoError(t, err) + + err = deployment.Run(ctx) + require.NoError(t, err) + + // Get the container ID after initial deployment. + svc, err := cli.InspectService(ctx, name) + require.NoError(t, err) + require.Len(t, svc.Containers, 1) + + originalContainerID := svc.Containers[0].Container.ID + originalSpec := svc.Containers[0].Container.ServiceSpec + assert.Empty(t, originalSpec.DeployLabels, "Initial spec should have no deploy labels") + + // Redeploy with deploy labels added, still pinned to machine-1. + updatedYAML := ` +services: + test-compose-deploy-labels: + image: portainer/pause:3.9 + environment: + VAR: value + x-machines: machine-1 + deploy: + labels: + app.version: "1.0" + app.environment: "test" +` + project, err = compose.LoadProjectFromContent(ctx, updatedYAML) + 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.Operations, 1, "Expected 1 service plan") + + // The compose plan contains service plans. Unwrap to get the actual operation. + servicePlan, ok := plan.Operations[0].(*deploy.Plan) + require.True(t, ok, "Expected service plan, got %T", plan.Operations[0]) + require.Len(t, servicePlan.Operations, 1, "Expected 1 operation in service plan") + + // Verify the operation is an UpdateSpecOperation. + _, isUpdateSpec := servicePlan.Operations[0].(*deploy.UpdateSpecOperation) + assert.True(t, isUpdateSpec, "Operation should be UpdateSpecOperation, got %T", servicePlan.Operations[0]) + + err = deployment.Run(ctx) + require.NoError(t, err) + + // Verify the container was not recreated. + svc, err = cli.InspectService(ctx, name) + require.NoError(t, err) + require.Len(t, svc.Containers, 1) + + assert.Equal(t, originalContainerID, svc.Containers[0].Container.ID, + "Container should not be recreated when only deploy labels change") + + // Verify the spec was updated with deploy labels. + updatedSpec := svc.Containers[0].Container.ServiceSpec + assert.Equal(t, map[string]string{ + "app.version": "1.0", + "app.environment": "test", + }, updatedSpec.DeployLabels, "Spec should have updated deploy labels") + + // Verify Corrosion replication by reading from a different machine. + // Connect to machine 2 (different from machine 1 used for the update). + cli2, err := c.Machines[1].Connect(ctx) + require.NoError(t, err) + defer cli2.Close() + + // Read from machine 2's Corrosion store - should see the replicated update. + // We use 2 seconds as the timeout: long enough for Corrosion to sync, but short enough + // that we can be confident the sync was triggered immediately (not by the 30s periodic ticker). + require.Eventually(t, func() bool { + svcFromStore, err := cli2.InspectServiceFromStore(ctx, name) + if err != nil { + return false + } + if len(svcFromStore.Containers) == 0 { + return false + } + // Check if deploy labels have been replicated. + spec := svcFromStore.Containers[0].Container.ServiceSpec + return spec.DeployLabels["app.version"] == "1.0" && + spec.DeployLabels["app.environment"] == "test" + }, 2*time.Second, 100*time.Millisecond, + "Corrosion should replicate updated deploy labels to machine 2") + }) } diff --git a/website/docs/8-compose-file-reference/1-support-matrix.md b/website/docs/8-compose-file-reference/1-support-matrix.md index bf28e8d1..62be845f 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -21,7 +21,7 @@ The following table shows the support status for main Compose features: | `gpus` | ✅ Supported | GPU device access | | `image` | ✅ Supported | Container image specification | | `init` | ✅ Supported | Run init process in container | -| `labels` | ❌ Not supported | | +| `labels` | ✅ Supported | Applied to containers | | `links` | ❌ Not supported | Use service names for communication | | `logging` | ✅ Supported | Defaults to [local](https://docs.docker.com/engine/logging/drivers/local/) log driver | | `mem_limit` | ✅ Supported | Memory limit | @@ -39,7 +39,7 @@ The following table shows the support status for main Compose features: | `user` | ✅ Supported | Set container user | | `volumes` | ✅ Supported | Named volumes, bind mounts, tmpfs | | **Deploy** | | | -| `labels` | ❌ Not supported | | +| `labels` | ✅ Supported | Service metadata (updated without container recreation) | | `mode` | ✅ Supported | Either `global` or `replicated` | | `placement` | ❌ Not supported | Use `x-machines` extension | | `replicas` | ✅ Supported | Number of container replicas | diff --git a/website/docs/9-cli-reference/uc_run.md b/website/docs/9-cli-reference/uc_run.md index 84147a80..a74c10fc 100644 --- a/website/docs/9-cli-reference/uc_run.md +++ b/website/docs/9-cli-reference/uc_run.md @@ -15,6 +15,8 @@ uc run IMAGE [COMMAND...] [flags] -e, --env strings Set an environment variable for service containers. Can be specified multiple times. Format: VAR=value or just VAR to use the value from the local environment. -h, --help help for run + -l, --label strings Set a label on service containers. Can be specified multiple times. + Format: key=value -m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine) --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte) diff --git a/website/docs/9-cli-reference/uc_service_run.md b/website/docs/9-cli-reference/uc_service_run.md index de57203a..963023d0 100644 --- a/website/docs/9-cli-reference/uc_service_run.md +++ b/website/docs/9-cli-reference/uc_service_run.md @@ -15,6 +15,8 @@ uc service run IMAGE [COMMAND...] [flags] -e, --env strings Set an environment variable for service containers. Can be specified multiple times. Format: VAR=value or just VAR to use the value from the local environment. -h, --help help for run + -l, --label strings Set a label on service containers. Can be specified multiple times. + Format: key=value -m, --machine strings Placement constraint by machine names, limiting which machines the service can run on. Can be specified multiple times or as a comma-separated list of machine names. (default is any suitable machine) --memory bytes Maximum amount of memory a service container can use. Value is a positive integer with optional unit suffix (b, k, m, g). Default unit is bytes if no suffix specified. Examples: 1073741824, 1024m, 1g (all equal 1 gibibyte)