add docker specifics

This commit is contained in:
2026-03-18 19:17:10 +01:00
parent beb9b479df
commit 9172f0256f
127 changed files with 564 additions and 10 deletions

0
local/api/api.go Normal file → Executable file
View File

0
local/controller/config.go Normal file → Executable file
View File

0
local/controller/controller.go Normal file → Executable file
View File

0
local/controller/leaderboard.go Normal file → Executable file
View File

0
local/controller/lookup.go Normal file → Executable file
View File

0
local/controller/membership.go Normal file → Executable file
View File

0
local/controller/server.go Normal file → Executable file
View File

0
local/controller/service_control.go Normal file → Executable file
View File

0
local/controller/state_history.go Normal file → Executable file
View File

0
local/controller/system.go Normal file → Executable file
View File

0
local/controller/websocket.go Normal file → Executable file
View File

0
local/middleware/access_key.go Normal file → Executable file
View File

0
local/middleware/auth.go Normal file → Executable file
View File

0
local/middleware/logging/request_logging.go Normal file → Executable file
View File

0
local/middleware/security/security.go Normal file → Executable file
View File

0
local/model/cache.go Normal file → Executable file
View File

0
local/model/config.go Normal file → Executable file
View File

0
local/model/filter.go Normal file → Executable file
View File

0
local/model/leaderboard.go Normal file → Executable file
View File

0
local/model/lookup.go Normal file → Executable file
View File

0
local/model/model.go Normal file → Executable file
View File

0
local/model/permission.go Normal file → Executable file
View File

0
local/model/permissions.go Normal file → Executable file
View File

0
local/model/role.go Normal file → Executable file
View File

0
local/model/service_control.go Normal file → Executable file
View File

0
local/model/state_history.go Normal file → Executable file
View File

0
local/model/state_history_stats.go Normal file → Executable file
View File

0
local/model/steam_2fa.go Normal file → Executable file
View File

0
local/model/steam_credentials.go Normal file → Executable file
View File

0
local/model/user.go Normal file → Executable file
View File

0
local/model/websocket.go Normal file → Executable file
View File

0
local/repository/base.go Normal file → Executable file
View File

0
local/repository/config.go Normal file → Executable file
View File

0
local/repository/leaderboard.go Normal file → Executable file
View File

0
local/repository/lookup.go Normal file → Executable file
View File

0
local/repository/membership.go Normal file → Executable file
View File

0
local/repository/repository.go Normal file → Executable file
View File

0
local/repository/server.go Normal file → Executable file
View File

0
local/repository/service_control.go Normal file → Executable file
View File

0
local/repository/state_history.go Normal file → Executable file
View File

0
local/repository/steam_credentials.go Normal file → Executable file
View File

0
local/service/config.go Normal file → Executable file
View File

View File

@@ -0,0 +1,74 @@
package service
import (
"acc-server-manager/local/model"
"acc-server-manager/local/utl/logging"
"bufio"
"context"
"io"
"sync"
"github.com/docker/docker/api/types"
dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/google/uuid"
)
type DockerLogStreamer struct {
client *dockerclient.Client
cancels sync.Map // uuid.UUID → context.CancelFunc
}
func NewDockerLogStreamer(client *dockerclient.Client) *DockerLogStreamer {
return &DockerLogStreamer{client: client}
}
func (s *DockerLogStreamer) Start(ctx context.Context, server *model.Server, handleLine func(string)) error {
if _, exists := s.cancels.Load(server.ID); exists {
return nil
}
if server.ContainerID == "" {
logging.Warn("DockerLogStreamer.Start: no container ID for server %s, skipping", server.ID)
return nil
}
streamCtx, cancel := context.WithCancel(ctx)
s.cancels.Store(server.ID, cancel)
go func() {
defer s.cancels.Delete(server.ID)
rc, err := s.client.ContainerLogs(streamCtx, server.ContainerID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Timestamps: false,
})
if err != nil {
logging.Error("DockerLogStreamer: failed to attach to container %s: %v", server.ContainerID[:12], err)
return
}
defer rc.Close()
pr, pw := io.Pipe()
go func() {
defer pw.Close()
if _, err := stdcopy.StdCopy(pw, pw, rc); err != nil && err != io.EOF {
logging.Warn("DockerLogStreamer: stdcopy error for %s: %v", server.ServiceName, err)
}
}()
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
handleLine(scanner.Text())
}
}()
return nil
}
func (s *DockerLogStreamer) Stop(serverID uuid.UUID) {
if v, ok := s.cancels.LoadAndDelete(serverID); ok {
v.(context.CancelFunc)()
}
}

