added basic servers implementation

This commit is contained in:
ACoolName 2025-03-16 19:23:02 +02:00
parent d1f1d4e7c9
commit f57888cb8e
8 changed files with 280 additions and 552 deletions

View File

@ -1,6 +1,10 @@
package dbhandler package dbhandler
import "git.acooldomain.co/server-manager/backend-kubernetes-go/models" import (
"context"
"git.acooldomain.co/server-manager/backend-kubernetes-go/models"
)
type Server struct { type Server struct {
Id string Id string
@ -20,9 +24,9 @@ type ServerUpdateRequest struct {
} }
type ServersDbHandler interface { type ServersDbHandler interface {
ListServers() ([]Server, error) ListServers(ctx context.Context) ([]Server, error)
GetServer(serverId string) (*Server, error) GetServer(ctx context.Context, serverId string) (*Server, error)
CreateServer(server Server) error CreateServer(ctx context.Context, server Server) error
DeleteServer(serverId string) error DeleteServer(ctx context.Context, serverId string) error
UpdateServer(serverId string, updateParams ServerUpdateRequest) error UpdateServer(ctx context.Context, serverId string, updateParams ServerUpdateRequest) error
} }

View File

@ -1,12 +1,16 @@
package dbhandler package dbhandler
import "git.acooldomain.co/server-manager/backend-kubernetes-go/models" import (
"context"
"git.acooldomain.co/server-manager/backend-kubernetes-go/models"
)
type ServersAuthorizationDbHandler interface { type ServersAuthorizationDbHandler interface {
AddPermissions(username string, server_id string, permissions models.Permission) error AddPermissions(ctx context.Context, username string, server_id string, permissions models.Permission) error
RemovePermissions(username string, server_id string, permissions models.Permission) error RemovePermissions(ctx context.Context, string, server_id string, permissions models.Permission) error
SetPermissions(username string, server_id string, permissions models.Permission) error SetPermissions(ctx context.Context, username string, server_id string, permissions models.Permission) error
GetPermissions(username string, server_id string) (models.Permission, error) GetPermissions(ctx context.Context, username string, server_id string) (models.Permission, error)
RemoveUser(username string) error RemoveUser(ctx context.Context, username string) error
RemoveServer(server_id string) error RemoveServer(ctx context.Context, server_id string) error
} }

View File

@ -4,10 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log"
"net" "net"
"strings"
instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager" instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager"
"git.acooldomain.co/server-manager/backend-kubernetes-go/models" "git.acooldomain.co/server-manager/backend-kubernetes-go/models"
@ -63,6 +60,22 @@ func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*v
// General // General
// Read Only // 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) { func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager.Image, error) {
imageFilters, err := convertLabelsToFilter(ImageLabels{Type: Game}) imageFilters, err := convertLabelsToFilter(ImageLabels{Type: Game})
if err != nil { if err != nil {
@ -81,14 +94,7 @@ func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager.
return nil, err return nil, err
} }
ports := convertImagePortsToPorts(imageInspect.Config.ExposedPorts) images[i] = convertImageInspectToInstanceImage(imageInspect)
imageId := convertContainerImageToImage(rawImage.RepoTags[0])
images[i] = instancemanager.Image{
Registry: imageId.Registry,
Tag: imageId.Tag,
Ports: ports,
Command: strings.Join(imageInspect.Config.Cmd, " "),
}
} }
return images, nil return images, nil
@ -117,6 +123,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i
RunningCommand: "", RunningCommand: "",
RunningImage: nil, RunningImage: nil,
Ports: nil, Ports: nil,
Domain: self.config.GamesDomain,
}, nil }, nil
} }
@ -125,7 +132,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i
running := serverContainer.State == "running" running := serverContainer.State == "running"
runningCommand := serverContainer.Command runningCommand := serverContainer.Command
image := convertContainerImageToImage(serverContainer.Image) image := convertImageStringToModelsImage(serverContainer.Image)
return &instancemanager.Server{ return &instancemanager.Server{
Id: volume.Name, Id: volume.Name,
@ -133,6 +140,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i
RunningCommand: runningCommand, RunningCommand: runningCommand,
Ports: convertContainerPortsToPorts(serverContainer.Ports), Ports: convertContainerPortsToPorts(serverContainer.Ports),
RunningImage: &image, RunningImage: &image,
Domain: self.config.GamesDomain,
}, nil }, nil
} }
@ -156,6 +164,7 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
RunningCommand: "", RunningCommand: "",
Ports: nil, Ports: nil,
RunningImage: nil, RunningImage: nil,
Domain: self.config.GamesDomain,
} }
} }
@ -177,7 +186,7 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
continue continue
} }
image := convertContainerImageToImage(container.Image) image := convertImageStringToModelsImage(container.Image)
serverStatus[containerLabels.VolumeId].Ports = convertContainerPortsToPorts(container.Ports) serverStatus[containerLabels.VolumeId].Ports = convertContainerPortsToPorts(container.Ports)
serverStatus[containerLabels.VolumeId].Running = true serverStatus[containerLabels.VolumeId].Running = true
@ -197,7 +206,12 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager
} }
// State Changing // State Changing
func (self *InstanceManager) StartServer(ctx context.Context, serverId string, image instancemanager.Image, command string, ports []models.Port) error { func (self *InstanceManager) StartServer(ctx context.Context,
serverId string,
imageId string,
command string,
ports []models.Port,
) error {
server, err := self.GetServer(ctx, serverId) server, err := self.GetServer(ctx, serverId)
if err != nil { if err != nil {
return err return err
@ -207,7 +221,6 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i
return fmt.Errorf("Server %s already running", serverId) return fmt.Errorf("Server %s already running", serverId)
} }
imageId := image.Registry + ":" + image.Tag
containerLabels := ContainerLabels{ containerLabels := ContainerLabels{
VolumeId: server.Id, VolumeId: server.Id,
Type: "GAME", Type: "GAME",
@ -216,6 +229,10 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i
volumes := make(map[string]struct{}) volumes := make(map[string]struct{})
var portMapping nat.PortMap = make(nat.PortMap) var portMapping nat.PortMap = make(nat.PortMap)
image, err := self.GetImage(ctx, imageId)
if err != nil {
return err
}
if len(ports) == 0 { if len(ports) == 0 {
for _, port := range image.Ports { for _, port := range image.Ports {
@ -232,8 +249,6 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i
} }
} }
rawImage, _, err := self.client.ImageInspectWithRaw(ctx, imageId)
if command == "" { if command == "" {
command = image.Command command = image.Command
} }
@ -274,7 +289,7 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i
}, },
&container.HostConfig{ &container.HostConfig{
AutoRemove: true, AutoRemove: true,
Mounts: []mount.Mount{{Source: server.Id, Target: rawImage.Config.WorkingDir, Type: "volume"}}, Mounts: []mount.Mount{{Source: server.Id, Target: image.WorkingDir, Type: "volume"}},
PortBindings: portMapping, PortBindings: portMapping,
ConsoleSize: [2]uint{1000, 1000}, ConsoleSize: [2]uint{1000, 1000},
}, },
@ -328,6 +343,7 @@ func (self *InstanceManager) CreateServer(ctx context.Context) (*instancemanager
Running: false, Running: false,
RunningImage: nil, RunningImage: nil,
RunningCommand: "", RunningCommand: "",
Domain: self.config.GamesDomain,
}, nil }, nil
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"reflect"
"strings" "strings"
instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager" instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager"
@ -21,6 +20,7 @@ func convertLabelsToFilter(labels any) (*filters.Args, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
for key, value := range *labelMap { for key, value := range *labelMap {
args.Add("label", fmt.Sprintf("%s=%s", key, value)) args.Add("label", fmt.Sprintf("%s=%s", key, value))
} }
@ -29,7 +29,6 @@ func convertLabelsToFilter(labels any) (*filters.Args, error) {
} }
func convertLabelsToMap(labels any) (*map[string]string, error) { func convertLabelsToMap(labels any) (*map[string]string, error) {
raw, err := json.Marshal(labels) raw, err := json.Marshal(labels)
if err != nil { if err != nil {
return nil, err return nil, err
@ -80,7 +79,7 @@ func convertContainerPortsToPorts(ports []types.Port) []models.Port {
return containerPorts return containerPorts
} }
func convertContainerImageToImage(image string) models.Image { func convertImageStringToModelsImage(image string) models.Image {
imageSegments := strings.Split(image, ":") imageSegments := strings.Split(image, ":")
imageRegistry := imageSegments[0] imageRegistry := imageSegments[0]
imageTag := imageSegments[1] imageTag := imageSegments[1]
@ -91,6 +90,20 @@ func convertContainerImageToImage(image string) models.Image {
} }
} }
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) { func convertContainerLabelsToStruct(labels map[string]string) (*ContainerLabels, error) {
var containerLabels ContainerLabels var containerLabels ContainerLabels
@ -104,6 +117,7 @@ func convertContainerLabelsToStruct(labels map[string]string) (*ContainerLabels,
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &containerLabels, nil return &containerLabels, nil
} }
@ -120,6 +134,7 @@ func convertVolumeLabelsToStruct(labels map[string]string) (*VolumeLabels, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &volumeLabels, nil return &volumeLabels, nil
} }

View File

@ -13,6 +13,7 @@ type Server struct {
RunningCommand string RunningCommand string
RunningImage *models.Image RunningImage *models.Image
Ports []models.Port Ports []models.Port
Domain string
} }
type Port struct { type Port struct {
@ -24,18 +25,20 @@ type Image struct {
Registry string Registry string
Tag string Tag string
Command string Command string
WorkingDir string
Ports []Port Ports []Port
} }
type InstanceManager interface { type InstanceManager interface {
//General //General
// Read Only // Read Only
GetImage(ctx context.Context, imageId string) (*Image, error)
ListImages(ctx context.Context) ([]Image, error) ListImages(ctx context.Context) ([]Image, error)
GetServer(ctx context.Context, serverId string) (*Server, error) GetServer(ctx context.Context, serverId string) (*Server, error)
ListServers(ctx context.Context) ([]Server, error) ListServers(ctx context.Context) ([]Server, error)
// State Changing // State Changing
StartServer(ctx context.Context, serverId string, image Image, command string, ports []models.Port) error StartServer(ctx context.Context, serverId string, imageId string, command string, ports []models.Port) error
StopServer(ctx context.Context, serverId string) error StopServer(ctx context.Context, serverId string) error
CreateServer(ctx context.Context) (*Server, error) CreateServer(ctx context.Context) (*Server, error)

13
main.go
View File

@ -18,14 +18,6 @@ import (
const CONFIG_SECRET_NAME = "CONFIG_PATH" const CONFIG_SECRET_NAME = "CONFIG_PATH"
func get_servers_db_handler(config models.ServersDatabaseConfig) {
}
func get_users_db_handler(config models.UsersDatabaseConfig) {
}
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -49,11 +41,6 @@ func main() {
panic(err) panic(err)
} }
switch config.UserDatabase.Mongo {
case nil:
default:
}
client, err := dbhandler.Connect(config.UsersDatabase.Mongo) client, err := dbhandler.Connect(config.UsersDatabase.Mongo)
defer func() { defer func() {
if err = client.Disconnect(context.TODO()); err != nil { if err = client.Disconnect(context.TODO()); err != nil {

View File

@ -49,13 +49,15 @@ type FileBrowserConfig struct {
} }
type DockerInstanceManagerConfig struct { type DockerInstanceManagerConfig struct {
GamesDomain string `yaml:"games_domain"`
BrowsersDomain string `yaml:"browsers_domain"` BrowsersDomain string `yaml:"browsers_domain"`
CertificateResolver string `yaml:"certificate_resolver"` CertificateResolver string `yaml:"certificate_resolver"`
FileBrowser FileBrowserConfig `yaml:"file_browser"` FileBrowser FileBrowserConfig `yaml:"file_browser"`
} }
type InstanceManagerConfig struct { type InstanceManagerConfig struct {
Type InstanceManagerType Type InstanceManagerType `yaml:"type"`
Docker DockerInstanceManagerConfig `yaml:"docker"`
} }
type ServersDatabaseConfig struct { type ServersDatabaseConfig struct {

View File

@ -7,56 +7,21 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"git.acooldomain.co/server-manager/backend-kubernetes-go/auth" "git.acooldomain.co/server-manager/backend-kubernetes-go/auth"
"git.acooldomain.co/server-manager/backend-kubernetes-go/db_handler/mongo"
"git.acooldomain.co/server-manager/backend-kubernetes-go/dbhandler"
instancemanager "git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager"
"git.acooldomain.co/server-manager/backend-kubernetes-go/instance_manager/docker"
"git.acooldomain.co/server-manager/backend-kubernetes-go/models" "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/filters"
"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"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
) )
const FILE_BROWSER_IMAGE = "filebrowser/filebrowser:latest"
var DOMAIN string = ""
type Connection struct {
databaseConnection *mongo.Client
dockerClient *client.Client
}
type ContainerLabels struct {
OwnerId string `json:"user_id"`
ImageId string `json:"image_id"`
VolumeId string `json:"volume_id"`
Type string `json:"type"`
}
type VolumeLabels struct {
OwnerId string `json:"user_id"`
ImageId string `json:"image_id"`
Type string `json:"type"`
}
type ImageLabels struct {
Type string `json:"type"`
}
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
@ -65,113 +30,34 @@ var upgrader = websocket.Upgrader{
}, },
} }
func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, error) { type Connection struct {
var volumeLabels VolumeLabels ServersDbHandler dbhandler.ServersDbHandler
var serverData models.ServerData InstanceManager instancemanager.InstanceManager
ServerAuthorization dbhandler.ServersAuthorizationDbHandler
con.databaseConnection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "Id", Value: volume.Name}}).Decode(&serverData)
jsonData, err := json.Marshal(volume.Labels)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonData, &volumeLabels)
if err != nil {
return nil, err
}
var imagePorts []models.Port
if len(serverData.DefaultPorts) == 0 {
imageList, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("reference", volumeLabels.ImageId), filters.Arg("label", "type=GAME"))})
if err != nil {
return nil, err
}
if len(imageList) == 0 {
return nil, fmt.Errorf("ImageId %s does not exist", volumeLabels.ImageId)
}
imageSummery := imageList[0]
imageInspect, _, err := con.dockerClient.ImageInspectWithRaw(context.TODO(), imageSummery.ID)
if err != nil {
return nil, err
} }
imagePorts = make([]models.Port, len(imageInspect.Config.ExposedPorts)) type ImageInfo struct {
i := 0 Name string `json:"Name"`
for imagePort := range imageInspect.Config.ExposedPorts { Version string `json:"Version"`
imagePorts[i] = models.Port{Protocol: imagePort.Proto(), Number: imagePort.Int()} Ports []models.Port `json:"Ports"`
i += 1
}
} else {
imagePorts = serverData.DefaultPorts
} }
imageNameAndVersion := strings.Split(volumeLabels.ImageId, ":") type ServerInfo struct {
Id string `json:"Id"`
imageName := imageNameAndVersion[0] OwnerId string `json:"OwnerId"`
imageVersion := imageNameAndVersion[1] DefaultCommand string `json:"DefaultCommand"`
containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ Image ImageInfo `json:"Image"`
All: true, On bool `json:"On"`
Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("label", fmt.Sprintf("volume_id=%s", volume.Name))), Nickname string `json:"Nickname"`
}) Ports []models.Port `json:"Ports"`
var state bool Domain string `json:"Domain"`
var ports []models.Port
if err != nil || len(containers) == 0 {
state = false
ports = nil
} else {
container := containers[0]
state = container.State == "running"
ports = transformContainerPortsToModel(container.Ports)
} }
serverInfo := models.ServerInfo{ type FileBrowserInfo struct {
Id: volume.Name, Id string `json:"Id"`
Image: models.Image{ OwnerId string `json:"OwnerId"`
Name: imageName, ConnectedTo ServerInfo `json:"ConnectedTo"`
Version: imageVersion, Url string `json:"Url"`
Ports: imagePorts,
},
OwnerId: volumeLabels.OwnerId,
On: state,
Ports: ports,
Nickname: serverData.Nickname,
DefaultCommand: serverData.DefaultCommand,
Domain: DOMAIN,
}
return &serverInfo, nil
}
func (con Connection) getServerInfoFromId(ServerId string) (*models.ServerInfo, error) {
volume, err := con.dockerClient.VolumeInspect(context.TODO(), ServerId)
if err != nil {
return nil, err
}
return con.getServerInfo(volume)
}
func transformContainerPortsToModel(ports []types.Port) []models.Port {
modelPorts := make([]models.Port, len(ports))
for index, port := range ports {
modelPorts[index] = models.Port{
Number: int(port.PublicPort),
Protocol: port.Type,
}
}
return modelPorts
}
func convertLabelsToMap(v any) (map[string]string, error) {
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
x := map[string]string{}
json.Unmarshal(data, &x)
return x, nil
} }
type CreateServerRequest struct { type CreateServerRequest struct {
@ -187,6 +73,8 @@ func (con Connection) CreateServer(ctx *gin.Context) {
ctx.AbortWithStatus(500) ctx.AbortWithStatus(500)
return return
} }
serverClaims := claims.(*auth.AuthClaims)
var request CreateServerRequest var request CreateServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&request) err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil { if err != nil {
@ -194,40 +82,34 @@ func (con Connection) CreateServer(ctx *gin.Context) {
return return
} }
imageList, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", request.ImageId), filters.Arg("label", "type=GAME"))}) instanceServer, err := con.InstanceManager.CreateServer(ctx)
if err != nil { if err != nil {
ctx.AbortWithError(400, err) ctx.AbortWithError(500, err)
return return
} }
if len(imageList) == 0 {
ctx.AbortWithStatusJSON(404, "imageNotFound") imageSegments := strings.Split(request.ImageId, ":")
return
} registry := imageSegments[0]
imageSummary := imageList[0] tag := imageSegments[1]
labels, err := convertLabelsToMap(VolumeLabels{OwnerId: claims.(*auth.AuthClaims).Username, ImageId: imageSummary.RepoTags[0], Type: "GAME"})
if err != nil { err = con.ServersDbHandler.CreateServer(ctx, dbhandler.Server{
ctx.AbortWithError(400, err) Id: instanceServer.Id,
return Owner: serverClaims.Username,
} Image: &models.Image{
volumeResponse, err := con.dockerClient.VolumeCreate(context.TODO(), volume.CreateOptions{ Registry: registry,
Labels: labels, Tag: tag,
},
Nickname: request.Nickname,
Command: request.DefaultCommand,
Ports: request.DefaultPorts,
}) })
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
con.databaseConnection.Database("Backend").Collection("Servers").InsertOne(context.TODO(), models.ServerData{
Id: volumeResponse.Name,
OwnerId: claims.(*auth.AuthClaims).Username,
Image: imageSummary.RepoTags[0],
VolumeId: volumeResponse.Name,
DefaultPorts: request.DefaultPorts,
DefaultCommand: request.DefaultCommand,
Nickname: request.Nickname,
UserPermissions: make(map[string]models.Permission),
})
ctx.JSON(200, volumeResponse.Name) ctx.JSON(200, instanceServer.Id)
} }
type PortMappingRequest struct { type PortMappingRequest struct {
@ -242,221 +124,118 @@ type StartServerRequest struct {
func (con Connection) StartServer(ctx *gin.Context) { func (con Connection) StartServer(ctx *gin.Context) {
serverId := ctx.Param("server_id") serverId := ctx.Param("server_id")
claims, exists := ctx.Get("claims")
var request StartServerRequest var request StartServerRequest
json.NewDecoder(ctx.Request.Body).Decode(&request) err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if !exists {
ctx.AbortWithStatus(403)
return
}
serverInfo, err := con.getServerInfoFromId(serverId)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
if serverInfo.On { instanceServer, err := con.InstanceManager.GetServer(ctx, serverId)
if instanceServer.Running {
ctx.Status(200) ctx.Status(200)
return return
} }
imageId := serverInfo.Image.Name + ":" + serverInfo.Image.Version server, err := con.ServersDbHandler.GetServer(ctx, serverId)
labels := ContainerLabels{
OwnerId: claims.(*auth.AuthClaims).Username,
ImageId: imageId,
VolumeId: serverInfo.Id,
Type: "GAME",
}
jsonString, err := json.Marshal(labels)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return
}
jsonLabels := make(map[string]string)
json.Unmarshal(jsonString, &jsonLabels)
volumes := make(map[string]struct{})
image, _, err := con.dockerClient.ImageInspectWithRaw(context.TODO(), imageId)
if err != nil {
ctx.AbortWithError(500, err)
return
} }
var portMapping nat.PortMap = make(nat.PortMap) err = con.InstanceManager.StartServer(
if len(request.Ports) == 0 { ctx,
for _, port := range serverInfo.Image.Ports { instanceServer.Id,
dockerPort, err := nat.NewPort(port.Protocol, fmt.Sprint(port.Number)) server.Image.Registry+":"+server.Image.Tag,
if err != nil { server.Command,
ctx.AbortWithError(500, err) server.Ports,
return
}
portMapping[dockerPort] = []nat.PortBinding{{HostIP: "0.0.0.0"}}
}
} else {
for _, portCouple := range request.Ports {
portMapping[nat.Port(fmt.Sprintf("%d/%s", portCouple.Source.Number, portCouple.Source.Protocol))] = []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: fmt.Sprint(portCouple.Destination.Number)}}
}
}
command := request.Command
if command == "" {
command = serverInfo.DefaultCommand
}
words, err := shellwords.Split(command)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if len(words) == 0 {
words = nil
}
portSet := make(nat.PortSet)
for port := range portMapping {
portSet[port] = struct{}{}
}
response, err := con.dockerClient.ContainerCreate(
context.TODO(),
&container.Config{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
OpenStdin: true,
StdinOnce: false,
Image: imageId,
Volumes: volumes,
Labels: jsonLabels,
Cmd: words,
ExposedPorts: portSet,
},
&container.HostConfig{
AutoRemove: true,
Mounts: []mount.Mount{{Source: serverInfo.Id, Target: image.Config.WorkingDir, Type: "volume"}},
PortBindings: portMapping,
ConsoleSize: [2]uint{1000, 1000},
},
&network.NetworkingConfig{},
&v1.Platform{},
"",
) )
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
if err := con.dockerClient.ContainerStart(ctx, response.ID, container.StartOptions{}); err != nil {
ctx.AbortWithError(500, err)
return
}
UPNPPath, exists := os.LookupEnv("UPNP_PATH")
HostIP, hostIPexists := os.LookupEnv("HOST_IP")
if exists && hostIPexists { ctx.JSON(200, instanceServer.Id)
time.Sleep(time.Millisecond * 100)
containerData, err := con.dockerClient.ContainerInspect(context.TODO(), response.ID)
if err != nil {
ctx.AbortWithError(500, err)
return
}
fo, err := os.OpenFile(UPNPPath, os.O_WRONLY, os.ModeAppend)
if err != nil {
ctx.AbortWithError(503, err)
con.dockerClient.ContainerStop(context.TODO(), containerData.ID, container.StopOptions{})
return
}
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
for containerPort, portBindings := range containerData.NetworkSettings.Ports {
for _, hostPort := range portBindings {
number, proto := hostPort.HostPort, containerPort.Proto()
UPNPCommand := fmt.Sprintf("%s|%s|%s|%s\n", HostIP, number, number, strings.ToUpper(proto))
_, err := fo.Write([]byte(UPNPCommand))
if err != nil {
ctx.AbortWithError(500, err)
return
}
log.Printf("Wrote command \"%s\" to %s", UPNPCommand, UPNPPath)
}
}
}
ctx.JSON(200, response.ID)
} }
func (con Connection) GetServers(ctx *gin.Context) { func (con Connection) GetServers(ctx *gin.Context) {
volumes, err := con.dockerClient.VolumeList( instanceServers, err := con.InstanceManager.ListServers(ctx)
context.TODO(),
volume.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", "type=GAME")),
},
)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
} return
var servers []models.ServerInfo = make([]models.ServerInfo, 0, len(volumes.Volumes))
for _, volume := range volumes.Volumes {
serverInfo, err := con.getServerInfo(*volume)
if err != nil {
log.Printf("failed to get server info: %s, %s", volume.Name, err)
continue
}
servers = append(servers, *serverInfo)
} }
serverConfigs, err := con.ServersDbHandler.ListServers(ctx)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return
} }
serverConfigsMap := make(map[string]dbhandler.Server, len(serverConfigs))
for _, serverConfig := range serverConfigs {
serverConfigsMap[serverConfig.Id] = serverConfig
}
servers := make([]ServerInfo, len(instanceServers))
for i, instanceServer := range instanceServers {
server := serverConfigsMap[instanceServer.Id]
var image ImageInfo
if instanceServer.Running {
image = ImageInfo{
Name: instanceServer.RunningImage.Registry,
Version: instanceServer.RunningImage.Tag,
}
} else {
image = ImageInfo{
Name: server.Image.Registry,
Version: server.Image.Tag,
}
}
servers[i] = ServerInfo{
Id: instanceServer.Id,
Image: image,
OwnerId: server.Owner,
DefaultCommand: server.Command,
Ports: instanceServer.Ports,
On: instanceServer.Running,
Nickname: server.Nickname,
Domain: instanceServer.Domain,
}
}
ctx.JSON(200, servers) ctx.JSON(200, servers)
} }
func (con Connection) StopServer(ctx *gin.Context) { func (con Connection) StopServer(ctx *gin.Context) {
serverId := ctx.Param("server_id") serverId := ctx.Param("server_id")
containersList, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ err := con.InstanceManager.StopServer(ctx, serverId)
Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME")),
})
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
if len(containersList) == 0 {
ctx.Status(200)
return
}
for _, containerData := range containersList {
con.dockerClient.ContainerStop(context.TODO(), containerData.ID, container.StopOptions{})
}
ctx.Status(200) ctx.Status(200)
} }
func (con Connection) DeleteServer(ctx *gin.Context) { func (con Connection) DeleteServer(ctx *gin.Context) {
serverId := ctx.Param("server_id") serverId := ctx.Param("server_id")
containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{All: true, Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId))})
err := con.InstanceManager.DeleteServer(ctx, serverId)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
for _, containerInstance := range containers { err = con.ServersDbHandler.DeleteServer(ctx, serverId)
err := con.dockerClient.ContainerRemove(context.TODO(), containerInstance.ID, container.RemoveOptions{Force: true})
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(501, err)
return return
} }
}
con.dockerClient.VolumeRemove(context.TODO(), serverId, false)
con.databaseConnection.Database("Backend").Collection("Servers").FindOneAndDelete(context.TODO(), bson.D{{Key: "Id", Value: serverId}})
ctx.JSON(200, "ok") ctx.JSON(200, "ok")
} }
@ -474,26 +253,21 @@ func (con Connection) RunCommand(ctx *gin.Context) {
serverId := ctx.Param("server_id") serverId := ctx.Param("server_id")
log.Print("Writing command \"", request.Command, "\"") log.Print("Writing command \"", request.Command, "\"")
containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME"))})
consolePointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
for _, containerData := range containers { console := *consolePointer
hijacked, err := con.dockerClient.ContainerAttach(context.TODO(), containerData.ID, container.AttachOptions{Stream: true, Stdin: true}) defer console.Close()
_, err = console.Write([]byte(request.Command + "\n"))
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
defer func() { hijacked.Close(); hijacked.CloseWrite() }()
number, err := hijacked.Conn.Write([]byte(request.Command + "\n"))
log.Print("Wrote ", number, " bytes")
if err != nil {
ctx.AbortWithError(500, err)
return
}
}
ctx.JSON(200, "OK") ctx.JSON(200, "OK")
} }
@ -519,20 +293,12 @@ func (con Connection) AttachServer(ctx *gin.Context) {
close(containerRead) close(containerRead)
}() }()
containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME"))}) hijackedPointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if len(containers) == 0 {
ctx.AbortWithStatus(404)
return
}
hijacked, err := con.dockerClient.ContainerAttach(context.TODO(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true, Logs: true})
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
hijacked := *hijackedPointer
defer hijacked.Close() defer hijacked.Close()
ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
@ -548,10 +314,15 @@ func (con Connection) AttachServer(ctx *gin.Context) {
if stop { if stop {
break break
} }
count, err := hijacked.Reader.Read(data) count, err := hijacked.Read(data)
if err != nil { if err != nil {
time.Sleep(500) time.Sleep(500)
hijacked, err = con.dockerClient.ContainerAttach(context.TODO(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true, Logs: false}) hijackedPointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
hijacked = *hijackedPointer
if err != nil { if err != nil {
stop = true stop = true
break break
@ -591,7 +362,7 @@ func (con Connection) AttachServer(ctx *gin.Context) {
case Command := <-websocketRead: case Command := <-websocketRead:
switch Command.CommandType { switch Command.CommandType {
case "insert": case "insert":
_, err = hijacked.Conn.Write([]byte(Command.Arguments)) _, err = hijacked.Write([]byte(Command.Arguments))
if err != nil { if err != nil {
log.Printf("Write to docker failed %s", errors.Unwrap(err)) log.Printf("Write to docker failed %s", errors.Unwrap(err))
@ -616,16 +387,10 @@ func (con Connection) AttachServer(ctx *gin.Context) {
} }
height := uint(i_height) height := uint(i_height)
err2 = con.dockerClient.ContainerResize(context.TODO(), containers[0].ID, container.ResizeOptions{Height: height, Width: width}) err2 = con.InstanceManager.ResizeTerminal(ctx, serverId, width, height)
if err2 != nil { if err2 != nil {
log.Printf("Failed to resize container to %dx%d: %s", width, height, err) log.Printf("Failed to resize container to %dx%d: %s", width, height, err)
} }
// hijacked, err2 = con.dockerClient.ContainerAttach(context.TODO(), containers[0].ID, container.AttachOptions{Stream: true, Stdin: true, Stdout: true, Stderr: true})
// if err2 != nil {
// log.Printf("Failed to reattach container %s", err)
// }
} }
case data := <-containerRead: case data := <-containerRead:
err := ws.WriteJSON(data) err := ws.WriteJSON(data)
@ -641,33 +406,37 @@ type UpdateServerRequest struct {
DefaultPorts []models.Port `json:"DefaultPorts"` DefaultPorts []models.Port `json:"DefaultPorts"`
DefaultCommand string `json:"DefaultCommand"` DefaultCommand string `json:"DefaultCommand"`
Nickname string `json:"Nickname"` Nickname string `json:"Nickname"`
UserPermissions map[string][]models.Permission `json:"UserPermissions"` UserPermissions map[string]models.Permission `json:"UserPermissions"`
} }
func (con Connection) UpdateServer(ctx *gin.Context) { func (con Connection) UpdateServer(ctx *gin.Context) {
serverId := ctx.Param("server_id") serverId := ctx.Param("server_id")
var Request UpdateServerRequest var request UpdateServerRequest
err := json.NewDecoder(ctx.Request.Body).Decode(&Request) err := json.NewDecoder(ctx.Request.Body).Decode(&request)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
updateOperation := bson.M{} err = con.ServersDbHandler.UpdateServer(ctx, serverId, dbhandler.ServerUpdateRequest{
if Request.DefaultCommand != "" { Ports: request.DefaultPorts,
updateOperation["DefaultCommand"] = Request.DefaultCommand Nickname: request.Nickname,
} Command: request.DefaultCommand,
if Request.DefaultPorts != nil { })
updateOperation["DefaultPorts"] = Request.DefaultPorts
}
if Request.Nickname != "" {
updateOperation["Nickname"] = Request.Nickname
}
_, err = con.databaseConnection.Database("Backend").Collection("Servers").UpdateOne(context.TODO(), bson.D{{Key: "Id", Value: serverId}}, bson.D{{Key: "$set", Value: updateOperation}})
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return
}
for user, permissions := range request.UserPermissions {
err = con.ServerAuthorization.SetPermissions(ctx, user, serverId, permissions)
if err != nil {
log.Printf("failed to change user %s permissions for server %s due to %e", user, serverId, err)
continue
}
} }
ctx.JSON(200, "OK") ctx.JSON(200, "OK")
@ -675,115 +444,13 @@ func (con Connection) UpdateServer(ctx *gin.Context) {
} }
func (con Connection) BrowseServer(ctx *gin.Context) { func (con Connection) BrowseServer(ctx *gin.Context) {
serverID := ctx.Param("server_id") serverId := ctx.Param("server_id")
claims, exists := ctx.Get("claims") browserInfo, err := con.InstanceManager.StartFileBrowser(ctx, serverId)
if !exists {
ctx.AbortWithStatus(403)
return
}
serverInfo, err := con.getServerInfoFromId(serverID)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
} }
labelId := serverInfo.Id[: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, DOMAIN)
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", DOMAIN)
browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.domains[0].sans", labelId)] = fmt.Sprintf("*.%s.%s", "browsers", DOMAIN)
browserLabels[fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", labelId)] = "myresolver"
containerLabels := ContainerLabels{
OwnerId: claims.(*auth.AuthClaims).Username,
ImageId: FILE_BROWSER_IMAGE,
VolumeId: serverInfo.Id,
Type: "FILE_BROWSER",
}
jsonLabels, err := json.Marshal(containerLabels)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = json.Unmarshal(jsonLabels, &browserLabels)
if err != nil {
ctx.AbortWithError(500, err)
return
}
command, err := shellwords.Split("--noauth -r /tmp/data")
if err != nil {
ctx.AbortWithError(500, err)
return
}
imageRef := fmt.Sprintf("%s:%s", serverInfo.Image.Name, serverInfo.Image.Version)
images, err := con.dockerClient.ImageList(context.TODO(), image.ListOptions{Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("reference", imageRef))})
if err != nil {
ctx.AbortWithError(500, err)
return
}
if len(images) == 0 {
ctx.AbortWithError(500, fmt.Errorf("image %s no longer exists", imageRef))
return
}
browserInfo, err := con.getBrowserInfoFromServerId(serverInfo.Id)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if browserInfo != nil {
ctx.JSON(200, browserInfo.Url)
return
}
ContainerResponse, err := con.dockerClient.ContainerCreate(
context.TODO(),
&container.Config{
Cmd: command,
Image: FILE_BROWSER_IMAGE,
Labels: browserLabels,
Tty: true,
},
&container.HostConfig{
Mounts: []mount.Mount{{Source: serverInfo.Id, Target: "/tmp/data", Type: "volume"}},
AutoRemove: true,
ConsoleSize: [2]uint{1000, 1000},
},
&network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{"test": {
NetworkID: "exposed",
}},
},
&v1.Platform{},
"",
)
if err != nil {
ctx.AbortWithError(500, err)
return
}
err = con.dockerClient.ContainerStart(context.TODO(), ContainerResponse.ID, container.StartOptions{})
if err != nil {
ctx.AbortWithError(500, err)
return
}
browserInfo, err = con.getBrowserInfoFromServerId(serverInfo.Id)
if err != nil {
ctx.AbortWithError(500, err)
return
}
if browserInfo == nil {
ctx.AbortWithError(500, fmt.Errorf("failed to open browser for server %s", serverInfo.Id))
return
}
ctx.JSON(200, browserInfo.Url) ctx.JSON(200, browserInfo.Url)
} }
@ -793,16 +460,20 @@ func (con Connection) GetServerUserPermissions(ctx *gin.Context) {
ctx.AbortWithStatus(500) ctx.AbortWithStatus(500)
return return
} }
authClaims := claims.(*auth.AuthClaims)
server_id := ctx.Param("server_id") serverId := ctx.Param("server_id")
if server_id == "" { if serverId == "" {
ctx.AbortWithStatus(500) ctx.AbortWithStatus(500)
return return
} }
permissions, err := con.ServerAuthorization.GetPermissions(ctx, authClaims.Username, serverId)
if err != nil {
ctx.AbortWithError(500, err)
return
}
var serverData models.ServerData ctx.JSON(200, permissions)
con.databaseConnection.Database("Backend").Collection("Servers").FindOne(context.TODO(), bson.D{{Key: "Id", Value: server_id}}).Decode(&serverData)
ctx.JSON(200, serverData.UserPermissions[claims.(*auth.AuthClaims).Username])
} }
type SetServerUserPermissionsRequest struct { type SetServerUserPermissionsRequest struct {
@ -811,8 +482,8 @@ type SetServerUserPermissionsRequest struct {
} }
func (con Connection) SetServerUserPermissions(ctx *gin.Context) { func (con Connection) SetServerUserPermissions(ctx *gin.Context) {
server_id := ctx.Param("server_id") serverId := ctx.Param("server_id")
if server_id == "" { if serverId == "" {
ctx.AbortWithStatus(500) ctx.AbortWithStatus(500)
return return
} }
@ -824,26 +495,52 @@ func (con Connection) SetServerUserPermissions(ctx *gin.Context) {
return return
} }
_, err = con.databaseConnection.Database("Backend").Collection("Servers").UpdateOne(context.TODO(), bson.D{{Key: "Id", Value: server_id}}, bson.D{{Key: "$set", Value: bson.D{{Key: fmt.Sprintf("UserPermissions.%s", request.Username), Value: request.Permissions}}}}) err = con.ServerAuthorization.SetPermissions(ctx, request.Username, serverId, request.Permissions)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
ctx.JSON(200, "OK") ctx.JSON(200, "OK")
} }
func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client, config models.GlobalConfig) { func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) {
apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
var instanceManager instancemanager.InstanceManager
var serversDbHandler dbhandler.ServersDbHandler
var serversAuthorizationHandler dbhandler.ServersAuthorizationDbHandler
var err error
if config.InstanceManager.Type == models.DOCKER {
instanceManager, err = docker.NewInstanceManager(config.InstanceManager.Docker)
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer apiClient.Close() }
DOMAIN = config.Domain if config.ServersDatabase.Type == models.MONGO {
serversDbHandler, err = mongo.NewServersDbHandler(*config.ServersDatabase.Mongo)
if err != nil {
panic(err)
}
}
connection := Connection{databaseConnection: mongo_client, dockerClient: apiClient} if config.ServersAuthorizationDatabase.Type == models.MONGO {
authConnection := auth.Connection{DatabaseConnection: mongo_client} serversAuthorizationHandler, err = mongo.NewAuthorizationHandler(*config.ServersAuthorizationDatabase.Mongo)
if err != nil {
panic(err)
}
}
connection := Connection{
ServersDbHandler: serversDbHandler,
ServerAuthorization: serversAuthorizationHandler,
InstanceManager: instanceManager,
}
authConnection := auth.Connection{}
group.POST("/:server_id/start", auth.AuthorizedTo(models.Start, authConnection.ServerAuthorized(models.Start)), connection.StartServer) group.POST("/:server_id/start", auth.AuthorizedTo(models.Start, authConnection.ServerAuthorized(models.Start)), connection.StartServer)
group.POST("", auth.AuthorizedTo(models.Create), connection.CreateServer) group.POST("", auth.AuthorizedTo(models.Create), connection.CreateServer)