mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-27 03:23:34 +00:00
add service inspect command, introduce squirrel for sql query building
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"uncloud/internal/machine/docker"
|
||||
)
|
||||
|
||||
var NotFound = errors.New("not found")
|
||||
|
||||
// Client is a client for the machine API.
|
||||
type Client struct {
|
||||
connector Connector
|
||||
@@ -46,6 +48,6 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return errors.Join(c.conn.Close(), c.connector.Close())
|
||||
func (cli *Client) Close() error {
|
||||
return errors.Join(cli.conn.Close(), cli.connector.Close())
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"slices"
|
||||
"strings"
|
||||
"uncloud/internal/docker"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
machinedocker "uncloud/internal/machine/docker"
|
||||
"uncloud/internal/secret"
|
||||
@@ -34,7 +35,7 @@ type RunServiceResponse struct {
|
||||
MachineName string
|
||||
}
|
||||
|
||||
func (c *Client) RunService(ctx context.Context, opts *ServiceOptions) (RunServiceResponse, error) {
|
||||
func (cli *Client) RunService(ctx context.Context, opts *ServiceOptions) (RunServiceResponse, error) {
|
||||
var resp RunServiceResponse
|
||||
|
||||
image, err := reference.ParseDockerRef(opts.Image)
|
||||
@@ -51,7 +52,7 @@ func (c *Client) RunService(ctx context.Context, opts *ServiceOptions) (RunServi
|
||||
}
|
||||
|
||||
// Find a machine to run the service on.
|
||||
listResp, err := c.ListMachines(ctx, &emptypb.Empty{})
|
||||
listResp, err := cli.ListMachines(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("list machines: %w", err)
|
||||
}
|
||||
@@ -114,8 +115,8 @@ func (c *Client) RunService(ctx context.Context, opts *ServiceOptions) (RunServi
|
||||
config := &container.Config{
|
||||
Image: opts.Image,
|
||||
Labels: map[string]string{
|
||||
docker.LabelServiceID: serviceID,
|
||||
docker.LabelServiceName: serviceName,
|
||||
service.LabelServiceID: serviceID,
|
||||
service.LabelServiceName: serviceName,
|
||||
},
|
||||
}
|
||||
netConfig := &network.NetworkingConfig{
|
||||
@@ -124,11 +125,11 @@ func (c *Client) RunService(ctx context.Context, opts *ServiceOptions) (RunServi
|
||||
},
|
||||
}
|
||||
// TODO: pull image if it doesn't exist on the machine.
|
||||
createResp, err := c.CreateContainer(ctx, config, nil, netConfig, nil, containerName)
|
||||
createResp, err := cli.CreateContainer(ctx, config, nil, netConfig, nil, containerName)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("create container: %w", err)
|
||||
}
|
||||
if err = c.StartContainer(ctx, createResp.ID, container.StartOptions{}); err != nil {
|
||||
if err = cli.StartContainer(ctx, createResp.ID, container.StartOptions{}); err != nil {
|
||||
return resp, fmt.Errorf("start container: %w", err)
|
||||
}
|
||||
|
||||
@@ -157,11 +158,23 @@ func firstAvailableMachine(machines []*pb.MachineMember) (*pb.MachineMember, err
|
||||
return nil, errors.New("no available machine to run the service")
|
||||
}
|
||||
|
||||
func (c *Client) InspectService(ctx context.Context, id string) error {
|
||||
_, err := c.MachineClient.InspectService(ctx, &pb.InspectServiceRequest{Id: id})
|
||||
// InspectService returns detailed information about a service and its containers.
|
||||
func (cli *Client) InspectService(ctx context.Context, id string) (service.Service, error) {
|
||||
var svc service.Service
|
||||
|
||||
resp, err := cli.MachineClient.InspectService(ctx, &pb.InspectServiceRequest{Id: id})
|
||||
if err != nil {
|
||||
return err
|
||||
if s, ok := status.FromError(err); ok {
|
||||
if s.Code() == codes.NotFound {
|
||||
return svc, NotFound
|
||||
}
|
||||
}
|
||||
return svc, err
|
||||
}
|
||||
|
||||
return errors.New("not implemented")
|
||||
svc, err = service.FromProto(resp.Service)
|
||||
if err != nil {
|
||||
return svc, fmt.Errorf("from proto: %w", err)
|
||||
}
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"log/slog"
|
||||
"time"
|
||||
"uncloud/internal/docker"
|
||||
"uncloud/internal/machine/store"
|
||||
"uncloud/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -152,8 +152,8 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||
// List only Uncloud service containers identified by their labels.
|
||||
containers, err := m.client.ContainerList(ctx, dockercontainer.ListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", docker.LabelServiceID),
|
||||
filters.Arg("label", docker.LabelServiceName),
|
||||
filters.Arg("label", service.LabelServiceID),
|
||||
filters.Arg("label", service.LabelServiceName),
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -185,7 +185,7 @@ func (m *Manager) syncContainersToStore(ctx context.Context) error {
|
||||
|
||||
// Create or update the current Docker containers in the store.
|
||||
for _, dc := range containers {
|
||||
c := &docker.Container{Container: dc}
|
||||
c := &service.Container{Container: dc}
|
||||
if err = m.store.CreateOrUpdateContainer(ctx, c, m.machineID); err != nil {
|
||||
storeErr = errors.Join(storeErr, fmt.Errorf("create or update container %q: %w", c.ID, err))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package machine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -686,6 +687,7 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InspectService returns detailed information about a service and its containers.
|
||||
func (m *Machine) InspectService(
|
||||
ctx context.Context, req *pb.InspectServiceRequest,
|
||||
) (*pb.InspectServiceResponse, error) {
|
||||
@@ -698,11 +700,26 @@ func (m *Machine) InspectService(
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "list containers: %v", err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, status.Error(codes.NotFound, "service not found")
|
||||
}
|
||||
|
||||
fmt.Println("## records: ", records)
|
||||
return &pb.InspectServiceResponse{
|
||||
Service: &pb.Service{
|
||||
Id: req.Id,
|
||||
},
|
||||
}, nil
|
||||
containers := make([]*pb.Service_Container, len(records))
|
||||
for i, r := range records {
|
||||
containerJSON, err := json.Marshal(r.Container)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
|
||||
}
|
||||
containers[i] = &pb.Service_Container{
|
||||
MachineId: r.MachineID,
|
||||
Container: containerJSON,
|
||||
}
|
||||
}
|
||||
|
||||
svc := &pb.Service{
|
||||
Id: records[0].Container.ServiceID(),
|
||||
Name: records[0].Container.ServiceName(),
|
||||
Containers: containers,
|
||||
}
|
||||
return &pb.InspectServiceResponse{Service: svc}, nil
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
"uncloud/internal/docker"
|
||||
"uncloud/internal/service"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
type ContainerRecord struct {
|
||||
Container *docker.Container
|
||||
Container *service.Container
|
||||
MachineID string
|
||||
SyncStatus string
|
||||
UpdatedAt time.Time
|
||||
@@ -46,7 +47,7 @@ type DeleteOptions struct {
|
||||
|
||||
// CreateOrUpdateContainer creates a new container record or updates an existing one in the store database.
|
||||
// The container is associated with the given machine ID that indicates which machine the container is running on.
|
||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *docker.Container, machineID string) error {
|
||||
func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *service.Container, machineID string) error {
|
||||
cJSON, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal container: %w", err)
|
||||
@@ -75,34 +76,27 @@ func (s *Store) CreateOrUpdateContainer(ctx context.Context, c *docker.Container
|
||||
|
||||
// ListContainers returns a list of container records from the store database that match the given options.
|
||||
func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*ContainerRecord, error) {
|
||||
query := "SELECT container, machine_id, sync_status, updated_at FROM containers"
|
||||
var args []any
|
||||
q := sq.Select("container", "machine_id", "sync_status", "updated_at").From("containers")
|
||||
|
||||
var whereConditions []string
|
||||
if len(opts.MachineIDs) > 0 {
|
||||
whereConditions = append(whereConditions, "machine_id IN (?"+strings.Repeat(", ?", len(opts.MachineIDs)-1)+")")
|
||||
args = make([]any, len(opts.MachineIDs))
|
||||
for i, id := range opts.MachineIDs {
|
||||
args[i] = id
|
||||
q = q.Where(sq.Eq{"machine_id": opts.MachineIDs})
|
||||
}
|
||||
|
||||
if opts.ServiceIDOrName.ID != "" || opts.ServiceIDOrName.Name != "" {
|
||||
var conditions []sq.Sqlizer
|
||||
if opts.ServiceIDOrName.ID != "" {
|
||||
conditions = append(conditions, sq.Eq{"service_id": opts.ServiceIDOrName.ID})
|
||||
}
|
||||
if opts.ServiceIDOrName.Name != "" {
|
||||
conditions = append(conditions, sq.Eq{"service_name": opts.ServiceIDOrName.Name})
|
||||
}
|
||||
q = q.Where(sq.Or(conditions))
|
||||
}
|
||||
|
||||
var serviceConditions []string
|
||||
var serviceArgs []any
|
||||
if opts.ServiceIDOrName.ID != "" {
|
||||
serviceConditions = append(serviceConditions, "service_id = ?")
|
||||
serviceArgs = append(serviceArgs, opts.ServiceIDOrName.ID)
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build query: %w", err)
|
||||
}
|
||||
if opts.ServiceIDOrName.Name != "" {
|
||||
serviceConditions = append(serviceConditions, "service_name = ?")
|
||||
serviceArgs = append(serviceArgs, opts.ServiceIDOrName.Name)
|
||||
}
|
||||
if len(serviceConditions) > 0 {
|
||||
whereConditions = append(whereConditions, "("+strings.Join(serviceConditions, " OR ")+")")
|
||||
args = append(args, serviceArgs...)
|
||||
}
|
||||
|
||||
query += " WHERE " + strings.Join(whereConditions, " AND ")
|
||||
|
||||
rows, err := s.corro.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
@@ -119,7 +113,7 @@ func (s *Store) ListContainers(ctx context.Context, opts ListOptions) ([]*Contai
|
||||
return nil, fmt.Errorf("scan container record: %w", err)
|
||||
}
|
||||
|
||||
var c docker.Container
|
||||
var c service.Container
|
||||
if err = json.Unmarshal([]byte(cJSON), &c); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal container: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
@@ -14,6 +14,16 @@ type Container struct {
|
||||
types.Container
|
||||
}
|
||||
|
||||
// ServiceID returns the service ID that the container is part of.
|
||||
func (c *Container) ServiceID() string {
|
||||
return c.Labels[LabelServiceID]
|
||||
}
|
||||
|
||||
// ServiceName returns the service name that the container is part of.
|
||||
func (c *Container) ServiceName() string {
|
||||
return c.Labels[LabelServiceName]
|
||||
}
|
||||
|
||||
// runningStatusRegex matches the status string of a running container.
|
||||
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
|
||||
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
|
||||
@@ -1,4 +1,4 @@
|
||||
package docker
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
+13
-14
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"uncloud/internal/machine/api/pb"
|
||||
)
|
||||
|
||||
@@ -15,19 +14,19 @@ const (
|
||||
type Service struct {
|
||||
ID string
|
||||
Name string
|
||||
Containers []Container
|
||||
Containers []MachineContainer
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
type MachineContainer struct {
|
||||
MachineID string
|
||||
Container types.Container
|
||||
Container Container
|
||||
}
|
||||
|
||||
func FromProto(s *pb.Service) (Service, error) {
|
||||
var err error
|
||||
containers := make([]Container, len(s.Containers))
|
||||
for i, c := range s.Containers {
|
||||
containers[i], err = containerFromProto(c)
|
||||
containers := make([]MachineContainer, len(s.Containers))
|
||||
for i, sc := range s.Containers {
|
||||
containers[i], err = machineContainerFromProto(sc)
|
||||
if err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
@@ -40,14 +39,14 @@ func FromProto(s *pb.Service) (Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func containerFromProto(c *pb.Service_Container) (Container, error) {
|
||||
var dockerCtr types.Container
|
||||
if err := json.Unmarshal(c.Container, &dockerCtr); err != nil {
|
||||
return Container{}, fmt.Errorf("unmarshal container: %w", err)
|
||||
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
|
||||
var c Container
|
||||
if err := json.Unmarshal(sc.Container, &c); err != nil {
|
||||
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
|
||||
}
|
||||
|
||||
return Container{
|
||||
MachineID: c.MachineId,
|
||||
Container: dockerCtr,
|
||||
return MachineContainer{
|
||||
MachineID: sc.MachineId,
|
||||
Container: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user