refactored

This commit is contained in:
2025-03-18 23:27:27 +02:00
parent 64f59ea232
commit 6c1f34c682
45 changed files with 398 additions and 413 deletions

View File

@@ -0,0 +1,3 @@
module git.acooldomain.co/server-manager/backend/instancemanager/docker
go 1.22.0

View File

@@ -0,0 +1,584 @@
package docker
import (
"context"
"encoding/json"
"fmt"
"net"
instancemanager "git.acooldomain.co/server-manager/backend/instancemanager"
"git.acooldomain.co/server-manager/backend/models"
"github.com/buildkite/shellwords"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type InstanceManager struct {
instancemanager.InstanceManager
client client.Client
config models.DockerInstanceManagerConfig
}
func (self *InstanceManager) containerList(ctx context.Context, labels ContainerLabels, all bool) ([]types.Container, error) {
filters, err := convertLabelsToFilter(labels)
if err != nil {
return nil, err
}
containers, err := self.client.ContainerList(ctx, container.ListOptions{Filters: *filters, All: all})
if err != nil {
return nil, err
}
return containers, nil
}
func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*volume.Volume, error) {
volume, err := self.client.VolumeInspect(ctx, serverId)
if err != nil {
return nil, err
}
labels, err := convertVolumeLabelsToStruct(volume.Labels)
if err != nil {
return nil, err
}
if labels.Type != Game {
return nil, fmt.Errorf("Server not found")
}
return &volume, err
}
// General
// Read Only
func (self *InstanceManager) GetImage(ctx context.Context, imageId string) (*instancemanager.Image, error) {
imageInspect, _, err := self.client.ImageInspectWithRaw(ctx, imageId)
if err != nil {
return nil, err
}
if imageInspect.Config.Labels["type"] != "game" {
return nil, fmt.Errorf("Image not found")
}
image := convertImageInspectToInstanceImage(imageInspect)
return &image, nil
}
func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager.Image, error) {
imageFilters, err := convertLabelsToFilter(ImageLabels{Type: Game})
if err != nil {
return nil, err
}
rawImages, err := self.client.ImageList(ctx, image.ListOptions{
Filters: *imageFilters,
})
images := make([]instancemanager.Image, len(rawImages))
for i, rawImage := range rawImages {
imageInspect, _, err := self.client.ImageInspectWithRaw(ctx, rawImage.ID)
if err != nil {
return nil, err
}
images[i] = convertImageInspectToInstanceImage(imageInspect)
}
return images, nil
}
func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*instancemanager.Server, error) {
volume, err := self.getVolume(ctx, serverId)
if err != nil {
return nil, err
}
serverContainers, err := self.containerList(
ctx,
ContainerLabels{
VolumeId: serverId,
},
true,
)
if err != nil {
return nil, err
}
if len(serverContainers) == 0 {
return &instancemanager.Server{
Id: volume.Name,
Running: false,
RunningCommand: "",
RunningImage: nil,
Ports: nil,
Domain: self.config.GamesDomain,
}, nil
}
serverContainer := serverContainers[0]
running := serverContainer.State == "running"
runningCommand := serverContainer.Command
image := convertImageStringToModelsImage(serverContainer.Image)
return &instancemanager.Server{
Id: volume.Name,
Running: running,
RunningCommand: runningCommand,
Ports: convertContainerPortsToPorts(serverContainer.Ports),
RunningImage: &image,
Domain: self.config.GamesDomain,
}, nil
}
func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager.Server, error) {
volumeFilter, err := convertLabelsToFilter(VolumeLabels{Type: Game})
if err != nil {
return nil, err
}
volumes, err := self.client.VolumeList(ctx, volume.ListOptions{Filters: *volumeFilter})
if err != nil {
return nil, err
}
serverStatus := make(map[string]*instancemanager.Server)
for _, volume := range volumes.Volumes {
serverStatus[volume.Name] = &instancemanager.Server{
Id: volume.Name,
Running: false,
RunningCommand: "",
Ports: nil,
RunningImage: nil,
Domain: self.config.GamesDomain,
}
}
containers, err := self.containerList(ctx, ContainerLabels{Type: Game}, false)
for _, container := range containers {
rawLabels, err := json.Marshal(container.Labels)
if err != nil {
return nil, err
}
var containerLabels ContainerLabels
err = json.Unmarshal(rawLabels, &containerLabels)
if err != nil {
return nil, err
}
if container.State != "running" {
continue
}
image := convertImageStringToModelsImage(container.Image)
serverStatus[containerLabels.VolumeId].Ports = convertContainerPortsToPorts(container.Ports)
serverStatus[containerLabels.VolumeId].Running = true
serverStatus[containerLabels.VolumeId].RunningImage = &image
serverStatus[containerLabels.VolumeId].RunningCommand = container.Command
}
servers := make([]instancemanager.Server, len(serverStatus))
i := 0
for _, value := range serverStatus {
servers[i] = *value
i++
}
return servers, nil
}
// State Changing
func (self *InstanceManager) StartServer(ctx context.Context,
serverId string,
imageId string,
command string,
ports []models.Port,
) error {
server, err := self.GetServer(ctx, serverId)
if err != nil {
return err
}
if server.Running {
return fmt.Errorf("Server %s already running", serverId)
}
containerLabels := ContainerLabels{
VolumeId: server.Id,
Type: "GAME",
}
volumes := make(map[string]struct{})
var portMapping nat.PortMap = make(nat.PortMap)
image, err := self.GetImage(ctx, imageId)
if err != nil {
return err
}
if len(ports) == 0 {
for _, port := range image.Ports {
dockerPort, err := nat.NewPort(string(port.Protocol), fmt.Sprint(port.Number))
if err != nil {
return err
}
portMapping[dockerPort] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
}
} else {
for _, portCouple := range ports {
portMapping[nat.Port(fmt.Sprintf("%d/%s", portCouple.ContainerPort, portCouple.Protocol))] = []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: fmt.Sprint(portCouple.PublicPort)}}
}
}
if command == "" {
command = image.Command
}
words, err := shellwords.Split(command)
if err != nil {
return err
}
if len(words) == 0 {
words = nil
}
portSet := make(nat.PortSet)
for port := range portMapping {
portSet[port] = struct{}{}
}
labels, err := convertLabelsToMap(containerLabels)
if err != nil {
return err
}
createdContainer, err := self.client.ContainerCreate(
context.TODO(),
&container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
OpenStdin: true,
StdinOnce: false,
Image: imageId,
Volumes: volumes,
Labels: *labels,
Cmd: words,
ExposedPorts: portSet,
},
&container.HostConfig{
AutoRemove: true,
Mounts: []mount.Mount{{Source: server.Id, Target: image.WorkingDir, Type: "volume"}},
PortBindings: portMapping,
ConsoleSize: [2]uint{1000, 1000},
},
&network.NetworkingConfig{},
&v1.Platform{},
"",
)
if err != nil {
return err
}
err = self.client.ContainerStart(ctx, createdContainer.ID, container.StartOptions{})
if err != nil {
return err
}
return nil
}
func (self *InstanceManager) StopServer(ctx context.Context, serverId string) error {
runningContainers, err := self.containerList(ctx, ContainerLabels{VolumeId: serverId, Type: Game}, false)
for _, rawContainer := range runningContainers {
err = self.client.ContainerStop(ctx, rawContainer.ID, container.StopOptions{})
if err != nil {
return err
}
}
return nil
}
func (self *InstanceManager) CreateServer(ctx context.Context) (*instancemanager.Server, error) {
labels, err := convertLabelsToMap(VolumeLabels{
Type: Game,
})
if err != nil {
return nil, err
}
volume, err := self.client.VolumeCreate(ctx, volume.CreateOptions{
Labels: *labels,
})
if err != nil {
return nil, err
}
return &instancemanager.Server{
Id: volume.Name,
Running: false,
RunningImage: nil,
RunningCommand: "",
Domain: self.config.GamesDomain,
}, nil
}
func (self *InstanceManager) DeleteServer(ctx context.Context, serverId string) error {
server, err := self.GetServer(ctx, serverId)
if err != nil {
return err
}
if server.Running {
err = self.StopServer(ctx, server.Id)
if err != nil {
return err
}
}
err = self.StopFileBrowser(ctx, server.Id)
if err != nil {
return err
}
err = self.client.VolumeRemove(ctx, server.Id, true)
if err != nil {
return err
}
return nil
}
// Terminal
// Status Changing
func (self *InstanceManager) InteractiveTerminal(ctx context.Context, serverId string) (*net.Conn, error) {
server, err := self.GetServer(ctx, serverId)
if err != nil {
return nil, err
}
if !server.Running {
return nil, fmt.Errorf("Server %s not running", server.Id)
}
rawContainers, err := self.containerList(ctx, ContainerLabels{VolumeId: serverId, Type: Game}, false)
if err != nil {
return nil, err
}
if len(rawContainers) == 0 {
return nil, fmt.Errorf("Server %s not running and wasn't caught", server.Id)
}
rawContainer := rawContainers[0]
attach, err := self.client.ContainerAttach(ctx,
rawContainer.ID,
container.AttachOptions{Stream: true,
Stdin: true,
Stdout: true,
Stderr: true,
Logs: true,
},
)
if err != nil {
return nil, err
}
return &attach.Conn, nil
}
func (self *InstanceManager) ResizeTerminal(ctx context.Context, serverId string, width uint, height uint) error {
containers, err := self.containerList(ctx, ContainerLabels{VolumeId: serverId, Type: Game}, false)
if err != nil {
return err
}
if len(containers) == 0 {
return fmt.Errorf("Server %s not running", serverId)
}
err = self.client.ContainerResize(context.TODO(), containers[0].ID, container.ResizeOptions{Height: height, Width: width})
return err
}
// File Browser
// Read Only
func (self *InstanceManager) GetFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) {
containers, err := self.containerList(ctx, ContainerLabels{VolumeId: serverId, Type: FileBrowser}, false)
if err != nil {
return nil, err
}
if len(containers) == 0 {
return nil, fmt.Errorf("File Browser for server %s is not running", serverId)
}
rawContainer := containers[0]
containerLabels, err := convertContainerLabelsToStruct(rawContainer.Labels)
if err != nil {
return nil, err
}
return &models.FileBrowser{
Url: containerLabels.VolumeId[:12] + "." + self.config.BrowsersDomain,
Id: rawContainer.ID,
}, nil
}
func (self *InstanceManager) ListFileBrowsers(ctx context.Context) ([]models.FileBrowser, error) {
containers, err := self.containerList(ctx, ContainerLabels{Type: FileBrowser}, false)
if err != nil {
return nil, err
}
fileBrowsers := make([]models.FileBrowser, len(containers))
for i, rawContainer := range containers {
containerLabels, err := convertContainerLabelsToStruct(rawContainer.Labels)
if err != nil {
return nil, err
}
fileBrowsers[i] = models.FileBrowser{
Url: containerLabels.VolumeId[:12] + "." + self.config.BrowsersDomain,
Id: rawContainer.ID,
}
}
return fileBrowsers, nil
}
// Status Changing
func (self *InstanceManager) StartFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) {
server, err := self.GetServer(ctx, serverId)
if err != nil {
return nil, err
}
labelId := serverId[:12]
browserLabels := make(map[string]string)
browserLabels["traefik.enable"] = "true"
browserLabels[fmt.Sprintf("traefik.http.routers.%s.rule", labelId)] = fmt.Sprintf("Host(`%s.browsers.%s`)", labelId, self.config.BrowsersDomain)
browserLabels[fmt.Sprintf("traefik.http.routers.%s.entrypoints", labelId)] = "websecure"
browserLabels[fmt.Sprintf("traefik.http.routers.%s.middlewares", labelId)] = "games@docker"
browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.domains[0].main", labelId)] = fmt.Sprintf("%s.%s", "browsers", self.config.BrowsersDomain)
browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.domains[0].sans", labelId)] = fmt.Sprintf("*.%s.%s", "browsers", self.config.BrowsersDomain)
browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", labelId)] = "myresolver"
containerConfig, err := convertLabelsToMap(ContainerLabels{VolumeId: serverId, Type: FileBrowser})
if err != nil {
return nil, err
}
for key, value := range *containerConfig {
browserLabels[key] = value
}
command := self.config.FileBrowser.Command
if command == "" {
command = "--noauth -r /tmp/data"
}
splitCommand, err := shellwords.Split(command)
if err != nil {
return nil, err
}
ContainerResponse, err := self.client.ContainerCreate(
context.TODO(),
&container.Config{
Cmd: splitCommand,
Image: fmt.Sprintf("%s:%s", self.config.FileBrowser.Image.Registry, self.config.FileBrowser.Image.Tag),
Labels: browserLabels,
Tty: true,
},
&container.HostConfig{
Mounts: []mount.Mount{{Source: server.Id, Target: "/tmp/data", Type: "volume"}},
AutoRemove: true,
ConsoleSize: [2]uint{1000, 1000},
},
&network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{"browsers": {
NetworkID: self.config.FileBrowser.Network,
}},
},
&v1.Platform{},
"",
)
if err != nil {
return nil, err
}
err = self.client.ContainerStart(context.TODO(), ContainerResponse.ID, container.StartOptions{})
if err != nil {
return nil, err
}
return &models.FileBrowser{
Url: serverId[:12] + "." + self.config.BrowsersDomain,
Id: ContainerResponse.ID,
}, nil
}
func (self *InstanceManager) StopFileBrowser(ctx context.Context, serverId string) error {
containers, err := self.containerList(ctx, ContainerLabels{Type: FileBrowser}, false)
if err != nil {
return err
}
for _, rawContainer := range containers {
err := self.client.ContainerStop(ctx, rawContainer.ID, container.StopOptions{})
if err != nil {
return err
}
}
return nil
}
func NewInstanceManager(config models.DockerInstanceManagerConfig) (*InstanceManager, error) {
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}
defer apiClient.Close()
return &InstanceManager{
config: config,
client: *apiClient,
}, nil
}

