basic docker impl

This commit is contained in:
ACoolName 2025-03-16 00:50:06 +02:00
parent 2212eee35a
commit 9f19744dce
5 changed files with 478 additions and 115 deletions

View File

@ -4,22 +4,43 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"strings"
instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager"
"git.acooldomain.co/server-manager/backend-kubernetes-go/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) {
@ -27,14 +48,8 @@ func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*v
if err != nil {
return nil, err
}
var labels VolumeLabels
rawLabels, err := json.Marshal(volume.Labels)
if err != nil {
return nil, err
}
err = json.Unmarshal(rawLabels, &labels)
labels, err := convertVolumeLabelsToStruct(volume.Labels)
if err != nil {
return nil, err
}
@ -46,30 +61,6 @@ func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*v
return &volume, err
}
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
}
// General
// Read Only
func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager.Image, error) {
@ -108,17 +99,12 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i
if err != nil {
return nil, err
}
filterArgs, err := convertLabelsToFilter(ContainerLabels{
VolumeId: volume.Name,
Type: Game,
})
serverContainers, err := self.client.ContainerList(
serverContainers, err := self.containerList(
ctx,
container.ListOptions{
Filters: *filterArgs,
ContainerLabels{
VolumeId: serverId,
},
true,
)
if err != nil {
return nil, err
@ -136,17 +122,6 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i
serverContainer := serverContainers[0]
var containerLabels ContainerLabels
rawContainerLabels, err := json.Marshal(serverContainer.Labels)
if err != nil {
return nil, err
}
if err = json.Unmarshal(rawContainerLabels, &containerLabels); err != nil {
return nil, err
}
running := serverContainer.State == "running"
runningCommand := serverContainer.Command
@ -167,11 +142,6 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
return nil, err
}
containerFilters, err := convertLabelsToFilter(ContainerLabels{Type: Game})
if err != nil {
return nil, err
}
volumes, err := self.client.VolumeList(ctx, volume.ListOptions{Filters: *volumeFilter})
if err != nil {
return nil, err
@ -189,7 +159,7 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
}
}
containers, err := self.client.ContainerList(ctx, container.ListOptions{Filters: *containerFilters})
containers, err := self.containerList(ctx, ContainerLabels{Type: Game}, false)
for _, container := range containers {
rawLabels, err := json.Marshal(container.Labels)
@ -227,52 +197,358 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
}
// State Changing
func (self *InstanceManager) StartServer(ctx context.Context, serverId string, command string, ports []models.Port) error {
func (self *InstanceManager) StartServer(ctx context.Context, serverId string, image instancemanager.Image, 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)
}
imageId := image.Registry + ":" + image.Tag
containerLabels := ContainerLabels{
VolumeId: server.Id,
Type: "GAME",
}
volumes := make(map[string]struct{})
var portMapping nat.PortMap = make(nat.PortMap)
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)}}
}
}
rawImage, _, err := self.client.ImageInspectWithRaw(ctx, imageId)
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: rawImage.Config.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, image models.Image) (*instancemanager.Server, error) {
func (self *InstanceManager) CreateServer(ctx context.Context) (*instancemanager.Server, error) {
labels, err := convertLabelsToMap(VolumeLabels{
Type: Game,
})
if err != nil {
return nil, err
}
return nil, nil
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: "",
}, 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
// 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) {
server, err := self.GetServer(ctx, serverId)
if err != nil {
return nil, err
}
return "", nil
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) {
return nil, nil
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) {
return nil, nil
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) {
return nil, nil
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
}

View File