View File

@@ -0,0 +1,194 @@
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()
}
}

0
local/service/firewall_service.go Normal file → Executable file
View File

0
local/service/leaderboard.go Normal file → Executable file
View File

0
local/service/lookup.go Normal file → Executable file
View File

0
local/service/membership.go Normal file → Executable file
View File

View File

@@ -0,0 +1,7 @@
package service
type NoOpFirewall struct{}
func (f *NoOpFirewall) CreateServerRules(_ string, _, _ []int) error { return nil }
func (f *NoOpFirewall) DeleteServerRules(_ string, _, _ []int) error { return nil }
func (f *NoOpFirewall) UpdateServerRules(_ string, _, _ []int) error { return nil }

0
local/service/service_manager.go Normal file → Executable file
View File

0
local/service/state_history.go Normal file → Executable file
View File

0
local/service/websocket.go Normal file → Executable file
View File

0
local/service/windows_service.go Normal file → Executable file
View File

0
local/utl/audit/audit.go Normal file → Executable file
View File

0
local/utl/cache/cache.go vendored Normal file → Executable file
View File

0
local/utl/command/callback_executor.go Normal file → Executable file
View File

0
local/utl/command/callbacks.go Normal file → Executable file
View File

0
local/utl/command/executor.go Normal file → Executable file
View File

0
local/utl/command/interactive_executor.go Normal file → Executable file
View File

0
local/utl/common/common.go Normal file → Executable file
View File

0
local/utl/configs/configs.go Normal file → Executable file
View File

View File

@@ -0,0 +1,12 @@
package docker
import (
dockerclient "github.com/docker/docker/client"
)
func NewDockerClient() (*dockerclient.Client, error) {
return dockerclient.NewClientWithOpts(
dockerclient.FromEnv,
dockerclient.WithAPIVersionNegotiation(),
)
}

0
local/utl/error_handler/controller_error_handler.go Normal file → Executable file
View File

0
local/utl/errors/safe_error.go Normal file → Executable file
View File

0
local/utl/graceful/shutdown.go Normal file → Executable file
View File

0
local/utl/jwt/jwt.go Normal file → Executable file
View File

0
local/utl/logging/base.go Normal file → Executable file
View File

0
local/utl/logging/debug.go Normal file → Executable file
View File

0
local/utl/logging/error.go Normal file → Executable file
View File

0
local/utl/logging/info.go Normal file → Executable file
View File

0
local/utl/logging/logger.go Normal file → Executable file
View File

0
local/utl/logging/warn.go Normal file → Executable file
View File

0
local/utl/network/port.go Normal file → Executable file
View File

0
local/utl/password/password.go Normal file → Executable file
View File

0
local/utl/regex_handler/regex_handler.go Normal file → Executable file
View File

0
local/utl/security/download_verifier.go Normal file → Executable file
View File

0
local/utl/security/path_validator.go Normal file → Executable file
View File

0
local/utl/tracking/log_tailer.go Normal file → Executable file
View File

0
local/utl/tracking/position_tracker.go Normal file → Executable file
View File

0
local/utl/tracking/tracking.go Normal file → Executable file
View File

0
local/utl/websocket/websocket.go Normal file → Executable file
View File