Docker auto-scale and asleep motd status (#488)
This commit is contained in:
+195
-46
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user