Docker auto-scale and asleep motd status (#488)

This commit is contained in:
Lenart Kos
2025-12-20 20:31:34 +01:00
committed by GitHub
parent b67d0985dc
commit 4dff00dda9
18 changed files with 885 additions and 165 deletions
+195 -46
View File
@@ -3,12 +3,12 @@ package server
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
@@ -19,10 +19,13 @@ type IDockerWatcher interface {
}
const (
DockerRouterLabelHost = "mc-router.host"
DockerRouterLabelPort = "mc-router.port"
DockerRouterLabelDefault = "mc-router.default"
DockerRouterLabelNetwork = "mc-router.network"
DockerRouterLabelHost = "mc-router.host"
DockerRouterLabelPort = "mc-router.port"
DockerRouterLabelDefault = "mc-router.default"
DockerRouterLabelNetwork = "mc-router.network"
DockerRouterLabelAutoScaleUp = "mc-router.auto-scale-up"
DockerRouterLabelAutoScaleDown = "mc-router.auto-scale-down"
DockerRouterLabelAutoScaleAsleepMOTD = "mc-router.auto-scale-asleep-motd"
)
type dockerWatcherConfig struct {
@@ -63,22 +66,94 @@ type dockerWatcherImpl struct {
client *client.Client
}
func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) ScalerFunc {
if !w.config.autoScaleUp {
func (w *dockerWatcherImpl) makeWakerFunc(rc *routableContainer) WakerFunc {
if rc == nil || !rc.autoScaleUp {
return nil
}
return func(ctx context.Context) error {
logrus.Fatal("Auto scale up is not yet supported for docker")
return nil
return func(ctx context.Context) (string, error) {
containerID := rc.containerID
if containerID == "" {
return "", fmt.Errorf("missing container id for wake")
}
inspect, err := w.client.ContainerInspect(ctx, containerID)
if err != nil {
return "", err
}
if inspect.State == nil {
return "", fmt.Errorf("unable to determine container state")
}
// If paused, unpause; if not running, start; otherwise no-op
if inspect.State.Paused {
logrus.WithFields(logrus.Fields{"containerID": containerID}).Debug("Unpausing container for wake")
if err := w.client.ContainerUnpause(ctx, containerID); err != nil {
return "", err
}
} else if !inspect.State.Running {
logrus.WithFields(logrus.Fields{"containerID": containerID}).Debug("Starting container for wake")
if err := w.client.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
return "", err
}
}
inspect, err = w.client.ContainerInspect(ctx, containerID)
if err != nil {
return "", err
}
data, ok := w.parseContainerData(&inspect)
if !ok {
return "", fmt.Errorf("failed to parse container data after starting")
}
if data.ip == "" {
return "", fmt.Errorf("container has no accessible IP after starting")
}
endpoint := net.JoinHostPort(data.ip, strconv.Itoa(int(data.port)))
// Wait until the container is reachable
deadline := time.Now().Add(60 * time.Second)
for {
conn, err := net.DialTimeout("tcp", endpoint, 1*time.Second)
if err == nil {
_ = conn.Close()
break
}
if ctx.Err() != nil {
return endpoint, ctx.Err()
}
if time.Now().After(deadline) {
return endpoint, fmt.Errorf("timeout waiting for container to become reachable at %s", endpoint)
}
select {
case <-ctx.Done():
return endpoint, ctx.Err()
case <-time.After(500 * time.Millisecond):
}
}
return endpoint, nil
}
}
func (w *dockerWatcherImpl) makeSleeperFunc(_ *routableContainer) ScalerFunc {
if !w.config.autoScaleDown {
func (w *dockerWatcherImpl) makeSleeperFunc(rc *routableContainer) SleeperFunc {
if rc == nil || !rc.autoScaleDown {
return nil
}
return func(ctx context.Context) error {
logrus.Fatal("Auto scale down is not yet supported for docker")
containerID := rc.containerID
if containerID == "" {
return fmt.Errorf("missing container id for sleep")
}
inspect, err := w.client.ContainerInspect(ctx, containerID)
if err != nil {
return err
}
if inspect.State != nil && inspect.State.Running {
// Graceful stop with 60s timeout
timeout := 60
logrus.WithFields(logrus.Fields{"containerID": containerID}).Debug("Stopping container for sleep")
if err := w.client.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout}); err != nil {
return err
}
}
return nil
}
}
@@ -104,7 +179,6 @@ func (w *dockerWatcherImpl) Start(ctx context.Context) error {
}
ticker := time.NewTicker(refreshInterval)
containerMap := map[string]*routableContainer{}
logrus.Trace("Performing initial listing of Docker containers")
initialContainers, err := w.listContainers(ctx)
@@ -112,12 +186,15 @@ func (w *dockerWatcherImpl) Start(ctx context.Context) error {
return err
}
containerMap := map[string]*routableContainer{}
for _, c := range initialContainers {
containerMap[c.externalContainerName] = c
wakerFunc := w.makeWakerFunc(c)
sleeperFunc := w.makeSleeperFunc(c)
if c.externalContainerName != "" {
Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, w.makeWakerFunc(c), w.makeSleeperFunc(c))
Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, wakerFunc, sleeperFunc, c.autoScaleAsleepMOTD)
} else {
Routes.SetDefaultRoute(c.containerEndpoint)
Routes.SetDefaultRoute(c.containerEndpoint, wakerFunc, sleeperFunc, c.autoScaleAsleepMOTD)
}
}
@@ -137,18 +214,26 @@ func (w *dockerWatcherImpl) Start(ctx context.Context) error {
if oldRs, ok := containerMap[rs.externalContainerName]; !ok {
containerMap[rs.externalContainerName] = rs
logrus.WithField("routableContainer", rs).Debug("ADD")
wakerFunc := w.makeWakerFunc(rs)
sleeperFunc := w.makeSleeperFunc(rs)
if rs.externalContainerName != "" {
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, wakerFunc, sleeperFunc, rs.autoScaleAsleepMOTD)
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
Routes.SetDefaultRoute(rs.containerEndpoint, wakerFunc, sleeperFunc, rs.autoScaleAsleepMOTD)
}
} else if oldRs.containerEndpoint != rs.containerEndpoint {
} else if oldRs.containerEndpoint != rs.containerEndpoint ||
oldRs.containerID != rs.containerID ||
oldRs.autoScaleUp != rs.autoScaleUp ||
oldRs.autoScaleDown != rs.autoScaleDown ||
oldRs.autoScaleAsleepMOTD != rs.autoScaleAsleepMOTD {
containerMap[rs.externalContainerName] = rs
wakerFunc := w.makeWakerFunc(rs)
sleeperFunc := w.makeSleeperFunc(rs)
if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalContainerName)
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, wakerFunc, sleeperFunc, rs.autoScaleAsleepMOTD)
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
Routes.SetDefaultRoute(rs.containerEndpoint, wakerFunc, sleeperFunc, rs.autoScaleAsleepMOTD)
}
logrus.WithFields(logrus.Fields{"old": oldRs, "new": rs}).Debug("UPDATE")
}
@@ -160,7 +245,7 @@ func (w *dockerWatcherImpl) Start(ctx context.Context) error {
if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalContainerName)
} else {
Routes.SetDefaultRoute("")
Routes.SetDefaultRoute("", nil, nil, "")
}
logrus.WithField("routableContainer", rs).Debug("DELETE")
}
@@ -179,28 +264,46 @@ func (w *dockerWatcherImpl) Start(ctx context.Context) error {
}
func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableContainer, error) {
containers, err := w.client.ContainerList(ctx, container.ListOptions{})
containers, err := w.client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, err
}
var result []*routableContainer
for _, container := range containers {
data, ok := w.parseContainerData(&container)
inspect, err := w.client.ContainerInspect(ctx, container.ID)
if err != nil {
logrus.WithFields(logrus.Fields{"containerID": container.ID}).WithError(err).Error("Failed to inspect Docker container")
continue
}
data, ok := w.parseContainerData(&inspect)
if !ok {
continue
}
endpoint := ""
if !data.notRunning {
endpoint = fmt.Sprintf("%s:%d", data.ip, data.port)
}
for _, host := range data.hosts {
result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
containerEndpoint: endpoint,
externalContainerName: host,
containerID: container.ID,
autoScaleUp: data.autoScaleUp,
autoScaleDown: data.autoScaleDown,
autoScaleAsleepMOTD: data.autoScaleAsleepMOTD,
})
}
if data.def != nil && *data.def {
result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
containerEndpoint: endpoint,
externalContainerName: "",
containerID: container.ID,
autoScaleUp: data.autoScaleUp,
autoScaleDown: data.autoScaleDown,
autoScaleAsleepMOTD: data.autoScaleAsleepMOTD,
})
}
}
@@ -209,18 +312,24 @@ func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableCont
}
type parsedDockerContainerData struct {
hosts []string
port uint64
def *bool
network *string
ip string
hosts []string
port uint64
def *bool
network *string
ip string
autoScaleDown bool
autoScaleUp bool
autoScaleAsleepMOTD string
notRunning bool
}
func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container) (data parsedDockerContainerData, ok bool) {
for key, value := range container.Labels {
func (w *dockerWatcherImpl) parseContainerData(container *container.InspectResponse) (data parsedDockerContainerData, ok bool) {
data.autoScaleUp = w.config.autoScaleUp
data.autoScaleDown = w.config.autoScaleDown
for key, value := range container.Config.Labels {
if key == DockerRouterLabelHost {
if data.hosts != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelHost)
return
}
@@ -229,14 +338,14 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
if key == DockerRouterLabelPort {
if data.port != 0 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelPort)
return
}
var err error
data.port, err = strconv.ParseUint(value, 10, 32)
if err != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
WithError(err).
Warnf("ignoring container with invalid %s label", DockerRouterLabelPort)
return
@@ -244,24 +353,51 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
}
if key == DockerRouterLabelDefault {
if data.def != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelDefault)
return
}
data.def = new(bool)
lowerValue := strings.TrimSpace(strings.ToLower(value))
*data.def = lowerValue != "" && lowerValue != "0" && lowerValue != "false" && lowerValue != "no"
defaultValue, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
WithError(err).
Warnf("ignoring container with invalid value for %s label", DockerRouterLabelDefault)
return
}
data.def = &defaultValue
}
if key == DockerRouterLabelNetwork {
if data.network != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelNetwork)
return
}
data.network = new(string)
*data.network = value
}
if key == DockerRouterLabelAutoScaleUp {
autoScaleUp, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
WithError(err).
Warnf("ignoring container with invalid value for %s label", DockerRouterLabelAutoScaleUp)
return
}
data.autoScaleUp = autoScaleUp
}
if key == DockerRouterLabelAutoScaleDown {
autoScaleDown, err := strconv.ParseBool(strings.TrimSpace(value))
if err != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
WithError(err).
Warnf("ignoring container with invalid value for %s label", DockerRouterLabelAutoScaleDown)
return
}
data.autoScaleDown = autoScaleDown
}
if key == DockerRouterLabelAutoScaleAsleepMOTD {
data.autoScaleAsleepMOTD = value
}
}
// probably not minecraft related
@@ -270,7 +406,7 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
}
if len(container.NetworkSettings.Networks) == 0 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container, no networks found")
return
}
@@ -304,7 +440,7 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
// if there's more than one network on this container, we should require that the user specifies a network to avoid
// weird problems.
if len(container.NetworkSettings.Networks) > 1 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container, multiple networks found and none specified using label %s", DockerRouterLabelNetwork)
return
}
@@ -315,12 +451,21 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
}
}
if data.ip == "" {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
if data.ip == "" && container.State != nil && container.State.Running {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container, unable to find accessible ip address")
return
}
if container.State != nil && !container.State.Running {
if !w.config.autoScaleUp {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Name}).
Warnf("ignoring container, not running and auto scale up is disabled")
return
}
data.notRunning = true
}
ok = true
return
@@ -329,4 +474,8 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
type routableContainer struct {
externalContainerName string
containerEndpoint string
containerID string
autoScaleUp bool
autoScaleDown bool
autoScaleAsleepMOTD string
}