diff --git a/.gitignore b/.gitignore index 7da9830..72f6da6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ /mc-router /dist/ /*.private.env.json -/kustomization.yml \ No newline at end of file +/kustomization.yml + +/*.pem \ No newline at end of file diff --git a/.run/run (in docker).run.xml b/.run/run (in docker).run.xml new file mode 100644 index 0000000..7bddb8f --- /dev/null +++ b/.run/run (in docker).run.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.run/run (in docker, auto scale).run.xml b/.run/run (in docker, auto scale).run.xml new file mode 100644 index 0000000..bf632d9 --- /dev/null +++ b/.run/run (in docker, auto scale).run.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 618a24d..9ee0fe5 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,31 @@ then add the _Artifact Registry Reader_ Role to the _Compute Engine default serv then use e.g. `gcloud auth configure-docker europe-docker.pkg.dev` or equivalent one time (to create a `~/.docker/config.json`), and then use e.g. `--default-repo=europe-docker.pkg.dev/YOUR-PROJECT/YOUR-ARTIFACT-REGISTRY` option for `skaffold dev`. +### Running in devcontainer + +This approach is useful for testing changes for [Docker auto scaling](#docker-auto-scale-updown). + +With IntelliJ Ultimate, [use these instructions](https://www.jetbrains.com/help/idea/start-dev-container-inside-ide.html). It is recommended to use the option to mount sources. + +![Start devcontainer in IntelliJ](docs/intellij-devcontainer.png) + +Use the example compose file [in examples/docker-discovery](examples/docker-discovery/compose.yml) or similar with `network_mode` set to "bridge" to ensure that the mc-router instance running within the devcontainer can reach the backend servers. + +When applying the `mc-router.host` label to containers to be auto-discovered, it's easiest to use an external host of "localhost": + +```yaml + vanilla: + image: itzg/minecraft-server + environment: + EULA: "TRUE" + labels: + mc-router.host: "localhost" +``` + +Run one of the labeled services by clicking the run icon in the gutter. + + + ### Performing snapshot release with Docker ```bash diff --git a/docs/intellij-devcontainer.png b/docs/intellij-devcontainer.png new file mode 100644 index 0000000..2bec066 Binary files /dev/null and b/docs/intellij-devcontainer.png differ diff --git a/examples/docker-discovery/compose.yml b/examples/docker-discovery/compose.yml index eabcb4c..3e16205 100644 --- a/examples/docker-discovery/compose.yml +++ b/examples/docker-discovery/compose.yml @@ -14,7 +14,7 @@ services: environment: EULA: "TRUE" labels: - mc-router.host: "localhost.itzg.me" + mc-router.host: "localhost" # To allow for routing from devcontainer network_mode: bridge paper: diff --git a/server/configs.go b/server/configs.go index 2c82cfe..b688ca7 100644 --- a/server/configs.go +++ b/server/configs.go @@ -8,11 +8,11 @@ type WebhookConfig struct { } type AutoScale struct { - Up bool `usage:"Scale from zero on access. For Kubernetes, increases StatefulSet replicas from 0 to 1. For Docker, starts or unpauses the container when accessed"` - Down bool `default:"false" usage:"Scale to zero after idle. For Kubernetes, decreases StatefulSet replicas from 1 to 0. For Docker, gracefully stops the container when there are no connections"` - DownAfter string `default:"10m" usage:"Server scale down delay after there are no connections"` - AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` - AsleepMOTD string `usage:"MOTD to display when auto-scaled down servers are accessed; if empty, no status will be served"` + Up bool `usage:"Scale from zero on access. For Kubernetes, increases StatefulSet replicas from 0 to 1. For Docker, starts or unpauses the container when accessed"` + Down bool `default:"false" usage:"Scale to zero after idle. For Kubernetes, decreases StatefulSet replicas from 1 to 0. For Docker, gracefully stops the container when there are no connections"` + DownAfter time.Duration `default:"10m" usage:"Server scale down delay after there are no connections"` + AllowDeny string `usage:"Path to config for server allowlists and denylists. If a global/server entry is specified, only players allowed to connect to the server will be able to trigger a scale up when -auto-scale-up is enabled or cancel active down scalers when -auto-scale-down is enabled"` + AsleepMOTD string `usage:"MOTD to display when auto-scaled down servers are accessed; if empty, no status will be served"` } type RoutesConfig struct { @@ -37,9 +37,9 @@ type Config struct { KubeNamespace string `usage:"The namespace to watch or blank for all, which is the default"` InDocker bool `usage:"Use Docker service discovery"` InDockerSwarm bool `usage:"Use Docker Swarm service discovery"` - DockerSocket string `default:"unix:///var/run/docker.sock" usage:"Path to Docker socket to use"` - DockerTimeout int `default:"0" usage:"Timeout configuration in seconds for the Docker integrations"` - DockerRefreshInterval int `default:"15" usage:"Refresh interval in seconds for the Docker integrations"` + DockerSocket string `usage:"Path to Docker socket to use"` + DockerTimeout time.Duration `usage:"Timeout (as duration) for the Docker integrations"` + DockerRefreshInterval time.Duration `default:"15s" usage:"Refresh interval (as duration) for the Docker integrations"` DockerApiVersion string `usage:"Instead of auto-negotiating, use specific Docker API version"` MetricsBackend string `default:"discard" usage:"Backend to use for metrics exposure/publishing: discard,expvar,influxdb,prometheus"` MetricsBackendConfig MetricsBackendConfig diff --git a/server/docker.go b/server/docker.go index 8c144c0..e29330c 100644 --- a/server/docker.go +++ b/server/docker.go @@ -29,12 +29,12 @@ const ( ) type dockerWatcherConfig struct { - autoScaleUp bool - autoScaleDown bool - socket string - timeoutSeconds int - refreshIntervalSeconds int - apiVersion string + autoScaleUp bool + autoScaleDown bool + socket string + timeout time.Duration + refreshInterval time.Duration + apiVersion string } func (c *dockerWatcherConfig) apiVersionOpt() client.Opt { @@ -47,15 +47,15 @@ func (c *dockerWatcherConfig) apiVersionOpt() client.Opt { } } -func NewDockerWatcher(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { +func NewDockerWatcher(socket string, timeout time.Duration, refreshInterval time.Duration, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { return &dockerWatcherImpl{ config: dockerWatcherConfig{ - socket: socket, - timeoutSeconds: timeoutSeconds, - refreshIntervalSeconds: refreshIntervalSeconds, - autoScaleUp: autoScaleUp, - autoScaleDown: autoScaleDown, - apiVersion: dockerApiVersion, + socket: socket, + timeout: timeout, + refreshInterval: refreshInterval, + autoScaleUp: autoScaleUp, + autoScaleDown: autoScaleDown, + apiVersion: dockerApiVersion, }, } } @@ -225,24 +225,25 @@ func (w *dockerWatcherImpl) monitorContainers(ctx context.Context) error { func (w *dockerWatcherImpl) Start(ctx context.Context) error { var err error - timeout := time.Duration(w.config.timeoutSeconds) * time.Second - refreshInterval := time.Duration(w.config.refreshIntervalSeconds) * time.Second - opts := []client.Opt{ - client.WithHost(w.config.socket), - client.WithTimeout(timeout), + client.FromEnv, + client.WithTimeout(w.config.timeout), client.WithHTTPHeaders(map[string]string{ "User-Agent": "mc-router ", }), w.config.apiVersionOpt(), } + if w.config.socket != "" { + opts = append(opts, client.WithHost(w.config.socket)) + } w.client, err = client.NewClientWithOpts(opts...) if err != nil { return err } - ticker := time.NewTicker(refreshInterval) + // TODO: replace all this with events listening + ticker := time.NewTicker(w.config.refreshInterval) logrus.Trace("Performing initial listing of Docker containers") initialContainers, err := w.listContainers(ctx) diff --git a/server/docker_swarm.go b/server/docker_swarm.go index bbe1046..319d25e 100644 --- a/server/docker_swarm.go +++ b/server/docker_swarm.go @@ -19,15 +19,15 @@ import ( "github.com/sirupsen/logrus" ) -func NewDockerSwarmWatcher(socket string, timeoutSeconds int, refreshIntervalSeconds int, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { +func NewDockerSwarmWatcher(socket string, timeout time.Duration, refreshInterval time.Duration, autoScaleUp bool, autoScaleDown bool, dockerApiVersion string) IDockerWatcher { return &dockerSwarmWatcherImpl{ config: dockerWatcherConfig{ - socket: socket, - timeoutSeconds: timeoutSeconds, - refreshIntervalSeconds: refreshIntervalSeconds, - autoScaleUp: autoScaleUp, - autoScaleDown: autoScaleDown, - apiVersion: dockerApiVersion, + socket: socket, + timeout: timeout, + refreshInterval: refreshInterval, + autoScaleUp: autoScaleUp, + autoScaleDown: autoScaleDown, + apiVersion: dockerApiVersion, }, } } @@ -61,12 +61,9 @@ func (w *dockerSwarmWatcherImpl) makeSleeperFunc(_ *routableService) SleeperFunc func (w *dockerSwarmWatcherImpl) Start(ctx context.Context) error { var err error - timeout := time.Duration(w.config.timeoutSeconds) * time.Second - refreshInterval := time.Duration(w.config.refreshIntervalSeconds) * time.Second - opts := []client.Opt{ client.WithHost(w.config.socket), - client.WithTimeout(timeout), + client.WithTimeout(w.config.timeout), client.WithHTTPHeaders(map[string]string{ "User-Agent": "mc-router ", }), @@ -78,7 +75,7 @@ func (w *dockerSwarmWatcherImpl) Start(ctx context.Context) error { return err } - ticker := time.NewTicker(refreshInterval) + ticker := time.NewTicker(w.config.refreshInterval) serviceMap := map[string]*routableService{} logrus.Trace("Performing initial listing of Docker containers") diff --git a/server/down_scaler.go b/server/down_scaler.go index 0df1007..476e1c5 100644 --- a/server/down_scaler.go +++ b/server/down_scaler.go @@ -56,7 +56,6 @@ func (ds *downScalerImpl) Begin(backendEndpoint string) { scaleDownCancel() } - logrus.WithField("backendEndpoint", backendEndpoint).Debug("Beginning scale down") scaleDownContext, scaleDownContextCancellation := context.WithCancel(ds.parentContext) ds.contextCancellations[backendEndpoint] = scaleDownContextCancellation go ds.scaleDown(scaleDownContext, backendEndpoint) @@ -78,12 +77,18 @@ func (ds *downScalerImpl) Cancel(backendEndpoint string) { } func (ds *downScalerImpl) scaleDown(ctx context.Context, backendEndpoint string) { + logrus.WithField("backendEndpoint", backendEndpoint). + WithField("delay", ds.delay). + Debug("Starting scale-down timer") for { select { case <-ctx.Done(): return case <-time.After(ds.delay): sleepers := Routes.GetSleepers(backendEndpoint) + logrus.WithField("backendEndpoint", backendEndpoint). + WithField("sleepers", len(sleepers)). + Debug("Found sleepers to use") if len(sleepers) == 0 { return } diff --git a/server/server.go b/server/server.go index 757c1ab..e412ea5 100644 --- a/server/server.go +++ b/server/server.go @@ -7,7 +7,6 @@ import ( "os" "runtime/pprof" "strconv" - "time" "github.com/sirupsen/logrus" ) @@ -50,10 +49,7 @@ func NewServer(ctx context.Context, config *Config) (*Server, error) { metricsBuilder := NewMetricsBuilder(config.MetricsBackend, &config.MetricsBackendConfig) downScalerEnabled := config.AutoScale.Down && (config.InKubeCluster || config.KubeConfig != "" || config.InDocker) - downScalerDelay, err := time.ParseDuration(config.AutoScale.DownAfter) - if err != nil { - return nil, fmt.Errorf("could not parse auto-scale-down-after duration: %w", err) - } + downScalerDelay := config.AutoScale.DownAfter // Only one instance should be created DownScaler = NewDownScaler(ctx, downScalerEnabled, downScalerDelay)