Merge feature/multi-docker-host in main
This commit is contained in:
commit
50dcb2537d
9 files changed
+297
-5
No files matched your search
@@ -52,6 +52,9 @@ CS2_PORT_RANGE_END=27115
|
|||||||
# Gemeinsames Volume für die Spieldateien (~60 GB), damit nicht jedes Match neu herunterlädt
|
# Gemeinsames Volume für die Spieldateien (~60 GB), damit nicht jedes Match neu herunterlädt
|
||||||
CS2_DOCKER_VOLUME=cs2-mapveto-gamefiles
|
CS2_DOCKER_VOLUME=cs2-mapveto-gamefiles
|
||||||
CS2_DOCKER_EXTRA_ARGS=
|
CS2_DOCKER_EXTRA_ARGS=
|
||||||
|
# Optional: mehrere Docker-Hosts als JSON-Liste (eine Zeile). Leer = nur CS2_PUBLIC_HOST/DOCKER_HOST.
|
||||||
|
# Einrichtung pro Host: bash deploy/docker-host-einrichten.sh <ssh-ziel>
|
||||||
|
# CS2_DOCKER_HOSTS=[{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
|
||||||
CS2_START_TIMEOUT_MINUTES=20
|
CS2_START_TIMEOUT_MINUTES=20
|
||||||
DOCKER_BIN=docker
|
DOCKER_BIN=docker
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,24 @@ Voraussetzungen:
|
|||||||
|
|
||||||
Container heißen `cs2mv-<matchId>` und tragen das Label `cs2-mapveto=1`. Stoppen bedeutet `docker rm -f`.
|
Container heißen `cs2mv-<matchId>` und tragen das Label `cs2-mapveto=1`. Stoppen bedeutet `docker rm -f`.
|
||||||
|
|
||||||
|
### Mehrere Docker-Hosts
|
||||||
|
|
||||||
|
Mit `CS2_DOCKER_HOSTS` verteilt die Anwendung Matches auf mehrere Docker-Hosts. Jedes neue Match läuft auf dem Host mit den meisten freien Ports. Der Host bleibt im Match gespeichert, Status, Konfiguration und Stopp gehen immer an denselben Host.
|
||||||
|
|
||||||
|
1. Jeden Host einrichten (SSH-Schlüssel, Docker, Volume, Image):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/docker-host-einrichten.sh cs2@192.168.178.111
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Die Hosts in der `.env` eintragen, als JSON-Liste in einer Zeile:
|
||||||
|
|
||||||
|
```
|
||||||
|
CS2_DOCKER_HOSTS=[{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139},{"name":"host2","dockerHost":"ssh://cs2@192.168.178.112","publicHost":"192.168.178.112","portStart":27100,"portEnd":27139}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Jeder Host braucht eigene Spieldateien (rund 60 bis 90 GB). Die Ports müssen auf jedem Host frei sein.
|
||||||
|
|
||||||
### Option B: Vorhandene Server (`SERVER_ADAPTER=static`)
|
### Option B: Vorhandene Server (`SERVER_ADAPTER=static`)
|
||||||
|
|
||||||
Für selbst betriebene, dauerhaft laufende CS2-Server:
|
Für selbst betriebene, dauerhaft laufende CS2-Server:
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Richtet einen Docker-Host für CS2 Mapveto ein. Läuft vom eigenen Rechner aus per SSH.
|
||||||
|
#
|
||||||
|
# Aufruf: bash deploy/docker-host-einrichten.sh <ssh-ziel> [volume]
|
||||||
|
# Beispiel: bash deploy/docker-host-einrichten.sh cs2@192.168.178.111
|
||||||
|
#
|
||||||
|
# Voraussetzungen: SSH-Schlüssel hinterlegt (ohne Passwortabfrage), Docker auf dem Host installiert.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET="${1:?Aufruf: $0 <ssh-ziel> [volume]}"
|
||||||
|
VOLUME="${2:-cs2-mapveto-gamefiles}"
|
||||||
|
IMAGE="joedwards32/cs2:latest"
|
||||||
|
HOST_PART="${TARGET#*@}"
|
||||||
|
|
||||||
|
echo "→ Verbindung zu $TARGET prüfen"
|
||||||
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$TARGET" true
|
||||||
|
|
||||||
|
echo "→ Docker prüfen"
|
||||||
|
if ! ssh "$TARGET" "command -v docker >/dev/null 2>&1"; then
|
||||||
|
echo "Docker fehlt auf $HOST_PART. Installation: https://docs.docker.com/engine/install/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ssh "$TARGET" "docker info >/dev/null"
|
||||||
|
|
||||||
|
echo "→ Freier Platz für Docker (CS2 braucht rund 90 GB)"
|
||||||
|
ssh "$TARGET" "df -BG /var/lib/docker 2>/dev/null | tail -1 || df -BG / | tail -1"
|
||||||
|
|
||||||
|
echo "→ Volume $VOLUME anlegen (falls nicht vorhanden)"
|
||||||
|
ssh "$TARGET" "docker volume inspect $VOLUME >/dev/null 2>&1 || docker volume create $VOLUME >/dev/null"
|
||||||
|
|
||||||
|
echo "→ Image $IMAGE laden"
|
||||||
|
ssh "$TARGET" "docker pull $IMAGE"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Fertig. Diesen Eintrag in CS2_DOCKER_HOSTS (JSON-Liste in der .env) aufnehmen:
|
||||||
|
|
||||||
|
{"name":"$HOST_PART","dockerHost":"ssh://$TARGET","publicHost":"$HOST_PART","portStart":27100,"portEnd":27139}
|
||||||
|
|
||||||
|
Hinweise:
|
||||||
|
- name muss pro Host eindeutig sein.
|
||||||
|
- Ports 27100–27139 dürfen auf dem Host frei sein. Pro Host sind bis zu 20 Matches möglich.
|
||||||
|
- Der erste Server-Start lädt die CS2-Spieldateien (rund 60 bis 90 GB). Das dauert lange.
|
||||||
|
EOF
|
||||||
+3
-1
@@ -20,7 +20,9 @@ export function createApp(config, { adapter: adapterOverride, logger = console }
|
|||||||
const service = new MatchService({ db, pools, config, notifier });
|
const service = new MatchService({ db, pools, config, notifier });
|
||||||
|
|
||||||
let manager;
|
let manager;
|
||||||
const adapter = adapterOverride ?? createServerAdapter(config, { getUsedPorts: () => manager.usedPorts() });
|
const adapter = adapterOverride ?? createServerAdapter(config, {
|
||||||
|
getUsedPorts: (host) => (host ? manager.usedPortsOn(host) : manager.usedPorts()),
|
||||||
|
});
|
||||||
manager = new ServerManager({ db, service, adapter, config, logger });
|
manager = new ServerManager({ db, service, adapter, config, logger });
|
||||||
manager.init();
|
manager.init();
|
||||||
service.startVetoTimer();
|
service.startVetoTimer();
|
||||||
|
|||||||
@@ -22,6 +22,32 @@ function bool(name, fallback) {
|
|||||||
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
|
return ['1', 'true', 'yes', 'ja', 'on'].includes(raw.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDockerHosts(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
let list;
|
||||||
|
try {
|
||||||
|
list = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error('CS2_DOCKER_HOSTS ist kein gültiges JSON');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(list) || list.length === 0) throw new Error('CS2_DOCKER_HOSTS braucht mindestens einen Eintrag');
|
||||||
|
const names = new Set();
|
||||||
|
return list.map((h) => {
|
||||||
|
if (!h.name || !h.publicHost) throw new Error('Jeder Docker-Host braucht name und publicHost');
|
||||||
|
if (names.has(h.name)) throw new Error(`Docker-Host-Name "${h.name}" kommt doppelt vor`);
|
||||||
|
names.add(h.name);
|
||||||
|
return {
|
||||||
|
name: String(h.name),
|
||||||
|
dockerHost: h.dockerHost ?? null,
|
||||||
|
publicHost: String(h.publicHost),
|
||||||
|
rconHost: h.rconHost ?? h.publicHost,
|
||||||
|
portRangeStart: h.portStart == null ? undefined : Number(h.portStart),
|
||||||
|
portRangeEnd: h.portEnd == null ? undefined : Number(h.portEnd),
|
||||||
|
volume: h.volume ?? undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function str(name, fallback = '') {
|
function str(name, fallback = '') {
|
||||||
const raw = process.env[name];
|
const raw = process.env[name];
|
||||||
return raw === undefined || raw === '' ? fallback : raw;
|
return raw === undefined || raw === '' ? fallback : raw;
|
||||||
@@ -85,6 +111,9 @@ export function loadConfig(overrides = {}) {
|
|||||||
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
|
volume: str('CS2_DOCKER_VOLUME', 'cs2-mapveto-gamefiles'),
|
||||||
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
|
extraArgs: str('CS2_DOCKER_EXTRA_ARGS'),
|
||||||
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
|
startTimeoutMinutes: int('CS2_START_TIMEOUT_MINUTES', 20),
|
||||||
|
// Optional: mehrere Docker-Hosts als JSON-Liste, z. B.
|
||||||
|
// [{"name":"host1","dockerHost":"ssh://cs2@192.168.178.111","publicHost":"192.168.178.111","portStart":27100,"portEnd":27139}]
|
||||||
|
hosts: parseDockerHosts(str('CS2_DOCKER_HOSTS')),
|
||||||
},
|
},
|
||||||
|
|
||||||
staticPool: {
|
staticPool: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DemoServerAdapter } from './demoAdapter.js';
|
import { DemoServerAdapter } from './demoAdapter.js';
|
||||||
import { DockerServerAdapter } from './dockerAdapter.js';
|
import { DockerHostPool } from './dockerAdapter.js';
|
||||||
import { StaticPoolServerAdapter } from './staticPoolAdapter.js';
|
import { StaticPoolServerAdapter } from './staticPoolAdapter.js';
|
||||||
|
|
||||||
export function createServerAdapter(config, deps) {
|
export function createServerAdapter(config, deps) {
|
||||||
@@ -37,7 +37,7 @@ export function createServerAdapter(config, deps) {
|
|||||||
case 'demo':
|
case 'demo':
|
||||||
return new DemoServerAdapter(config.demo);
|
return new DemoServerAdapter(config.demo);
|
||||||
case 'docker':
|
case 'docker':
|
||||||
return new DockerServerAdapter(config.docker, deps);
|
return new DockerHostPool(config.docker, deps);
|
||||||
case 'static':
|
case 'static':
|
||||||
return new StaticPoolServerAdapter(config.staticPool, deps);
|
return new StaticPoolServerAdapter(config.staticPool, deps);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export class DockerServerAdapter {
|
|||||||
|
|
||||||
async docker(args, timeout = 60_000) {
|
async docker(args, timeout = 60_000) {
|
||||||
if (this.runDocker) return this.runDocker(args);
|
if (this.runDocker) return this.runDocker(args);
|
||||||
const { stdout } = await exec(this.options.bin, args, { timeout, maxBuffer: 4 * 1024 * 1024 });
|
const env = this.options.dockerHost ? { ...process.env, DOCKER_HOST: this.options.dockerHost } : process.env;
|
||||||
|
const { stdout } = await exec(this.options.bin, args, { timeout, maxBuffer: 4 * 1024 * 1024, env });
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +138,14 @@ export class DockerServerAdapter {
|
|||||||
await this.docker(args, 15 * 60_000);
|
await this.docker(args, 15 * 60_000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handle: { container, rconHost: this.options.rconHost, port, startedAt: Date.now(), rconPassword: secrets.rconPassword },
|
handle: {
|
||||||
|
container,
|
||||||
|
hostName: this.options.name,
|
||||||
|
rconHost: this.options.rconHost,
|
||||||
|
port,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
rconPassword: secrets.rconPassword,
|
||||||
|
},
|
||||||
host: this.options.host,
|
host: this.options.host,
|
||||||
port,
|
port,
|
||||||
tvPort,
|
tvPort,
|
||||||
@@ -192,3 +200,99 @@ export class DockerServerAdapter {
|
|||||||
await this.docker(['rm', '-f', handle.container]).catch(() => {});
|
await this.docker(['rm', '-f', handle.container]).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Mehrere Docker-Hosts. Jedes Match läuft auf dem Host mit den meisten freien Port-Paaren,
|
||||||
|
* der Host steht im Handle und damit im Match. Start, Status, Konfiguration und Stopp gehen
|
||||||
|
* immer an denselben Host. Ohne CS2_DOCKER_HOSTS gibt es genau einen Host: den bisherigen.
|
||||||
|
*/
|
||||||
|
export class DockerHostPool {
|
||||||
|
constructor(options, { getUsedPorts, runDocker } = {}) {
|
||||||
|
this.name = 'docker';
|
||||||
|
this.simulated = false;
|
||||||
|
this.depsGetUsedPorts = getUsedPorts;
|
||||||
|
const hosts = options.hosts ?? [{
|
||||||
|
name: 'default',
|
||||||
|
dockerHost: process.env.DOCKER_HOST || null,
|
||||||
|
publicHost: options.host,
|
||||||
|
rconHost: options.rconHost,
|
||||||
|
portRangeStart: options.portRangeStart,
|
||||||
|
portRangeEnd: options.portRangeEnd,
|
||||||
|
volume: options.volume,
|
||||||
|
}];
|
||||||
|
// Fehlende Angaben kommen aus den allgemeinen Docker-Einstellungen
|
||||||
|
this.hosts = hosts.map((h) => {
|
||||||
|
const host = {
|
||||||
|
name: h.name,
|
||||||
|
dockerHost: h.dockerHost ?? null,
|
||||||
|
publicHost: h.publicHost ?? options.host,
|
||||||
|
rconHost: h.rconHost ?? options.rconHost,
|
||||||
|
portRangeStart: h.portRangeStart ?? options.portRangeStart,
|
||||||
|
portRangeEnd: h.portRangeEnd ?? options.portRangeEnd,
|
||||||
|
volume: h.volume ?? options.volume,
|
||||||
|
};
|
||||||
|
return { ...options, ...host, host: host.publicHost };
|
||||||
|
});
|
||||||
|
this.adapters = new Map(this.hosts.map((h) => [h.name, new DockerServerAdapter(h, {
|
||||||
|
getUsedPorts: () => (getUsedPorts ? getUsedPorts(h.publicHost) : new Set()),
|
||||||
|
runDocker,
|
||||||
|
})]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Freie Port-Paare auf einem Host. */
|
||||||
|
freeSlots(host) {
|
||||||
|
const used = this.getUsedPorts(host.publicHost);
|
||||||
|
let free = 0;
|
||||||
|
for (let p = host.portRangeStart; p + 1 <= host.portRangeEnd; p += 2) {
|
||||||
|
if (!used.has(p) && !used.has(p + 1)) free += 1;
|
||||||
|
}
|
||||||
|
return free;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsedPorts(publicHost) {
|
||||||
|
return this.depsGetUsedPorts?.(publicHost) ?? new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
pickHost() {
|
||||||
|
const best = this.hosts
|
||||||
|
.map((h) => ({ host: h, free: this.freeSlots(h) }))
|
||||||
|
.filter((x) => x.free > 0)
|
||||||
|
.sort((a, b) => b.free - a.free)[0];
|
||||||
|
if (!best) throw new Error('Kein Docker-Host mit freien Ports');
|
||||||
|
return best.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(args) {
|
||||||
|
return this.adapters.get(this.pickHost().name).start(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterFor(handle) {
|
||||||
|
const name = handle?.hostName ?? this.hosts[0].name;
|
||||||
|
const adapter = this.adapters.get(name);
|
||||||
|
if (!adapter) throw new Error(`Docker-Host "${name}" ist nicht (mehr) konfiguriert`);
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async status(handle) {
|
||||||
|
return this.adapterFor(handle).status(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
async configure(handle, plan) {
|
||||||
|
return this.adapterFor(handle).configure(handle, plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRoundBackups(handle, matchId) {
|
||||||
|
return this.adapterFor(handle).listRoundBackups(handle, matchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadRoundBackup(handle, file) {
|
||||||
|
return this.adapterFor(handle).loadRoundBackup(handle, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendCommands(handle, commands) {
|
||||||
|
return this.adapterFor(handle).sendCommands(handle, commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(handle) {
|
||||||
|
return this.adapterFor(handle).stop(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,16 @@ export class ServerManager {
|
|||||||
this.service.changed(matchId);
|
this.service.changed(matchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ports, die auf einem bestimmten Docker-Host belegt sind. */
|
||||||
|
usedPortsOn(host) {
|
||||||
|
const used = new Set();
|
||||||
|
for (const r of this.db.prepare(`SELECT port, tv_port FROM servers WHERE host = ? AND status != 'stopped' AND port IS NOT NULL`).all(host)) {
|
||||||
|
used.add(r.port);
|
||||||
|
used.add(r.tv_port);
|
||||||
|
}
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
usedPorts() {
|
usedPorts() {
|
||||||
const used = new Set();
|
const used = new Set();
|
||||||
for (const r of this.db.prepare(`SELECT host, port, tv_port FROM servers WHERE status != 'stopped' AND port IS NOT NULL`).all()) {
|
for (const r of this.db.prepare(`SELECT host, port, tv_port FROM servers WHERE status != 'stopped' AND port IS NOT NULL`).all()) {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { DockerHostPool } from '../src/servers/dockerAdapter.js';
|
||||||
|
|
||||||
|
const secrets = { rconPassword: 'Rcon123', serverPassword: 'Pw123', tvPassword: 'Tv123' };
|
||||||
|
|
||||||
|
const hostA = { name: 'a', publicHost: '10.0.0.1', portRangeStart: 27100, portRangeEnd: 27103 }; // 2 Paare
|
||||||
|
const hostB = { name: 'b', publicHost: '10.0.0.2', portRangeStart: 27100, portRangeEnd: 27139 }; // 20 Paare
|
||||||
|
|
||||||
|
function options(hosts) {
|
||||||
|
return {
|
||||||
|
bin: 'docker', image: 'joedwards32/cs2:latest', host: '10.0.0.9', rconHost: '10.0.0.9',
|
||||||
|
portRangeStart: 27015, portRangeEnd: 27115, gslt: '', volume: 'cs2-mapveto-gamefiles', extraArgs: '',
|
||||||
|
startTimeoutMinutes: 1, hosts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Docker-Aufrufe merken, Container gelten sofort als fertig. */
|
||||||
|
function fakeDocker() {
|
||||||
|
const calls = [];
|
||||||
|
const run = async (args) => {
|
||||||
|
calls.push(args);
|
||||||
|
if (args[0] === 'inspect') return 'running';
|
||||||
|
if (args[0] === 'logs') return 'Starting CS2 Dedicated Server';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
return { run, calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nur die Konfiguration der Hosts, ohne auf echte Container zu warten. */
|
||||||
|
function pool(hosts, used = {}) {
|
||||||
|
const docker = fakeDocker();
|
||||||
|
const p = new DockerHostPool(options(hosts), {
|
||||||
|
getUsedPorts: (publicHost) => new Set(used[publicHost] ?? []),
|
||||||
|
runDocker: docker.run,
|
||||||
|
});
|
||||||
|
for (const a of p.adapters.values()) a.waitForUpdateCheck = async () => {};
|
||||||
|
return { pool: p, calls: docker.calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Ohne CS2_DOCKER_HOSTS gibt es genau einen Host, wie bisher', () => {
|
||||||
|
const { pool: p } = pool(null);
|
||||||
|
assert.equal(p.hosts.length, 1);
|
||||||
|
assert.equal(p.hosts[0].publicHost, '10.0.0.9');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein Match landet auf dem Host mit den meisten freien Ports', async () => {
|
||||||
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': [27100] }); // A hat ein Paar belegt, B ist frei
|
||||||
|
const r = await p.start({ matchId: 'm1', mode: 'competitive', secrets });
|
||||||
|
assert.equal(r.handle.hostName, 'b');
|
||||||
|
assert.equal(r.host, '10.0.0.2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ein voller Host wird übersprungen', async () => {
|
||||||
|
const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i); // alle Ports von A belegt
|
||||||
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA });
|
||||||
|
const r = await p.start({ matchId: 'm2', mode: 'wingman', secrets });
|
||||||
|
assert.equal(r.handle.hostName, 'b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sind alle Hosts voll, gibt es einen klaren Fehler', async () => {
|
||||||
|
const fullA = Array.from({ length: 4 }, (_, i) => 27100 + i);
|
||||||
|
const fullB = Array.from({ length: 40 }, (_, i) => 27100 + i);
|
||||||
|
const { pool: p } = pool([hostA, hostB], { '10.0.0.1': fullA, '10.0.0.2': fullB });
|
||||||
|
await assert.rejects(p.start({ matchId: 'm3', mode: 'wingman', secrets }), /Kein Docker-Host mit freien Ports/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Status, Stopp und Befehle gehen an den Host des Matches', async () => {
|
||||||
|
const { pool: p, calls } = pool([hostA, hostB], { '10.0.0.1': [27100] });
|
||||||
|
const r = await p.start({ matchId: 'm4', mode: 'competitive', secrets });
|
||||||
|
calls.length = 0;
|
||||||
|
await p.stop(r.handle);
|
||||||
|
assert.deepEqual(calls.at(-1)[0], 'rm');
|
||||||
|
assert.equal(calls.at(-1).at(-1), r.handle.container);
|
||||||
|
// Ein Handle von einem Host, der nicht mehr konfiguriert ist, wird abgelehnt
|
||||||
|
await assert.rejects(p.status({ ...r.handle, hostName: 'gelöscht' }), /nicht \(mehr\) konfiguriert/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Alte Handles ohne Host-Namen laufen auf dem ersten Host', () => {
|
||||||
|
const { pool: p } = pool([hostA, hostB]);
|
||||||
|
assert.equal(p.adapterFor({ container: 'x' }), p.adapters.get('a'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user