From f57888cb8e4ffb1be1f039b158e0929138990cd1 Mon Sep 17 00:00:00 2001 From: ACoolName Date: Sun, 16 Mar 2025 19:23:02 +0200 Subject: [PATCH] added basic servers implementation --- db_handler/servers.go | 16 +- db_handler/servers_authorization.go | 18 +- instance_manager/docker/instance_manager.go | 52 +- instance_manager/docker/utils.go | 21 +- instance_manager/instance_manager.go | 13 +- main.go | 13 - models/config.go | 4 +- servers/servers.go | 695 ++++++-------------- 8 files changed, 280 insertions(+), 552 deletions(-) diff --git a/db_handler/servers.go b/db_handler/servers.go index 4d8f690..3ffa2ec 100644 --- a/db_handler/servers.go +++ b/db_handler/servers.go @@ -1,6 +1,10 @@ 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 { Id string @@ -20,9 +24,9 @@ type ServerUpdateRequest struct { } type ServersDbHandler interface { - ListServers() ([]Server, error) - GetServer(serverId string) (*Server, error) - CreateServer(server Server) error - DeleteServer(serverId string) error - UpdateServer(serverId string, updateParams ServerUpdateRequest) error + ListServers(ctx context.Context) ([]Server, error) + GetServer(ctx context.Context, serverId string) (*Server, error) + CreateServer(ctx context.Context, server Server) error + DeleteServer(ctx context.Context, serverId string) error + UpdateServer(ctx context.Context, serverId string, updateParams ServerUpdateRequest) error } diff --git a/db_handler/servers_authorization.go b/db_handler/servers_authorization.go index 5b7840b..7982793 100644 --- a/db_handler/servers_authorization.go +++ b/db_handler/servers_authorization.go @@ -1,12 +1,16 @@ 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 { - AddPermissions(username string, server_id string, permissions models.Permission) error - RemovePermissions(username string, server_id string, permissions models.Permission) error - SetPermissions(username string, server_id string, permissions models.Permission) error - GetPermissions(username string, server_id string) (models.Permission, error) - RemoveUser(username string) error - RemoveServer(server_id string) error + AddPermissions(ctx context.Context, username string, server_id string, permissions models.Permission) error + RemovePermissions(ctx context.Context, string, server_id string, permissions models.Permission) error + SetPermissions(ctx context.Context, username string, server_id string, permissions models.Permission) error + GetPermissions(ctx context.Context, username string, server_id string) (models.Permission, error) + RemoveUser(ctx context.Context, username string) error + RemoveServer(ctx context.Context, server_id string) error } diff --git a/instance_manager/docker/instance_manager.go b/instance_manager/docker/instance_manager.go index 30ae2b5..2d70267 100644 --- a/instance_manager/docker/instance_manager.go +++ b/instance_manager/docker/instance_manager.go @@ -4,10 +4,7 @@ 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" @@ -63,6 +60,22 @@ func (self *InstanceManager) getVolume(ctx context.Context, serverId string) (*v // 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 { @@ -81,14 +94,7 @@ func (self *InstanceManager) ListImages(ctx context.Context) ([]instancemanager. return nil, err } - ports := convertImagePortsToPorts(imageInspect.Config.ExposedPorts) - imageId := convertContainerImageToImage(rawImage.RepoTags[0]) - images[i] = instancemanager.Image{ - Registry: imageId.Registry, - Tag: imageId.Tag, - Ports: ports, - Command: strings.Join(imageInspect.Config.Cmd, " "), - } + images[i] = convertImageInspectToInstanceImage(imageInspect) } return images, nil @@ -117,6 +123,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i RunningCommand: "", RunningImage: nil, Ports: nil, + Domain: self.config.GamesDomain, }, nil } @@ -125,7 +132,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i running := serverContainer.State == "running" runningCommand := serverContainer.Command - image := convertContainerImageToImage(serverContainer.Image) + image := convertImageStringToModelsImage(serverContainer.Image) return &instancemanager.Server{ Id: volume.Name, @@ -133,6 +140,7 @@ func (self *InstanceManager) GetServer(ctx context.Context, serverId string) (*i RunningCommand: runningCommand, Ports: convertContainerPortsToPorts(serverContainer.Ports), RunningImage: &image, + Domain: self.config.GamesDomain, }, nil } @@ -156,6 +164,7 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager RunningCommand: "", Ports: nil, RunningImage: nil, + Domain: self.config.GamesDomain, } } @@ -177,7 +186,7 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager continue } - image := convertContainerImageToImage(container.Image) + image := convertImageStringToModelsImage(container.Image) serverStatus[containerLabels.VolumeId].Ports = convertContainerPortsToPorts(container.Ports) serverStatus[containerLabels.VolumeId].Running = true @@ -197,7 +206,12 @@ func (self *InstanceManager) ListServers(ctx context.Context) ([]instancemanager } // 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) if err != nil { return err @@ -207,7 +221,6 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i return fmt.Errorf("Server %s already running", serverId) } - imageId := image.Registry + ":" + image.Tag containerLabels := ContainerLabels{ VolumeId: server.Id, Type: "GAME", @@ -216,6 +229,10 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i 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 { @@ -232,8 +249,6 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i } } - rawImage, _, err := self.client.ImageInspectWithRaw(ctx, imageId) - if command == "" { command = image.Command } @@ -274,7 +289,7 @@ func (self *InstanceManager) StartServer(ctx context.Context, serverId string, i }, &container.HostConfig{ 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, ConsoleSize: [2]uint{1000, 1000}, }, @@ -328,6 +343,7 @@ func (self *InstanceManager) CreateServer(ctx context.Context) (*instancemanager Running: false, RunningImage: nil, RunningCommand: "", + Domain: self.config.GamesDomain, }, nil } diff --git a/instance_manager/docker/utils.go b/instance_manager/docker/utils.go index 50046b5..64c83f0 100644 --- a/instance_manager/docker/utils.go +++ b/instance_manager/docker/utils.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "log" - "reflect" "strings" 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 { return nil, err } + for key, value := range *labelMap { 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) { - raw, err := json.Marshal(labels) if err != nil { return nil, err @@ -80,7 +79,7 @@ func convertContainerPortsToPorts(ports []types.Port) []models.Port { return containerPorts } -func convertContainerImageToImage(image string) models.Image { +func convertImageStringToModelsImage(image string) models.Image { imageSegments := strings.Split(image, ":") imageRegistry := imageSegments[0] 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) { var containerLabels ContainerLabels @@ -104,6 +117,7 @@ func convertContainerLabelsToStruct(labels map[string]string) (*ContainerLabels, if err != nil { return nil, err } + return &containerLabels, nil } @@ -120,6 +134,7 @@ func convertVolumeLabelsToStruct(labels map[string]string) (*VolumeLabels, error if err != nil { return nil, err } + return &volumeLabels, nil } diff --git a/instance_manager/instance_manager.go b/instance_manager/instance_manager.go index b9bebfd..a52400e 100644 --- a/instance_manager/instance_manager.go +++ b/instance_manager/instance_manager.go @@ -13,6 +13,7 @@ type Server struct { RunningCommand string RunningImage *models.Image Ports []models.Port + Domain string } type Port struct { @@ -21,21 +22,23 @@ type Port struct { } type Image struct { - Registry string - Tag string - Command string - Ports []Port + 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, 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 CreateServer(ctx context.Context) (*Server, error) diff --git a/main.go b/main.go index 42d209a..c41a72e 100644 --- a/main.go +++ b/main.go @@ -18,14 +18,6 @@ import ( const CONFIG_SECRET_NAME = "CONFIG_PATH" -func get_servers_db_handler(config models.ServersDatabaseConfig) { - -} - -func get_users_db_handler(config models.UsersDatabaseConfig) { - -} - func main() { router := gin.Default() @@ -49,11 +41,6 @@ func main() { panic(err) } - switch config.UserDatabase.Mongo { - case nil: - default: - } - client, err := dbhandler.Connect(config.UsersDatabase.Mongo) defer func() { if err = client.Disconnect(context.TODO()); err != nil { diff --git a/models/config.go b/models/config.go index 35fe971..d54a60a 100644 --- a/models/config.go +++ b/models/config.go @@ -49,13 +49,15 @@ type FileBrowserConfig struct { } type DockerInstanceManagerConfig struct { + GamesDomain string `yaml:"games_domain"` BrowsersDomain string `yaml:"browsers_domain"` CertificateResolver string `yaml:"certificate_resolver"` FileBrowser FileBrowserConfig `yaml:"file_browser"` } type InstanceManagerConfig struct { - Type InstanceManagerType + Type InstanceManagerType `yaml:"type"` + Docker DockerInstanceManagerConfig `yaml:"docker"` } type ServersDatabaseConfig struct { diff --git a/servers/servers.go b/servers/servers.go index 7934c20..83dd58d 100644 --- a/servers/servers.go +++ b/servers/servers.go @@ -7,56 +7,21 @@ import ( "fmt" "log" "net/http" - "os" "strconv" "strings" "time" "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" - "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/gorilla/websocket" 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{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -65,113 +30,34 @@ var upgrader = websocket.Upgrader{ }, } -func (con Connection) getServerInfo(volume volume.Volume) (*models.ServerInfo, error) { - var volumeLabels VolumeLabels - var serverData models.ServerData - - 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)) - i := 0 - for imagePort := range imageInspect.Config.ExposedPorts { - imagePorts[i] = models.Port{Protocol: imagePort.Proto(), Number: imagePort.Int()} - i += 1 - } - } else { - imagePorts = serverData.DefaultPorts - } - - imageNameAndVersion := strings.Split(volumeLabels.ImageId, ":") - - imageName := imageNameAndVersion[0] - imageVersion := imageNameAndVersion[1] - containers, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ - All: true, - Filters: filters.NewArgs(filters.Arg("label", "type=GAME"), filters.Arg("label", fmt.Sprintf("volume_id=%s", volume.Name))), - }) - var state bool - 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{ - Id: volume.Name, - Image: models.Image{ - Name: imageName, - Version: imageVersion, - Ports: imagePorts, - }, - OwnerId: volumeLabels.OwnerId, - On: state, - Ports: ports, - Nickname: serverData.Nickname, - DefaultCommand: serverData.DefaultCommand, - Domain: DOMAIN, - } - return &serverInfo, nil - +type Connection struct { + ServersDbHandler dbhandler.ServersDbHandler + InstanceManager instancemanager.InstanceManager + ServerAuthorization dbhandler.ServersAuthorizationDbHandler } -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) +type ImageInfo struct { + Name string `json:"Name"` + Version string `json:"Version"` + Ports []models.Port `json:"Ports"` } -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 +type ServerInfo struct { + Id string `json:"Id"` + OwnerId string `json:"OwnerId"` + DefaultCommand string `json:"DefaultCommand"` + Image ImageInfo `json:"Image"` + On bool `json:"On"` + Nickname string `json:"Nickname"` + Ports []models.Port `json:"Ports"` + Domain string `json:"Domain"` } -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 FileBrowserInfo struct { + Id string `json:"Id"` + OwnerId string `json:"OwnerId"` + ConnectedTo ServerInfo `json:"ConnectedTo"` + Url string `json:"Url"` } type CreateServerRequest struct { @@ -187,6 +73,8 @@ func (con Connection) CreateServer(ctx *gin.Context) { ctx.AbortWithStatus(500) return } + serverClaims := claims.(*auth.AuthClaims) + var request CreateServerRequest err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { @@ -194,40 +82,34 @@ func (con Connection) CreateServer(ctx *gin.Context) { 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 { - ctx.AbortWithError(400, err) + ctx.AbortWithError(500, err) return } - if len(imageList) == 0 { - ctx.AbortWithStatusJSON(404, "imageNotFound") - return - } - imageSummary := imageList[0] - labels, err := convertLabelsToMap(VolumeLabels{OwnerId: claims.(*auth.AuthClaims).Username, ImageId: imageSummary.RepoTags[0], Type: "GAME"}) - if err != nil { - ctx.AbortWithError(400, err) - return - } - volumeResponse, err := con.dockerClient.VolumeCreate(context.TODO(), volume.CreateOptions{ - Labels: labels, + + imageSegments := strings.Split(request.ImageId, ":") + + registry := imageSegments[0] + tag := imageSegments[1] + + err = con.ServersDbHandler.CreateServer(ctx, dbhandler.Server{ + Id: instanceServer.Id, + Owner: serverClaims.Username, + Image: &models.Image{ + Registry: registry, + Tag: tag, + }, + Nickname: request.Nickname, + Command: request.DefaultCommand, + Ports: request.DefaultPorts, }) if err != nil { ctx.AbortWithError(500, err) 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 { @@ -242,222 +124,119 @@ type StartServerRequest struct { func (con Connection) StartServer(ctx *gin.Context) { serverId := ctx.Param("server_id") - claims, exists := ctx.Get("claims") + var request StartServerRequest - json.NewDecoder(ctx.Request.Body).Decode(&request) - - if !exists { - ctx.AbortWithStatus(403) - return - } - - serverInfo, err := con.getServerInfoFromId(serverId) + err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { ctx.AbortWithError(500, err) return } - if serverInfo.On { + instanceServer, err := con.InstanceManager.GetServer(ctx, serverId) + if instanceServer.Running { ctx.Status(200) return } - imageId := serverInfo.Image.Name + ":" + serverInfo.Image.Version - labels := ContainerLabels{ - OwnerId: claims.(*auth.AuthClaims).Username, - ImageId: imageId, - VolumeId: serverInfo.Id, - Type: "GAME", - } - - jsonString, err := json.Marshal(labels) + server, err := con.ServersDbHandler.GetServer(ctx, serverId) if err != nil { 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) - if len(request.Ports) == 0 { - for _, port := range serverInfo.Image.Ports { - dockerPort, err := nat.NewPort(port.Protocol, fmt.Sprint(port.Number)) - if err != nil { - ctx.AbortWithError(500, err) - 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{}, - "", + err = con.InstanceManager.StartServer( + ctx, + instanceServer.Id, + server.Image.Registry+":"+server.Image.Tag, + server.Command, + server.Ports, ) if err != nil { ctx.AbortWithError(500, err) 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 { - 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) + ctx.JSON(200, instanceServer.Id) } func (con Connection) GetServers(ctx *gin.Context) { - volumes, err := con.dockerClient.VolumeList( - context.TODO(), - volume.ListOptions{ - Filters: filters.NewArgs(filters.Arg("label", "type=GAME")), - }, - ) + instanceServers, err := con.InstanceManager.ListServers(ctx) if err != nil { ctx.AbortWithError(500, err) - } - 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) + return } + serverConfigs, err := con.ServersDbHandler.ListServers(ctx) if err != nil { 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) } func (con Connection) StopServer(ctx *gin.Context) { serverId := ctx.Param("server_id") - containersList, err := con.dockerClient.ContainerList(context.TODO(), container.ListOptions{ - Filters: filters.NewArgs(filters.Arg("label", "volume_id="+serverId), filters.Arg("label", "type=GAME")), - }) + err := con.InstanceManager.StopServer(ctx, serverId) if err != nil { ctx.AbortWithError(500, err) 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) } func (con Connection) DeleteServer(ctx *gin.Context) { 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 { ctx.AbortWithError(500, err) return } - for _, containerInstance := range containers { - err := con.dockerClient.ContainerRemove(context.TODO(), containerInstance.ID, container.RemoveOptions{Force: true}) - if err != nil { - ctx.AbortWithError(500, err) - return - } + err = con.ServersDbHandler.DeleteServer(ctx, serverId) + if err != nil { + ctx.AbortWithError(501, err) + 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") } @@ -474,25 +253,20 @@ func (con Connection) RunCommand(ctx *gin.Context) { serverId := ctx.Param("server_id") 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 { ctx.AbortWithError(500, err) return } - for _, containerData := range containers { - hijacked, err := con.dockerClient.ContainerAttach(context.TODO(), containerData.ID, container.AttachOptions{Stream: true, Stdin: true}) - if err != nil { - ctx.AbortWithError(500, err) - 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 - } + console := *consolePointer + defer console.Close() + + _, err = console.Write([]byte(request.Command + "\n")) + if err != nil { + ctx.AbortWithError(500, err) + return } ctx.JSON(200, "OK") @@ -519,20 +293,12 @@ func (con Connection) AttachServer(ctx *gin.Context) { 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"))}) - 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}) + hijackedPointer, err := con.InstanceManager.InteractiveTerminal(ctx, serverId) if err != nil { ctx.AbortWithError(500, err) return } + hijacked := *hijackedPointer defer hijacked.Close() ws, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) @@ -548,10 +314,15 @@ func (con Connection) AttachServer(ctx *gin.Context) { if stop { break } - count, err := hijacked.Reader.Read(data) + count, err := hijacked.Read(data) if err != nil { 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 { stop = true break @@ -591,7 +362,7 @@ func (con Connection) AttachServer(ctx *gin.Context) { case Command := <-websocketRead: switch Command.CommandType { case "insert": - _, err = hijacked.Conn.Write([]byte(Command.Arguments)) + _, err = hijacked.Write([]byte(Command.Arguments)) if err != nil { 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) - 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 { 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: err := ws.WriteJSON(data) @@ -638,36 +403,40 @@ func (con Connection) AttachServer(ctx *gin.Context) { } type UpdateServerRequest struct { - DefaultPorts []models.Port `json:"DefaultPorts"` - DefaultCommand string `json:"DefaultCommand"` - Nickname string `json:"Nickname"` - UserPermissions map[string][]models.Permission `json:"UserPermissions"` + DefaultPorts []models.Port `json:"DefaultPorts"` + DefaultCommand string `json:"DefaultCommand"` + Nickname string `json:"Nickname"` + UserPermissions map[string]models.Permission `json:"UserPermissions"` } func (con Connection) UpdateServer(ctx *gin.Context) { serverId := ctx.Param("server_id") - var Request UpdateServerRequest - err := json.NewDecoder(ctx.Request.Body).Decode(&Request) + var request UpdateServerRequest + err := json.NewDecoder(ctx.Request.Body).Decode(&request) if err != nil { ctx.AbortWithError(500, err) return } - updateOperation := bson.M{} - if Request.DefaultCommand != "" { - updateOperation["DefaultCommand"] = Request.DefaultCommand - } - if Request.DefaultPorts != nil { - updateOperation["DefaultPorts"] = Request.DefaultPorts - } - if Request.Nickname != "" { - updateOperation["Nickname"] = Request.Nickname - } + err = con.ServersDbHandler.UpdateServer(ctx, serverId, dbhandler.ServerUpdateRequest{ + Ports: request.DefaultPorts, + Nickname: request.Nickname, + Command: request.DefaultCommand, + }) - _, 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 { 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") @@ -675,115 +444,13 @@ func (con Connection) UpdateServer(ctx *gin.Context) { } func (con Connection) BrowseServer(ctx *gin.Context) { - serverID := ctx.Param("server_id") - claims, exists := ctx.Get("claims") - if !exists { - ctx.AbortWithStatus(403) - return - } - serverInfo, err := con.getServerInfoFromId(serverID) + serverId := ctx.Param("server_id") + browserInfo, err := con.InstanceManager.StartFileBrowser(ctx, serverId) if err != nil { 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) } @@ -793,16 +460,20 @@ func (con Connection) GetServerUserPermissions(ctx *gin.Context) { ctx.AbortWithStatus(500) return } + authClaims := claims.(*auth.AuthClaims) - server_id := ctx.Param("server_id") - if server_id == "" { + serverId := ctx.Param("server_id") + if serverId == "" { ctx.AbortWithStatus(500) return } + permissions, err := con.ServerAuthorization.GetPermissions(ctx, authClaims.Username, serverId) + if err != nil { + ctx.AbortWithError(500, err) + return + } - var serverData models.ServerData - 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]) + ctx.JSON(200, permissions) } type SetServerUserPermissionsRequest struct { @@ -811,8 +482,8 @@ type SetServerUserPermissionsRequest struct { } func (con Connection) SetServerUserPermissions(ctx *gin.Context) { - server_id := ctx.Param("server_id") - if server_id == "" { + serverId := ctx.Param("server_id") + if serverId == "" { ctx.AbortWithStatus(500) return } @@ -824,26 +495,52 @@ func (con Connection) SetServerUserPermissions(ctx *gin.Context) { 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 { ctx.AbortWithError(500, err) return } + ctx.JSON(200, "OK") } -func LoadGroup(group *gin.RouterGroup, mongo_client *mongo.Client, config models.GlobalConfig) { - apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - panic(err) +func LoadGroup(group *gin.RouterGroup, config models.GlobalConfig) { + + 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 { + 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} - authConnection := auth.Connection{DatabaseConnection: mongo_client} + if config.ServersAuthorizationDatabase.Type == models.MONGO { + 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("", auth.AuthorizedTo(models.Create), connection.CreateServer)