add docker specifics
This commit is contained in:
0
local/api/api.go
Normal file → Executable file
0
local/api/api.go
Normal file → Executable file
0
local/controller/config.go
Normal file → Executable file
0
local/controller/config.go
Normal file → Executable file
0
local/controller/controller.go
Normal file → Executable file
0
local/controller/controller.go
Normal file → Executable file
0
local/controller/leaderboard.go
Normal file → Executable file
0
local/controller/leaderboard.go
Normal file → Executable file
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
0
local/controller/system.go
Normal file → Executable file
0
local/controller/system.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
0
local/middleware/auth.go
Normal file → Executable file
0
local/middleware/auth.go
Normal file → Executable file
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/model/cache.go
Normal file → Executable file
0
local/model/cache.go
Normal file → Executable file
0
local/model/config.go
Normal file → Executable file
0
local/model/config.go
Normal file → Executable file
0
local/model/filter.go
Normal file → Executable file
0
local/model/filter.go
Normal file → Executable file
0
local/model/leaderboard.go
Normal file → Executable file
0
local/model/leaderboard.go
Normal file → Executable file
0
local/model/lookup.go
Normal file → Executable file
0
local/model/lookup.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
0
local/model/service_control.go
Normal file → Executable file
0
local/model/service_control.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
0
local/repository/base.go
Normal file → Executable file
0
local/repository/base.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
0
local/repository/leaderboard.go
Normal file → Executable file
0
local/repository/leaderboard.go
Normal file → Executable file
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
0
local/repository/repository.go
Normal file → Executable file
0
local/repository/repository.go
Normal file → Executable file
0
local/repository/server.go
Normal file → Executable file
0
local/repository/server.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
0
local/service/config.go
Normal file → Executable file
0
local/service/config.go
Normal file → Executable file
74
local/service/docker_log_streamer.go
Normal file
74
local/service/docker_log_streamer.go
Normal 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)()
|
||||
}
|
||||
}
|
||||
194
local/service/docker_runtime.go
Normal file
194
local/service/docker_runtime.go
Normal 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
0
local/service/firewall_service.go
Normal file → Executable file
0
local/service/leaderboard.go
Normal file → Executable file
0
local/service/leaderboard.go
Normal file → Executable file
0
local/service/lookup.go
Normal file → Executable file
0
local/service/lookup.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
7
local/service/noop_firewall.go
Normal file
7
local/service/noop_firewall.go
Normal 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
0
local/service/service_manager.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
0
local/service/websocket.go
Normal file → Executable file
0
local/service/websocket.go
Normal file → Executable file
0
local/service/windows_service.go
Normal file → Executable file
0
local/service/windows_service.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
0
local/utl/command/callback_executor.go
Normal file → Executable file
0
local/utl/command/callback_executor.go
Normal file → Executable file
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
0
local/utl/command/interactive_executor.go
Normal file → Executable file
0
local/utl/command/interactive_executor.go
Normal file → Executable file
0
local/utl/common/common.go
Normal file → Executable file
0
local/utl/common/common.go
Normal file → Executable file
0
local/utl/configs/configs.go
Normal file → Executable file
0
local/utl/configs/configs.go
Normal file → Executable file
12
local/utl/docker/client.go
Normal file
12
local/utl/docker/client.go
Normal 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
0
local/utl/error_handler/controller_error_handler.go
Normal file → Executable file
0
local/utl/errors/safe_error.go
Normal file → Executable file
0
local/utl/errors/safe_error.go
Normal file → Executable file
0
local/utl/graceful/shutdown.go
Normal file → Executable file
0
local/utl/graceful/shutdown.go
Normal file → Executable file
0
local/utl/jwt/jwt.go
Normal file → Executable file
0
local/utl/jwt/jwt.go
Normal file → Executable file
0
local/utl/logging/base.go
Normal file → Executable file
0
local/utl/logging/base.go
Normal file → Executable file
0
local/utl/logging/debug.go
Normal file → Executable file
0
local/utl/logging/debug.go
Normal file → Executable file
0
local/utl/logging/error.go
Normal file → Executable file
0
local/utl/logging/error.go
Normal file → Executable file
0
local/utl/logging/info.go
Normal file → Executable file
0
local/utl/logging/info.go
Normal file → Executable file
0
local/utl/logging/logger.go
Normal file → Executable file
0
local/utl/logging/logger.go
Normal file → Executable file
0
local/utl/logging/warn.go
Normal file → Executable file
0
local/utl/logging/warn.go
Normal file → Executable file
0
local/utl/network/port.go
Normal file → Executable file
0
local/utl/network/port.go
Normal file → Executable file
0
local/utl/password/password.go
Normal file → Executable file
0
local/utl/password/password.go
Normal file → Executable file
0
local/utl/regex_handler/regex_handler.go
Normal file → Executable file
0
local/utl/regex_handler/regex_handler.go
Normal file → Executable file
0
local/utl/security/download_verifier.go
Normal file → Executable file
0
local/utl/security/download_verifier.go
Normal file → Executable file
0
local/utl/security/path_validator.go
Normal file → Executable file
0
local/utl/security/path_validator.go
Normal file → Executable file
0
local/utl/tracking/log_tailer.go
Normal file → Executable file
0
local/utl/tracking/log_tailer.go
Normal file → Executable file
0
local/utl/tracking/position_tracker.go
Normal file → Executable file
0
local/utl/tracking/position_tracker.go
Normal file → Executable file
0
local/utl/tracking/tracking.go
Normal file → Executable file
0
local/utl/tracking/tracking.go
Normal file → Executable file
0
local/utl/websocket/websocket.go
Normal file → Executable file
0
local/utl/websocket/websocket.go
Normal file → Executable file
Reference in New Issue
Block a user