diff --git a/internal/machine/api/pb/docker.pb.go b/internal/machine/api/pb/docker.pb.go index cae51745..af672504 100644 --- a/internal/machine/api/pb/docker.pb.go +++ b/internal/machine/api/pb/docker.pb.go @@ -1144,7 +1144,7 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x32, 0xf4, 0x05, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, + 0x61, 0x6d, 0x65, 0x32, 0xc3, 0x06, 0x0a, 0x06, 0x44, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, @@ -1191,11 +1191,16 @@ var file_internal_machine_api_pb_docker_proto_rawDesc = []byte{ 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, 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, + 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1251,18 +1256,20 @@ var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{ 12, // 13: api.Docker.InspectImage:input_type -> api.InspectImageRequest 15, // 14: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest 18, // 15: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest - 1, // 16: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse - 3, // 17: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse - 20, // 18: api.Docker.StartContainer:output_type -> google.protobuf.Empty - 20, // 19: api.Docker.StopContainer:output_type -> google.protobuf.Empty - 7, // 20: api.Docker.ListContainers:output_type -> api.ListContainersResponse - 20, // 21: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty - 11, // 22: api.Docker.PullImage:output_type -> api.JSONMessage - 13, // 23: api.Docker.InspectImage:output_type -> api.InspectImageResponse - 16, // 24: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse - 1, // 25: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse - 16, // [16:26] is the sub-list for method output_type - 6, // [6:16] is the sub-list for method input_type + 9, // 16: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest + 1, // 17: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse + 3, // 18: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse + 20, // 19: api.Docker.StartContainer:output_type -> google.protobuf.Empty + 20, // 20: api.Docker.StopContainer:output_type -> google.protobuf.Empty + 7, // 21: api.Docker.ListContainers:output_type -> api.ListContainersResponse + 20, // 22: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty + 11, // 23: api.Docker.PullImage:output_type -> api.JSONMessage + 13, // 24: api.Docker.InspectImage:output_type -> api.InspectImageResponse + 16, // 25: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse + 1, // 26: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse + 20, // 27: api.Docker.RemoveServiceContainer:output_type -> google.protobuf.Empty + 17, // [17:28] is the sub-list for method output_type + 6, // [6:17] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name 6, // [6:6] is the sub-list for extension extendee 0, // [0:6] is the sub-list for field type_name diff --git a/internal/machine/api/pb/docker.proto b/internal/machine/api/pb/docker.proto index 77c0f03c..9d468572 100644 --- a/internal/machine/api/pb/docker.proto +++ b/internal/machine/api/pb/docker.proto @@ -21,6 +21,7 @@ service Docker { rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse); rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse); + rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty); } message CreateContainerRequest { diff --git a/internal/machine/api/pb/docker_grpc.pb.go b/internal/machine/api/pb/docker_grpc.pb.go index f3b6c43d..861fb33c 100644 --- a/internal/machine/api/pb/docker_grpc.pb.go +++ b/internal/machine/api/pb/docker_grpc.pb.go @@ -30,6 +30,7 @@ const ( Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" + Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer" ) // DockerClient is the client API for Docker service. @@ -48,6 +49,7 @@ type DockerClient interface { // Docker auth credentials if necessary. InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) + RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) } type dockerClient struct { @@ -167,6 +169,16 @@ func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateSer return out, nil } +func (c *dockerClient) RemoveServiceContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Docker_RemoveServiceContainer_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. @@ -183,6 +195,7 @@ type DockerServer interface { // Docker auth credentials if necessary. InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) + RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) mustEmbedUnimplementedDockerServer() } @@ -223,6 +236,9 @@ func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRem func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented") } +func (UnimplementedDockerServer) RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RemoveServiceContainer not implemented") +} func (UnimplementedDockerServer) mustEmbedUnimplementedDockerServer() {} func (UnimplementedDockerServer) testEmbeddedByValue() {} @@ -417,6 +433,24 @@ func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Docker_RemoveServiceContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveContainerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DockerServer).RemoveServiceContainer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Docker_RemoveServiceContainer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DockerServer).RemoveServiceContainer(ctx, req.(*RemoveContainerRequest)) + } + 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) @@ -460,6 +494,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateServiceContainer", Handler: _Docker_CreateServiceContainer_Handler, }, + { + MethodName: "RemoveServiceContainer", + Handler: _Docker_RemoveServiceContainer_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/machine/docker/client.go b/internal/machine/docker/client.go index 9e5f45b6..1f479c69 100644 --- a/internal/machine/docker/client.go +++ b/internal/machine/docker/client.go @@ -5,6 +5,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "github.com/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -18,7 +20,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "io" ) // Client is a gRPC client for the Docker service that provides a similar interface to the Docker HTTP client. @@ -367,3 +368,23 @@ func (c *Client) CreateServiceContainer( } return resp, nil } + +// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID. +// A service container is a container that has been created with CreateServiceContainer. +func (c *Client) RemoveServiceContainer(ctx context.Context, id string, opts container.RemoveOptions) error { + optsBytes, err := json.Marshal(opts) + if err != nil { + return fmt.Errorf("marshal options: %w", err) + } + + _, err = c.grpcClient.RemoveServiceContainer(ctx, &pb.RemoveContainerRequest{ + Id: id, + Options: optsBytes, + }) + if err != nil { + if status.Convert(err).Code() == codes.NotFound { + return errdefs.NotFound(err) + } + } + return err +} diff --git a/internal/machine/docker/server.go b/internal/machine/docker/server.go index 5d58552f..f0798a39 100644 --- a/internal/machine/docker/server.go +++ b/internal/machine/docker/server.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "io" + "log/slog" + "regexp" "strconv" "strings" @@ -32,6 +34,8 @@ import ( "google.golang.org/protobuf/types/known/emptypb" ) +var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`) + // Server implements the gRPC Docker service that proxies requests to the Docker daemon. type Server struct { pb.UnimplementedDockerServer @@ -450,3 +454,34 @@ func (s *Server) CreateServiceContainer( return &pb.CreateContainerResponse{Response: respBytes}, nil } + +// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID. +// The difference between this method and RemoveContainer is that it also removes the container from the machine +// database. +func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) { + ctrID := req.Id + // If the ID is not a full Docker ID, inspect the container to get its full ID. + if !fullDockerIDRegex.MatchString(req.Id) { + ctr, err := s.client.ContainerInspect(ctx, req.Id) + if err != nil { + if client.IsErrNotFound(err) { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nil, status.Error(codes.Internal, err.Error()) + } + ctrID = ctr.ID + } + + resp, err := s.RemoveContainer(ctx, req) + if err != nil { + return nil, err + } + + if _, err = s.db.ExecContext(ctx, `DELETE FROM containers WHERE id = $1`, ctrID); err != nil { + slog.Error("Failed to remove container from machine database.", "err", err, "id", ctrID) + // Do not return an error because the container has already been removed from the Docker daemon. + // The orphaned db record will be ignored and eventually cleaned up by the garbage collector. + } + + return resp, nil +} diff --git a/pkg/client/container.go b/pkg/client/container.go index 83a27347..0c396c88 100644 --- a/pkg/client/container.go +++ b/pkg/client/container.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "strings" + "github.com/docker/compose/v2/pkg/progress" "github.com/docker/docker/api/types/container" dockerclient "github.com/docker/docker/client" @@ -11,7 +13,6 @@ import ( "github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/pkg/api" "google.golang.org/grpc/status" - "strings" ) // CreateContainer creates a new container for the given service on the specified machine. @@ -263,9 +264,9 @@ func (cli *Client) StopContainer( // RemoveContainer removes the specified container within the service. func (cli *Client) RemoveContainer( - ctx context.Context, serviceID, containerID string, opts container.RemoveOptions, + ctx context.Context, serviceID, containerNameOrID string, opts container.RemoveOptions, ) error { - ctr, err := cli.InspectContainer(ctx, serviceID, containerID) + ctr, err := cli.InspectContainer(ctx, serviceID, containerNameOrID) if err != nil { return err } @@ -280,7 +281,7 @@ func (cli *Client) RemoveContainer( eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name) pw.Event(progress.RemovingEvent(eventID)) - if err = cli.Docker.RemoveContainer(ctx, ctr.Container.ID, opts); err != nil { + if err = cli.Docker.RemoveServiceContainer(ctx, ctr.Container.ID, opts); err != nil { return err } pw.Event(progress.RemovedEvent(eventID))