backend/instance_manager/docker/instance_manager.go

585 lines
14 KiB
Go

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