@ -8,16 +8,18 @@ const (
)
type ContainerLabels struct {
OwnerId string `json:"user_id,omitempty"`
ImageId string `json:"image_id,omitempty"`
VolumeId string `json:"volume_id,omitempty"`
Type ContainerType `json:"type,omitempty"`
}
type BrowserLabels struct {
ContainerLabels
UrlRule string
Enabled bool
}
type VolumeLabels struct {
OwnerId string `json:"user_id,omitempty"`
ImageId string `json:"image_id,omitempty"`
Type ContainerType `json:"type,omitempty"`
Type ContainerType `json:"type,omitempty"`
}
type ImageLabels struct {

View File

@ -5,51 +5,54 @@ import (
"fmt"
"log"
"reflect"
"strings"
instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager"
"git.acooldomain.co/server-manager/backend-kubernetes-go/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()
raw, err := json.Marshal(labels)
labelMap, err := convertLabelsToMap(labels)
if err != nil {
return nil, err
}
var unflattenedMap map[string]any
err = json.Unmarshal(raw, &unflattenedMap)
if err != nil {
return nil, err
}
flatMap := flattenMap(unflattenedMap)
for key, value := range flatMap {
args.Add(key, value)
for key, value := range *labelMap {
args.Add("label", fmt.Sprintf("%s=%s", key, value))
}
return &args, nil
}
func flattenMap(m map[string]any, previous ...string) map[string]string {
flattenedMap := make(map[string]string)
for key, value := range m {
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Map:
inner := flattenMap(v.Interface().(map[string]any), append(previous, key)...)
for key, value := range inner {
flattenedMap[key] = value
}
func convertLabelsToMap(labels any) (*map[string]string, error) {
default:
flattenedMap[key] = fmt.Sprintf("%v", value)
}
raw, err := json.Marshal(labels)
if err != nil {
return nil, err
}
return flattenedMap
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 {
@ -87,3 +90,75 @@ func convertContainerImageToImage(image string) models.Image {
Tag: imageTag,
}
}
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
}

View File

@ -35,20 +35,17 @@ type InstanceManager interface {
ListServers(ctx context.Context) ([]Server, error)
// State Changing
StartServer(ctx context.Context, serverId string, command string, ports []models.Port) error
StartServer(ctx context.Context, serverId string, image Image, command string, ports []models.Port) error
StopServer(ctx context.Context, serverId string) error
CreateServer(ctx context.Context, image models.Image) (*Server, error)
CreateServer(ctx context.Context) (*Server, error)
DeleteServer(ctx context.Context, serverId string) error
// Terminal
// Read Only
GetLogs(ctx context.Context, serverId string) (string, error)
// Status Changing
InteractiveTerminal(ctx context.Context, serverId string) (*net.Conn, error)
RunCommand(ctx context.Context, serverId string, command string) (string, error)
ResizeTerminal(ctx context.Context, serverId string, width uint, height uint) error
// File Browser

View File

@ -42,7 +42,19 @@ type UsersDatabaseConfig struct {
Mongo *MongoDBConfig `yaml:"mongo"`
}
type ServersInstanceManagerConfig struct {
type FileBrowserConfig struct {
Image Image `yaml:"image"`
Command string `yaml:"command"`
Network string `yaml:"network"`
}
type DockerInstanceManagerConfig struct {
BrowsersDomain string `yaml:"browsers_domain"`
CertificateResolver string `yaml:"certificate_resolver"`
FileBrowser FileBrowserConfig `yaml:"file_browser"`
}
type InstanceManagerConfig struct {
Type InstanceManagerType
}
@ -58,10 +70,11 @@ type ServersAuthorizationDatabaseConfig struct {
type GlobalConfig struct {
// Features Configs
Email EmailConfig `yaml:"email"`
Domain string `yaml:"domain"`
Signing SigningConfig `yaml:"signing"`
Authentication AuthenticationConfig `yaml:"authentication"`
Email EmailConfig `yaml:"email"`
Domain string `yaml:"domain"`
Signing SigningConfig `yaml:"signing"`
Authentication AuthenticationConfig `yaml:"authentication"`
InstanceManager InstanceManagerConfig `yaml:"instance_manager"`
// Database Configs
ServersDatabase ServersDatabaseConfig `yaml:"servers_database"`