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 }