View File

@@ -0,0 +1,27 @@
package docker
type ContainerType string
const (
Game ContainerType = "GAME"
FileBrowser = "FILE_BROWSER"
)
type ContainerLabels struct {
VolumeId string `json:"volume_id,omitempty"`
Type ContainerType `json:"type,omitempty"`
}
type BrowserLabels struct {
ContainerLabels
UrlRule string
Enabled bool
}
type VolumeLabels struct {
Type ContainerType `json:"type,omitempty"`
}
type ImageLabels struct {
Type ContainerType `json:"type,omitempty"`
}

View File

@@ -0,0 +1,179 @@
package docker
import (
"encoding/json"
"fmt"
"log"
"strings"
instancemanager "git.acooldomain.co/server-manager/backend/instancemanager"
"git.acooldomain.co/server-manager/backend/models"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/go-connections/nat"
)
func convertLabelsToFilter(labels any) (*filters.Args, error) {
args := filters.NewArgs()
labelMap, err := convertLabelsToMap(labels)
if err != nil {
return nil, err
}
for key, value := range *labelMap {
args.Add("label", fmt.Sprintf("%s=%s", key, value))
}
return &args, nil
}
func convertLabelsToMap(labels any) (*map[string]string, error) {
raw, err := json.Marshal(labels)
if err != nil {
return nil, err
}
var rawMap map[string]any
err = json.Unmarshal(raw, &rawMap)
if err != nil {
return nil, err
}
stringifiedMap := stringifyMap(rawMap)
return &stringifiedMap, nil
}
func stringifyMap(m map[string]any) map[string]string {
stringifiedMap := make(map[string]string)
for key, value := range m {
stringifiedMap[key] = fmt.Sprintf("%v", value)
}
return stringifiedMap
}
func convertContainerPortsToPorts(ports []types.Port) []models.Port {
containerPorts := make([]models.Port, len(ports))
logger := log.Default()
for i, port := range ports {
var portProtocol models.PortProtocol
switch port.Type {
case "TCP":
portProtocol = models.TCP
case "UDP":
portProtocol = models.UDP
default:
logger.Println(fmt.Sprintf("Unkown Port Protocol %s assuming TCP", port.Type))
portProtocol = models.TCP
}
containerPorts[i] = models.Port{
PublicPort: port.PublicPort,
ContainerPort: port.PrivatePort,
Protocol: portProtocol,
}
}
return containerPorts
}
func convertImageStringToModelsImage(image string) models.Image {
imageSegments := strings.Split(image, ":")
imageRegistry := imageSegments[0]
imageTag := imageSegments[1]
return models.Image{
Registry: imageRegistry,
Tag: imageTag,
}
}
func convertImageInspectToInstanceImage(image types.ImageInspect) instancemanager.Image {
modelsImage := convertImageStringToModelsImage(image.RepoTags[0])
ports := convertImagePortsToPorts(image.Config.ExposedPorts)
return instancemanager.Image{
Registry: modelsImage.Registry,
Tag: modelsImage.Tag,
Command: strings.Join(image.Config.Cmd, " "),
Ports: ports,
WorkingDir: image.Config.WorkingDir,
}
}
func convertContainerLabelsToStruct(labels map[string]string) (*ContainerLabels, error) {
var containerLabels ContainerLabels
rawLabels, err := json.Marshal(labels)
if err != nil {
return nil, err
}
err = json.Unmarshal(rawLabels, &labels)
if err != nil {
return nil, err
}
return &containerLabels, nil
}
func convertVolumeLabelsToStruct(labels map[string]string) (*VolumeLabels, error) {
var volumeLabels VolumeLabels
rawLabels, err := json.Marshal(labels)
if err != nil {
return nil, err
}
err = json.Unmarshal(rawLabels, &labels)
if err != nil {
return nil, err
}
return &volumeLabels, nil
}
func convertImageLabelsToStruct(labels map[string]string) (*ImageLabels, error) {
var imageLabels ImageLabels
rawLabels, err := json.Marshal(labels)
if err != nil {
return nil, err
}
err = json.Unmarshal(rawLabels, &labels)
if err != nil {
return nil, err
}
return &imageLabels, nil
}
func convertImagePortsToPorts(rawPorts nat.PortSet) []instancemanager.Port {
ports := make([]instancemanager.Port, len(rawPorts))
i := 0
for imagePort := range rawPorts {
portNumber := imagePort.Int()
var protocol models.PortProtocol
switch imagePort.Proto() {
case "TCP":
protocol = models.TCP
case "UDP":
protocol = models.UDP
default:
log.Default().Println(fmt.Sprintf("Unknown port protocol %s using TCP", imagePort.Proto()))
protocol = models.TCP
}
ports[i] = instancemanager.Port{
Number: uint16(portNumber),
Protocol: protocol,
}
i++
}
return ports
}

