package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/env" "acc-server-manager/local/utl/logging" "context" "fmt" "strconv" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" dockerclient "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/google/uuid" ) type DockerRuntime struct { client *dockerclient.Client repo *repository.ServerRepository } func NewDockerRuntime(client *dockerclient.Client, repo *repository.ServerRepository) *DockerRuntime { return &DockerRuntime{ client: client, repo: repo, } } func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error { image := env.GetACCImage() gameVolume := env.GetACCGameVolume() configPath := server.GetConfigPath() portStr := strconv.Itoa(server.Port) natPort, err := nat.NewPort("tcp", portStr) if err != nil { return fmt.Errorf("invalid port %d: %v", server.Port, err) } natPortUDP, err := nat.NewPort("udp", portStr) if err != nil { return fmt.Errorf("invalid udp port %d: %v", server.Port, err) } portBindings := nat.PortMap{ natPort: []nat.PortBinding{{HostPort: portStr}}, natPortUDP: []nat.PortBinding{{HostPort: portStr}}, } exposedPorts := nat.PortSet{ natPort: struct{}{}, natPortUDP: struct{}{}, } binds := []string{ fmt.Sprintf("%s:/acc/game:ro", gameVolume), fmt.Sprintf("%s:/acc/cfg:rw", configPath), } resp, err := r.client.ContainerCreate(ctx, &container.Config{ Image: image, ExposedPorts: exposedPorts, Env: []string{ fmt.Sprintf("SERVER_PORT=%d", server.Port), }, }, &container.HostConfig{ Binds: binds, PortBindings: portBindings, RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, }, nil, nil, server.ServiceName) if err != nil { return fmt.Errorf("failed to create container %s: %v", server.ServiceName, err) } server.ContainerID = resp.ID logging.Info("Created Docker container %s (ID: %s) for server %s", server.ServiceName, resp.ID[:12], server.ID) return nil } func (r *DockerRuntime) Delete(ctx context.Context, serverID uuid.UUID) error { containerID, err := r.resolveContainerID(ctx, serverID) if err != nil { return err } if containerID == "" { return nil } err = r.client.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{ Force: true, RemoveVolumes: false, }) if err != nil { return fmt.Errorf("failed to remove container %s: %v", containerID[:12], err) } logging.Info("Removed Docker container %s for server %s", containerID[:12], serverID) return nil } func (r *DockerRuntime) Start(ctx context.Context, serverID uuid.UUID) (string, error) { containerID, err := r.resolveContainerID(ctx, serverID) if err != nil { return "", err } if err := r.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { return "", fmt.Errorf("failed to start container: %v", err) } return model.StatusRunning.String(), nil } func (r *DockerRuntime) Stop(ctx context.Context, serverID uuid.UUID) (string, error) { containerID, err := r.resolveContainerID(ctx, serverID) if err != nil { return "", err } if err := r.client.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil { return "", fmt.Errorf("failed to stop container: %v", err) } return model.StatusStopped.String(), nil } func (r *DockerRuntime) Restart(ctx context.Context, serverID uuid.UUID) (string, error) { containerID, err := r.resolveContainerID(ctx, serverID) if err != nil { return "", err } if err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{}); err != nil { return "", fmt.Errorf("failed to restart container: %v", err) } return model.StatusRunning.String(), nil } func (r *DockerRuntime) Status(ctx context.Context, serverID uuid.UUID) (string, error) { server, err := r.repo.GetByID(ctx, serverID) if err != nil || server == nil { return model.StatusUnknown.String(), err } containerID := server.ContainerID if containerID == "" { containerID, err = r.findContainerByName(ctx, server.ServiceName) if err != nil || containerID == "" { return model.StatusStopped.String(), nil } } info, err := r.client.ContainerInspect(ctx, containerID) if err != nil { return model.StatusUnknown.String(), nil } return dockerStateToStatus(info.State.Status), nil } func (r *DockerRuntime) resolveContainerID(ctx context.Context, serverID uuid.UUID) (string, error) { server, err := r.repo.GetByID(ctx, serverID) if err != nil { return "", fmt.Errorf("failed to look up server %s: %v", serverID, err) } if server == nil { return "", fmt.Errorf("server %s not found", serverID) } return server.ContainerID, nil } func (r *DockerRuntime) findContainerByName(ctx context.Context, name string) (string, error) { containers, err := r.client.ContainerList(ctx, types.ContainerListOptions{ All: true, Filters: filters.NewArgs(filters.Arg("name", "/"+name)), }) if err != nil { return "", err } if len(containers) == 0 { return "", nil } return containers[0].ID, nil } func dockerStateToStatus(state string) string { switch state { case "running": return model.StatusRunning.String() case "exited", "dead": return model.StatusStopped.String() case "restarting": return model.StatusRestarting.String() case "paused": return model.StatusStopped.String() default: return model.StatusUnknown.String() } }