mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
153 lines
5.1 KiB
Go
153 lines
5.1 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/client"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/psviderski/uncloud/pkg/api"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// Service provides higher-level Docker operations that extends Docker API with Uncloud-specific data
|
|
// from the machine database.
|
|
type Service struct {
|
|
// Client is a Docker client for managing Docker resources.
|
|
Client *client.Client
|
|
// db is a connection to the machine database.
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewService creates a new Docker service instance.
|
|
func NewService(client *client.Client, db *sqlx.DB) *Service {
|
|
return &Service{
|
|
Client: client,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// InspectServiceContainer inspects a Docker container and retrieves its associated ServiceSpec
|
|
// from the machine database, returning a complete ServiceContainer.
|
|
func (s *Service) InspectServiceContainer(ctx context.Context, nameOrID string) (api.ServiceContainer, error) {
|
|
var serviceCtr api.ServiceContainer
|
|
|
|
ctr, err := s.Client.ContainerInspect(ctx, nameOrID)
|
|
if err != nil {
|
|
return serviceCtr, err
|
|
}
|
|
if _, ok := ctr.Config.Labels[api.LabelManaged]; !ok {
|
|
return serviceCtr, fmt.Errorf("container '%s' is not managed by Uncloud", nameOrID)
|
|
}
|
|
|
|
serviceCtr.Container = api.Container{InspectResponse: ctr}
|
|
|
|
// Retrieve ServiceSpec from the machine database.
|
|
var specBytes []byte
|
|
err = s.db.QueryRowContext(ctx, `SELECT service_spec FROM containers WHERE id = $1`, ctr.ID).Scan(&specBytes)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
// If this happens, there is a bug in the code, or someone manually removed the container from the DB,
|
|
// or created a managed container out of band or by previous uncloud installation.
|
|
return serviceCtr, fmt.Errorf("service spec not found for container '%s' in machine DB", ctr.ID)
|
|
}
|
|
return serviceCtr, fmt.Errorf("get service spec for container '%s' from machine DB: %w", ctr.ID, err)
|
|
}
|
|
|
|
if err = json.Unmarshal(specBytes, &serviceCtr.ServiceSpec); err != nil {
|
|
return serviceCtr, fmt.Errorf("unmarshal service spec for container '%s': %w", ctr.ID, err)
|
|
}
|
|
|
|
return serviceCtr, nil
|
|
}
|
|
|
|
// ListServiceContainers lists Docker containers that belong to the service with the given name or ID.
|
|
// If serviceIDOrName is empty, all service containers are returned. The opts parameter allows additional filtering.
|
|
func (s *Service) ListServiceContainers(
|
|
ctx context.Context, serviceNameOrID string, opts container.ListOptions,
|
|
) ([]api.ServiceContainer, error) {
|
|
if opts.Filters.Len() == 0 {
|
|
opts.Filters = filters.NewArgs()
|
|
}
|
|
// Add labels to existing filters to list only Uncloud-managed service containers.
|
|
opts.Filters.Add("label", api.LabelServiceID)
|
|
opts.Filters.Add("label", api.LabelManaged)
|
|
|
|
containerSummaries, err := s.Client.ContainerList(ctx, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var containers []api.ServiceContainer
|
|
for _, cs := range containerSummaries {
|
|
// Filter by service name or ID if provided.
|
|
if serviceNameOrID != "" &&
|
|
cs.Labels[api.LabelServiceID] != serviceNameOrID &&
|
|
cs.Labels[api.LabelServiceName] != serviceNameOrID {
|
|
continue
|
|
}
|
|
|
|
ctr, err := s.InspectServiceContainer(ctx, cs.ID)
|
|
if err != nil {
|
|
// Log error but continue with other containers.
|
|
slog.Error("Failed to inspect service container.", "service", serviceNameOrID, "id", cs.ID, "err", err)
|
|
continue
|
|
}
|
|
containers = append(containers, ctr)
|
|
}
|
|
|
|
return containers, nil
|
|
}
|
|
|
|
// IsContainerdImageStoreEnabled checks if Docker is configured to use the containerd image store:
|
|
// https://docs.docker.com/engine/storage/containerd/
|
|
func (s *Service) IsContainerdImageStoreEnabled(ctx context.Context) (bool, error) {
|
|
info, err := s.Client.Info(ctx)
|
|
if err != nil {
|
|
return false, fmt.Errorf("get Docker info: %w", err)
|
|
}
|
|
|
|
return strings.Contains(fmt.Sprintf("%s", info.DriverStatus), "containerd.snapshotter"), nil
|
|
}
|
|
|
|
type Images struct {
|
|
// Images is a list of images present in the Docker image store (either internal or containerd).
|
|
Images []image.Summary
|
|
// ContainerdStore indicates whether Docker is using the containerd image store.
|
|
ContainerdStore bool
|
|
}
|
|
|
|
// ListImages lists Docker images with the given options and indicates whether Docker is using the containerd
|
|
// image store. It always includes image manifests in the response if the store is containerd.
|
|
func (s *Service) ListImages(ctx context.Context, opts image.ListOptions) (Images, error) {
|
|
var imagesResp Images
|
|
|
|
// Always include the image manifests in the response.
|
|
opts.Manifests = true
|
|
images, err := s.Client.ImageList(ctx, opts)
|
|
if err != nil {
|
|
return imagesResp, status.Errorf(codes.Internal, "list images: %v", err)
|
|
}
|
|
|
|
isContainerdStore, err := s.IsContainerdImageStoreEnabled(ctx)
|
|
if err != nil {
|
|
return imagesResp, status.Errorf(codes.Internal, "check if Docker uses containerd image store: %v", err)
|
|
}
|
|
|
|
imagesResp = Images{
|
|
Images: images,
|
|
ContainerdStore: isContainerdStore,
|
|
}
|
|
|
|
return imagesResp, nil
|
|
}
|