Files
galaxy-game/rtmanager/internal/ports/dockerclient.go
T
2026-04-28 20:39:18 +02:00

337 lines
11 KiB
Go

package ports
import (
"context"
"errors"
"fmt"
"time"
)
// PullPolicy enumerates the supported image pull policies. The value
// set mirrors `config.ImagePullPolicy`; the runtime/wiring layer
// translates between the two so the docker adapter does not import
// `internal/config` and the port package stays free of configuration
// concerns.
type PullPolicy string
// Supported pull policies, frozen by `rtmanager/README.md §Configuration`.
const (
// PullPolicyIfMissing pulls the image only when it is absent from
// the local Docker daemon.
PullPolicyIfMissing PullPolicy = "if_missing"
// PullPolicyAlways pulls the image on every start.
PullPolicyAlways PullPolicy = "always"
// PullPolicyNever skips the pull and fails the start when the image
// is absent.
PullPolicyNever PullPolicy = "never"
)
// IsKnown reports whether policy belongs to the frozen pull-policy
// vocabulary.
func (policy PullPolicy) IsKnown() bool {
switch policy {
case PullPolicyIfMissing, PullPolicyAlways, PullPolicyNever:
return true
default:
return false
}
}
//go:generate go run go.uber.org/mock/mockgen -destination=../adapters/docker/mocks/mock_dockerclient.go -package=mocks galaxy/rtmanager/internal/ports DockerClient
// DockerClient is the narrow Docker port Runtime Manager uses. The
// production adapter wraps `github.com/docker/docker/client`; service
// tests use a generated mock. The surface intentionally exposes only
// the operations RTM needs; `docker logs` and stream attach are out
// of scope for v1.
type DockerClient interface {
// EnsureNetwork verifies the configured Docker network is present
// on the daemon. It returns ErrNetworkMissing when the network does
// not exist; RTM never creates networks itself.
EnsureNetwork(ctx context.Context, name string) error
// PullImage pulls ref according to policy. It returns nil on
// success and a wrapped Docker error otherwise. Implementations
// honour PullPolicyNever by skipping the pull and returning nil
// when the image is already present, or returning ErrImageNotFound
// otherwise.
PullImage(ctx context.Context, ref string, policy PullPolicy) error
// InspectImage returns image metadata for ref. It returns
// ErrImageNotFound when no such image exists locally.
InspectImage(ctx context.Context, ref string) (ImageInspect, error)
// InspectContainer returns container metadata for containerID. It
// returns ErrContainerNotFound when no such container exists.
InspectContainer(ctx context.Context, containerID string) (ContainerInspect, error)
// Run creates and starts one container according to spec. The
// returned RunResult carries the assigned container id, the stable
// engine endpoint, and the wall-clock observed by the daemon.
Run(ctx context.Context, spec RunSpec) (RunResult, error)
// Stop sends SIGTERM to the container followed by SIGKILL after
// timeout. It returns nil when the container exited cleanly and
// ErrContainerNotFound when it is already gone.
Stop(ctx context.Context, containerID string, timeout time.Duration) error
// Remove removes the container. It returns nil when the container
// no longer exists (idempotent removal).
Remove(ctx context.Context, containerID string) error
// List returns container summaries that match filter. Implementations
// translate ListFilter into the appropriate Docker filters argument.
List(ctx context.Context, filter ListFilter) ([]ContainerSummary, error)
// EventsListen subscribes to the Docker events stream and returns
// the decoded event channel together with an asynchronous error
// channel. The caller cancels ctx to terminate the subscription.
// Implementations close events when the subscription terminates.
EventsListen(ctx context.Context) (events <-chan DockerEvent, errs <-chan error, err error)
}
// RunSpec stores the request shape used by DockerClient.Run.
type RunSpec struct {
// Name stores the container name (typically `galaxy-game-{game_id}`).
Name string
// Image stores the image reference resolved by the producer.
Image string
// Hostname stores the container hostname assigned for the embedded
// Docker DNS to resolve from other containers on the network.
Hostname string
// Network stores the user-defined Docker network the container
// attaches to.
Network string
// Env stores the environment variables forwarded to the container
// (e.g. GAME_STATE_PATH, STORAGE_PATH).
Env map[string]string
// Cmd overrides the entrypoint arguments for the container. Production
// callers leave it nil so the engine image's own CMD runs; tests use
// it to drive a tiny container that does not embed RTM-specific
// behaviour. Empty Cmd means "use image default", which mirrors the
// Docker SDK contract.
Cmd []string
// Labels stores the labels applied to the container so the
// reconciler and the events listener can identify it.
Labels map[string]string
// BindMounts stores the host-to-container bind mounts. RTM uses
// exactly one mount in v1 (the per-game state directory).
BindMounts []BindMount
// LogDriver stores the Docker logging driver name.
LogDriver string
// LogOpts stores the logging-driver options as key=value pairs.
LogOpts map[string]string
// CPUQuota stores the `--cpus` value applied as a resource limit.
CPUQuota float64
// Memory stores the `--memory` value (e.g. `512m`) applied as a
// resource limit.
Memory string
// PIDsLimit stores the `--pids-limit` value.
PIDsLimit int
}
// BindMount stores one host-to-container bind mount.
type BindMount struct {
// HostPath stores the absolute host path bound into the container.
HostPath string
// MountPath stores the absolute in-container path the host
// directory is mounted at.
MountPath string
// ReadOnly mounts the host path read-only when true.
ReadOnly bool
}
// RunResult stores the response shape returned by DockerClient.Run.
type RunResult struct {
// ContainerID identifies the created container.
ContainerID string
// EngineEndpoint stores the stable URL Game Master uses to reach
// the engine container.
EngineEndpoint string
// StartedAt stores the wall-clock the daemon observed for the
// start event.
StartedAt time.Time
}
// ImageInspect stores the subset of `docker image inspect` fields RTM
// reads. Only Labels are required at start time (resource limits live
// there); other fields may be populated when convenient for diagnostics.
type ImageInspect struct {
// Ref stores the image reference the inspection was scoped to.
Ref string
// Labels stores the image-level labels (e.g.
// `com.galaxy.cpu_quota`).
Labels map[string]string
}
// ContainerInspect stores the subset of `docker inspect` fields RTM
// reads from a running or exited container.
type ContainerInspect struct {
// ID identifies the container.
ID string
// ImageRef stores the image reference the container was started
// from.
ImageRef string
// Hostname stores the container hostname.
Hostname string
// Labels stores the container labels assigned at create time.
Labels map[string]string
// Status stores the verbatim Docker `State.Status` value (e.g.
// `running`, `exited`).
Status string
// Health stores the verbatim Docker `State.Health.Status` value
// (e.g. `healthy`, `unhealthy`). Empty when the image declares no
// HEALTHCHECK.
Health string
// RestartCount stores the Docker `RestartCount` observed at
// inspection time.
RestartCount int
// StartedAt stores the daemon-observed start wall-clock.
StartedAt time.Time
// FinishedAt stores the daemon-observed exit wall-clock. Zero when
// the container is still running.
FinishedAt time.Time
// ExitCode stores the exit code reported by the daemon. Zero when
// the container is still running.
ExitCode int
// OOMKilled reports whether the container was killed by the OOM
// killer.
OOMKilled bool
}
// ContainerSummary stores the subset of `docker ps` fields RTM reads.
type ContainerSummary struct {
// ID identifies the container.
ID string
// ImageRef stores the image reference.
ImageRef string
// Hostname stores the container hostname.
Hostname string
// Labels stores the container labels assigned at create time.
Labels map[string]string
// Status stores the verbatim Docker `State.Status` value.
Status string
// StartedAt stores the daemon-observed start wall-clock.
StartedAt time.Time
}
// ListFilter stores the criteria used by DockerClient.List.
type ListFilter struct {
// Labels stores label key=value pairs that must all be present on
// the container. Empty matches every container.
Labels map[string]string
}
// DockerEvent stores one decoded entry from the Docker events stream.
// RTM only consumes container-scoped events.
type DockerEvent struct {
// Action stores the Docker event action verbatim (e.g. `start`,
// `die`, `oom`, `destroy`).
Action string
// ContainerID identifies the container the event refers to.
ContainerID string
// Labels stores the container labels carried by the event
// attributes when present.
Labels map[string]string
// ExitCode stores the exit code attribute when applicable (e.g.
// `die` events). Zero when the action does not carry one.
ExitCode int
// OccurredAt stores the daemon-observed event wall-clock.
OccurredAt time.Time
}
// String returns policy as its stored enum value. Convenient for use in
// log fields and error messages.
func (policy PullPolicy) String() string {
return string(policy)
}
// ErrNetworkMissing reports that the configured Docker network is not
// present on the daemon.
var ErrNetworkMissing = errors.New("docker network missing")
// ErrImageNotFound reports that an image reference does not resolve to
// a local Docker image.
var ErrImageNotFound = errors.New("docker image not found")
// ErrContainerNotFound reports that a container id does not resolve to
// a Docker container.
var ErrContainerNotFound = errors.New("docker container not found")
// Validate reports whether spec carries the structural invariants
// required by DockerClient.Run. Adapters use it as the first defence
// against malformed specs originating in service code.
func (spec RunSpec) Validate() error {
if spec.Name == "" {
return fmt.Errorf("run spec: name must not be empty")
}
if spec.Image == "" {
return fmt.Errorf("run spec: image must not be empty")
}
if spec.Hostname == "" {
return fmt.Errorf("run spec: hostname must not be empty")
}
if spec.Network == "" {
return fmt.Errorf("run spec: network must not be empty")
}
if spec.LogDriver == "" {
return fmt.Errorf("run spec: log driver must not be empty")
}
if spec.CPUQuota <= 0 {
return fmt.Errorf("run spec: cpu quota must be positive")
}
if spec.Memory == "" {
return fmt.Errorf("run spec: memory must not be empty")
}
if spec.PIDsLimit <= 0 {
return fmt.Errorf("run spec: pids limit must be positive")
}
for index, mount := range spec.BindMounts {
if mount.HostPath == "" {
return fmt.Errorf("run spec: bind mounts[%d]: host path must not be empty", index)
}
if mount.MountPath == "" {
return fmt.Errorf("run spec: bind mounts[%d]: mount path must not be empty", index)
}
}
return nil
}