3
instancemanager/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.acooldomain.co/server-manager/backend/instancemanager
go 1.22.0

View File

@@ -0,0 +1,62 @@
package instancemanager
import (
"context"
"net"
"git.acooldomain.co/server-manager/backend/models"
)
type Server struct {
Id string
Running bool
RunningCommand string
RunningImage *models.Image
Ports []models.Port
Domain string
}
type Port struct {
Number uint16
Protocol models.PortProtocol
}
type Image struct {
Registry string
Tag string
Command string
WorkingDir string
Ports []Port
}
type InstanceManager interface {
//General
// Read Only
GetImage(ctx context.Context, imageId string) (*Image, error)
ListImages(ctx context.Context) ([]Image, error)
GetServer(ctx context.Context, serverId string) (*Server, error)
ListServers(ctx context.Context) ([]Server, error)
// State Changing
StartServer(ctx context.Context, serverId string, imageId string, command string, ports []models.Port) error
StopServer(ctx context.Context, serverId string) error
CreateServer(ctx context.Context) (*Server, error)
DeleteServer(ctx context.Context, serverId string) error
// Terminal
// Status Changing
InteractiveTerminal(ctx context.Context, serverId string) (*net.Conn, error)
ResizeTerminal(ctx context.Context, serverId string, width uint, height uint) error
// File Browser
// Read Only
GetFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error)
ListFileBrowsers(ctx context.Context) ([]models.FileBrowser, error)
// Status Changing
StartFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error)
StopFileBrowser(ctx context.Context, serverId string) error
}

