chore: handle RemoveContainer server-side that deletes container from machine db

This commit is contained in:
Pavel Sviderski
2025-03-29 20:06:49 +10:00
parent 615022c9ac
commit 0975cbab66
6 changed files with 126 additions and 23 deletions
+25 -18
View File
@@ -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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x16, 0x52, 0x65,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x6d, 0x6f, 0x76, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (
@@ -1251,18 +1256,20 @@ var file_internal_machine_api_pb_docker_proto_depIdxs = []int32{
12, // 13: api.Docker.InspectImage:input_type -> api.InspectImageRequest 12, // 13: api.Docker.InspectImage:input_type -> api.InspectImageRequest
15, // 14: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest 15, // 14: api.Docker.InspectRemoteImage:input_type -> api.InspectRemoteImageRequest
18, // 15: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest 18, // 15: api.Docker.CreateServiceContainer:input_type -> api.CreateServiceContainerRequest
1, // 16: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse 9, // 16: api.Docker.RemoveServiceContainer:input_type -> api.RemoveContainerRequest
3, // 17: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse 1, // 17: api.Docker.CreateContainer:output_type -> api.CreateContainerResponse
20, // 18: api.Docker.StartContainer:output_type -> google.protobuf.Empty 3, // 18: api.Docker.InspectContainer:output_type -> api.InspectContainerResponse
20, // 19: api.Docker.StopContainer:output_type -> google.protobuf.Empty 20, // 19: api.Docker.StartContainer:output_type -> google.protobuf.Empty
7, // 20: api.Docker.ListContainers:output_type -> api.ListContainersResponse 20, // 20: api.Docker.StopContainer:output_type -> google.protobuf.Empty
20, // 21: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty 7, // 21: api.Docker.ListContainers:output_type -> api.ListContainersResponse
11, // 22: api.Docker.PullImage:output_type -> api.JSONMessage 20, // 22: api.Docker.RemoveContainer:output_type -> google.protobuf.Empty
13, // 23: api.Docker.InspectImage:output_type -> api.InspectImageResponse 11, // 23: api.Docker.PullImage:output_type -> api.JSONMessage
16, // 24: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse 13, // 24: api.Docker.InspectImage:output_type -> api.InspectImageResponse
1, // 25: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse 16, // 25: api.Docker.InspectRemoteImage:output_type -> api.InspectRemoteImageResponse
16, // [16:26] is the sub-list for method output_type 1, // 26: api.Docker.CreateServiceContainer:output_type -> api.CreateContainerResponse
6, // [6:16] is the sub-list for method input_type 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 type_name
6, // [6:6] is the sub-list for extension extendee 6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name 0, // [0:6] is the sub-list for field type_name
+1
View File
@@ -21,6 +21,7 @@ service Docker {
rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse); rpc InspectRemoteImage(InspectRemoteImageRequest) returns (InspectRemoteImageResponse);
rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse); rpc CreateServiceContainer(CreateServiceContainerRequest) returns (CreateContainerResponse);
rpc RemoveServiceContainer(RemoveContainerRequest) returns (google.protobuf.Empty);
} }
message CreateContainerRequest { message CreateContainerRequest {
+38
View File
@@ -30,6 +30,7 @@ const (
Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage" Docker_InspectImage_FullMethodName = "/api.Docker/InspectImage"
Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage" Docker_InspectRemoteImage_FullMethodName = "/api.Docker/InspectRemoteImage"
Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer" Docker_CreateServiceContainer_FullMethodName = "/api.Docker/CreateServiceContainer"
Docker_RemoveServiceContainer_FullMethodName = "/api.Docker/RemoveServiceContainer"
) )
// DockerClient is the client API for Docker service. // DockerClient is the client API for Docker service.
@@ -48,6 +49,7 @@ type DockerClient interface {
// Docker auth credentials if necessary. // Docker auth credentials if necessary.
InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error) InspectRemoteImage(ctx context.Context, in *InspectRemoteImageRequest, opts ...grpc.CallOption) (*InspectRemoteImageResponse, error)
CreateServiceContainer(ctx context.Context, in *CreateServiceContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, 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 { type dockerClient struct {
@@ -167,6 +169,16 @@ func (c *dockerClient) CreateServiceContainer(ctx context.Context, in *CreateSer
return out, nil 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. // DockerServer is the server API for Docker service.
// All implementations must embed UnimplementedDockerServer // All implementations must embed UnimplementedDockerServer
// for forward compatibility. // for forward compatibility.
@@ -183,6 +195,7 @@ type DockerServer interface {
// Docker auth credentials if necessary. // Docker auth credentials if necessary.
InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error) InspectRemoteImage(context.Context, *InspectRemoteImageRequest) (*InspectRemoteImageResponse, error)
CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error)
RemoveServiceContainer(context.Context, *RemoveContainerRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedDockerServer() mustEmbedUnimplementedDockerServer()
} }
@@ -223,6 +236,9 @@ func (UnimplementedDockerServer) InspectRemoteImage(context.Context, *InspectRem
func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) { func (UnimplementedDockerServer) CreateServiceContainer(context.Context, *CreateServiceContainerRequest) (*CreateContainerResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceContainer not implemented") 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) mustEmbedUnimplementedDockerServer() {}
func (UnimplementedDockerServer) testEmbeddedByValue() {} func (UnimplementedDockerServer) testEmbeddedByValue() {}
@@ -417,6 +433,24 @@ func _Docker_CreateServiceContainer_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler) 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. // Docker_ServiceDesc is the grpc.ServiceDesc for Docker service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -460,6 +494,10 @@ var Docker_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateServiceContainer", MethodName: "CreateServiceContainer",
Handler: _Docker_CreateServiceContainer_Handler, Handler: _Docker_CreateServiceContainer_Handler,
}, },
{
MethodName: "RemoveServiceContainer",
Handler: _Docker_RemoveServiceContainer_Handler,
},
}, },
Streams: []grpc.StreamDesc{ Streams: []grpc.StreamDesc{
{ {
+22 -1
View File
@@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -18,7 +20,6 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "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. // 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 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
}
+35
View File
@@ -6,6 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"regexp"
"strconv" "strconv"
"strings" "strings"
@@ -32,6 +34,8 @@ import (
"google.golang.org/protobuf/types/known/emptypb" "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. // Server implements the gRPC Docker service that proxies requests to the Docker daemon.
type Server struct { type Server struct {
pb.UnimplementedDockerServer pb.UnimplementedDockerServer
@@ -450,3 +454,34 @@ func (s *Server) CreateServiceContainer(
return &pb.CreateContainerResponse{Response: respBytes}, nil 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
}
+5 -4
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/docker/compose/v2/pkg/progress" "github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
@@ -11,7 +13,6 @@ import (
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"strings"
) )
// CreateContainer creates a new container for the given service on the specified machine. // 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. // RemoveContainer removes the specified container within the service.
func (cli *Client) RemoveContainer( func (cli *Client) RemoveContainer(
ctx context.Context, serviceID, containerID string, opts container.RemoveOptions, ctx context.Context, serviceID, containerNameOrID string, opts container.RemoveOptions,
) error { ) error {
ctr, err := cli.InspectContainer(ctx, serviceID, containerID) ctr, err := cli.InspectContainer(ctx, serviceID, containerNameOrID)
if err != nil { if err != nil {
return err return err
} }
@@ -280,7 +281,7 @@ func (cli *Client) RemoveContainer(
eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name) eventID := fmt.Sprintf("Container %s on %s", ctr.Container.NameWithoutSlash(), machine.Machine.Name)
pw.Event(progress.RemovingEvent(eventID)) 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 return err
} }
pw.Event(progress.RemovedEvent(eventID)) pw.Event(progress.RemovedEvent(eventID))