Phase2
This commit is contained in:
191
apps/node-agent/internal/docker/client.go
Normal file
191
apps/node-agent/internal/docker/client.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
const MinecraftImage = "itzg/minecraft-server:java21"
|
||||
|
||||
// MinecraftServerSpec describes a Minecraft container to create.
|
||||
type MinecraftServerSpec struct {
|
||||
ServerID string
|
||||
MinecraftVersion string
|
||||
SoftwareFamily string
|
||||
HostPort int
|
||||
RamMb int
|
||||
DataPath string
|
||||
EulaAccepted bool
|
||||
}
|
||||
|
||||
// ContainerState summarizes a Docker container's runtime status.
|
||||
type ContainerState struct {
|
||||
ID string
|
||||
Status string
|
||||
Running bool
|
||||
}
|
||||
|
||||
// Client wraps the Docker Engine API for game server containers.
|
||||
type Client struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// New creates a Docker client connected via the given socket URL.
|
||||
func New(socket string) (*Client, error) {
|
||||
cli, err := client.NewClientWithOpts(
|
||||
client.FromEnv,
|
||||
client.WithHost(socket),
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create docker client: %w", err)
|
||||
}
|
||||
return &Client{cli: cli}, nil
|
||||
}
|
||||
|
||||
// Close releases the underlying Docker client.
|
||||
func (c *Client) Close() error {
|
||||
return c.cli.Close()
|
||||
}
|
||||
|
||||
// Ping verifies connectivity to the Docker daemon.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
_, err := c.cli.Ping(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateMinecraftServer creates a non-privileged Minecraft container and returns its ID.
|
||||
func (c *Client) CreateMinecraftServer(ctx context.Context, spec MinecraftServerSpec) (string, error) {
|
||||
if !spec.EulaAccepted {
|
||||
return "", fmt.Errorf("eula must be accepted before provisioning")
|
||||
}
|
||||
if spec.HostPort <= 0 {
|
||||
return "", fmt.Errorf("hostPort must be positive")
|
||||
}
|
||||
if strings.TrimSpace(spec.DataPath) == "" {
|
||||
return "", fmt.Errorf("dataPath is required")
|
||||
}
|
||||
if spec.RamMb <= 0 {
|
||||
return "", fmt.Errorf("ramMb must be positive")
|
||||
}
|
||||
|
||||
containerPort := nat.Port("25565/tcp")
|
||||
portBindings := nat.PortMap{
|
||||
containerPort: []nat.PortBinding{{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: strconv.Itoa(spec.HostPort),
|
||||
}},
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(spec.MinecraftVersion)
|
||||
if version == "" {
|
||||
version = "LATEST"
|
||||
}
|
||||
|
||||
resp, err := c.cli.ContainerCreate(ctx,
|
||||
&container.Config{
|
||||
Image: MinecraftImage,
|
||||
Env: []string{
|
||||
"EULA=TRUE",
|
||||
"VERSION=" + version,
|
||||
"TYPE=" + mapSoftwareFamily(spec.SoftwareFamily),
|
||||
fmt.Sprintf("MEMORY=%dM", spec.RamMb),
|
||||
},
|
||||
ExposedPorts: nat.PortSet{containerPort: struct{}{}},
|
||||
},
|
||||
&container.HostConfig{
|
||||
PortBindings: portBindings,
|
||||
Mounts: []mount.Mount{{
|
||||
Type: mount.TypeBind,
|
||||
Source: spec.DataPath,
|
||||
Target: "/data",
|
||||
}},
|
||||
Resources: container.Resources{
|
||||
Memory: int64(spec.RamMb) * 1024 * 1024,
|
||||
},
|
||||
Privileged: false,
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
containerName(spec.ServerID),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create container: %w", err)
|
||||
}
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// StartContainer starts a container by ID.
|
||||
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||||
if err := c.cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
|
||||
return fmt.Errorf("start container: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopContainer stops a running container gracefully.
|
||||
func (c *Client) StopContainer(ctx context.Context, containerID string) error {
|
||||
timeout := 30
|
||||
if err := c.cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}); err != nil {
|
||||
return fmt.Errorf("stop container: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveContainer removes a container, forcing removal if still running.
|
||||
func (c *Client) RemoveContainer(ctx context.Context, containerID string) error {
|
||||
if err := c.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}); err != nil {
|
||||
return fmt.Errorf("remove container: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetContainerState inspects a container and returns its current state.
|
||||
func (c *Client) GetContainerState(ctx context.Context, containerID string) (*ContainerState, error) {
|
||||
inspect, err := c.cli.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect container: %w", err)
|
||||
}
|
||||
return &ContainerState{
|
||||
ID: inspect.ID,
|
||||
Status: inspect.State.Status,
|
||||
Running: inspect.State.Running,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListManagedContainers returns container IDs labeled for a given server ID.
|
||||
func (c *Client) ListManagedContainers(ctx context.Context, serverID string) ([]string, error) {
|
||||
containers, err := c.cli.ContainerList(ctx, container.ListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.Arg("name", containerName(serverID))),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list containers: %w", err)
|
||||
}
|
||||
ids := make([]string, 0, len(containers))
|
||||
for _, ctr := range containers {
|
||||
ids = append(ids, ctr.ID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func containerName(serverID string) string {
|
||||
return "hexahost-" + serverID
|
||||
}
|
||||
|
||||
func mapSoftwareFamily(family string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(family)) {
|
||||
case "PAPER":
|
||||
return "PAPER"
|
||||
default:
|
||||
return "VANILLA"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user