mirror of
https://github.com/itzg/mc-router.git
synced 2024-11-24 11:55:24 +01:00
297 lines
8.2 KiB
Go
297 lines
8.2 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(socket string, timeoutSeconds int, refreshIntervalSeconds int) error
|
|
Stop()
|
|
}
|
|
|
|
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
|
|
client *client.Client
|
|
contextCancel context.CancelFunc
|
|
}
|
|
|
|
func (w *dockerWatcherImpl) makeWakerFunc(_ *routableContainer) func(ctx context.Context) error {
|
|
return func(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (w *dockerWatcherImpl) Start(socket string, timeoutSeconds int, refreshIntervalSeconds int) error {
|
|
var err error
|
|
|
|
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{}
|
|
|
|
var ctx context.Context
|
|
ctx, w.contextCancel = context.WithCancel(context.Background())
|
|
|
|
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))
|
|
} else {
|
|
Routes.SetDefaultRoute(c.containerEndpoint)
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
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))
|
|
} 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))
|
|
} 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():
|
|
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
|
|
}
|
|
|
|
func (w *dockerWatcherImpl) Stop() {
|
|
if w.contextCancel != nil {
|
|
w.contextCancel()
|
|
}
|
|
}
|
|
|
|
type routableContainer struct {
|
|
externalContainerName string
|
|
containerEndpoint string
|
|
}
|