Files
mc-router/server/docker.go
T

308 lines
8.7 KiB
Go

package server
import (
"context"
"fmt"
"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"
)
type IDockerWatcher interface {
Start(ctx context.Context, socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool) error
}
const (
DockerAPIVersion = "1.24"
DockerRouterLabelHost = "mc-router.host"
DockerRouterLabelPort = "mc-router.port"
DockerRouterLabelDefault = "mc-router.default"
DockerRouterLabelNetwork = "mc-router.network"
)
var DockerWatcher IDockerWatcher = &dockerWatcherImpl{}
type dockerWatcherImpl struct {
sync.RWMutex
autoScaleUp bool
autoScaleDown bool
client *client.Client
}
func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) ScalerFunc {
if !w.autoScaleUp {
return nil
}
return func(ctx context.Context) error {
logrus.Fatal("Auto scale up is not yet supported for docker")
return nil
}
}
func (w *dockerWatcherImpl) makeSleeperFunc(_ *routableContainer) ScalerFunc {
if !w.autoScaleDown {
return nil
}
return func(ctx context.Context) error {
logrus.Fatal("Auto scale down is not yet supported for docker")
return nil
}
}
func (w *dockerWatcherImpl) Start(ctx context.Context, socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool) error {
var err error
w.autoScaleUp = autoScaleUp
w.autoScaleDown = autoScaleDown
timeout := time.Duration(timeoutSeconds) * time.Second
refreshInterval := time.Duration(refreshIntervalSeconds) * time.Second
opts := []client.Opt{
client.WithHost(socket),
client.WithTimeout(timeout),
client.WithHTTPHeaders(map[string]string{
"User-Agent": "mc-router ",
}),
client.WithVersion(DockerAPIVersion),
}
w.client, err = client.NewClientWithOpts(opts...)
if err != nil {
return err
}
ticker := time.NewTicker(refreshInterval)
containerMap := map[string]*routableContainer{}
logrus.Trace("Performing initial listing of Docker containers")
initialContainers, err := w.listContainers(ctx)
if err != nil {
return err
}
for _, c := range initialContainers {
containerMap[c.externalContainerName] = c
if c.externalContainerName != "" {
Routes.CreateMapping(c.externalContainerName, c.containerEndpoint, w.makeWakerFunc(c), w.makeSleeperFunc(c))
} else {
Routes.SetDefaultRoute(c.containerEndpoint)
}
}
go func() {
for {
select {
case <-ticker.C:
logrus.Trace("Listing Docker containers")
containers, err := w.listContainers(ctx)
if err != nil {
logrus.WithError(err).Error("Docker failed to list containers")
return
}
visited := map[string]struct{}{}
for _, rs := range containers {
if oldRs, ok := containerMap[rs.externalContainerName]; !ok {
containerMap[rs.externalContainerName] = rs
logrus.WithField("routableContainer", rs).Debug("ADD")
if rs.externalContainerName != "" {
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
}
} else if oldRs.containerEndpoint != rs.containerEndpoint {
containerMap[rs.externalContainerName] = rs
if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalContainerName)
Routes.CreateMapping(rs.externalContainerName, rs.containerEndpoint, w.makeWakerFunc(rs), w.makeSleeperFunc(rs))
} else {
Routes.SetDefaultRoute(rs.containerEndpoint)
}
logrus.WithFields(logrus.Fields{"old": oldRs, "new": rs}).Debug("UPDATE")
}
visited[rs.externalContainerName] = struct{}{}
}
for _, rs := range containerMap {
if _, ok := visited[rs.externalContainerName]; !ok {
delete(containerMap, rs.externalContainerName)
if rs.externalContainerName != "" {
Routes.DeleteMapping(rs.externalContainerName)
} else {
Routes.SetDefaultRoute("")
}
logrus.WithField("routableContainer", rs).Debug("DELETE")
}
}
case <-ctx.Done():
logrus.Debug("Stopping Docker monitoring")
ticker.Stop()
return
}
}
}()
logrus.Info("Monitoring Docker for Minecraft containers")
return nil
}
func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableContainer, error) {
containers, err := w.client.ContainerList(ctx, container.ListOptions{})
if err != nil {
return nil, err
}
var result []*routableContainer
for _, container := range containers {
data, ok := w.parseContainerData(&container)
if !ok {
continue
}
for _, host := range data.hosts {
result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalContainerName: host,
})
}
if data.def != nil && *data.def {
result = append(result, &routableContainer{
containerEndpoint: fmt.Sprintf("%s:%d", data.ip, data.port),
externalContainerName: "",
})
}
}
return result, nil
}
type parsedDockerContainerData struct {
hosts []string
port uint64
def *bool
network *string
ip string
}
func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container) (data parsedDockerContainerData, ok bool) {
for key, value := range container.Labels {
if key == DockerRouterLabelHost {
if data.hosts != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelHost)
return
}
data.hosts = strings.Split(value, ",")
}
if key == DockerRouterLabelPort {
if data.port != 0 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
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}).
WithError(err).
Warnf("ignoring container with invalid %s label", DockerRouterLabelPort)
return
}
}
if key == DockerRouterLabelDefault {
if data.def != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
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"
}
if key == DockerRouterLabelNetwork {
if data.network != nil {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container with duplicate %s label", DockerRouterLabelNetwork)
return
}
data.network = new(string)
*data.network = value
}
}
// probably not minecraft related
if len(data.hosts) == 0 {
return
}
if len(container.NetworkSettings.Networks) == 0 {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container, no networks found")
return
}
if data.port == 0 {
data.port = 25565
}
if data.network != nil {
// Loop through all the container's networks and attempt to find one whose Network ID, Name, or Aliases match the
// specified network
for name, endpoint := range container.NetworkSettings.Networks {
if name == endpoint.NetworkID {
data.ip = endpoint.IPAddress
}
if name == *data.network {
data.ip = endpoint.IPAddress
break
}
for _, alias := range endpoint.Aliases {
if alias == name {
data.ip = endpoint.IPAddress
break
}
}
}
} else {
// If there's no endpoint specified we can just assume the only one is the network we should use. One caveat is
// 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}).
Warnf("ignoring container, multiple networks found and none specified using label %s", DockerRouterLabelNetwork)
return
}
for _, endpoint := range container.NetworkSettings.Networks {
data.ip = endpoint.IPAddress
break
}
}
if data.ip == "" {
logrus.WithFields(logrus.Fields{"containerId": container.ID, "containerNames": container.Names}).
Warnf("ignoring container, unable to find accessible ip address")
return
}
ok = true
return
}
type routableContainer struct {
externalContainerName string
containerEndpoint string
}