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