View File

@@ -0,0 +1,3 @@
module git.acooldomain.co/server-manager/backend/instancemanager/kubernetes
go 1.22.0

View File

@@ -0,0 +1,73 @@
package kubernetes
import (
"context"
"net"
"git.acooldomain.co/server-manager/backend/instancemanager"
"git.acooldomain.co/server-manager/backend/models"
)
type InstanceManager struct {
instancemanager.InstanceManager
}
// General
// Read Only
func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*instancemanager.Server, error) {
return nil, nil
}
func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager.Server, error) {
return nil, nil
}
// State Changing
func (self *InstanceManager) StartServer(ctx context.Context, serverId string, command string, ports []models.Port) error {
return nil
}
func (self *InstanceManager) StopServer(ctx context.Context, serverId string) error {
return nil
}
func (self *InstanceManager) CreateServer(ctx context.Context, image models.Image) (*instancemanager.Server, error) {
return nil, nil
}
func (self *InstanceManager) DeleteServer(ctx context.Context, serverId string) error {
return nil
}
// Terminal
// Read Only
func (self *InstanceManager) GetLogs(ctx context.Context, serverId string) (string, error) {
return "", nil
}
// Status Changing
func (self *InstanceManager) InteractiveTerminal(ctx context.Context, serverId string) (*net.Conn, error) {
return nil, nil
}
func (self *InstanceManager) RunCommand(ctx context.Context, serverId string, command string) (string, error) {
return "", nil
}
// File Browser
// Read Only
func (self *InstanceManager) GetFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) {
return nil, nil
}
func (self *InstanceManager) ListFileBrowsers(ctx context.Context) ([]models.FileBrowser, error) {
return nil, nil
}
// Status Changing
func (self *InstanceManager) StartFileBrowser(ctx context.Context, serverId string) (*models.FileBrowser, error) {
return nil, nil
}
func (self *InstanceManager) StopFileBrowser(ctx context.Context, serverId string) error {
return nil
}