feat(velocity): legacy forwarding 列表跟随 CR 的 forwarding=legacy 标签实时更新 (#15)
This commit is contained in:
15 files changed
+522
-14
No files matched your search
+13
-10
@@ -38,10 +38,12 @@
|
|||||||
# FELIS_NANO_PROXY_CIDR the proxy allowed to reach a non-loopback nano bind, as an address
|
# FELIS_NANO_PROXY_CIDR the proxy allowed to reach a non-loopback nano bind, as an address
|
||||||
# with a prefix length (for example 10.0.0.7/32). firewalld opens the
|
# with a prefix length (for example 10.0.0.7/32). firewalld opens the
|
||||||
# port to that source only; unset, it opens nothing
|
# port to that source only; unset, it opens nothing
|
||||||
# FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that receive their identity
|
# FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that always receive their
|
||||||
# through the handshake address instead of modern forwarding
|
# identity through the handshake address instead of modern forwarding
|
||||||
# (default: legacy18). Read once at Velocity start, so changing it
|
# (default: legacy18). A floor: any server whose MinecraftServer CR is
|
||||||
# means re-running this script and restarting the proxy.
|
# labelled felis.lolicon.best/forwarding=legacy joins it while the
|
||||||
|
# proxy runs (docs/operations.md), so a new 1.8 backend needs a label,
|
||||||
|
# not a re-run. Changing the floor itself means re-running this script.
|
||||||
# FELIS_VELOCITY_XMX maximum heap of the Velocity proxy, as <n>M or <n>G (default: 1G;
|
# FELIS_VELOCITY_XMX maximum heap of the Velocity proxy, as <n>M or <n>G (default: 1G;
|
||||||
# at least 256M). docs/operations.md sizes it by player count.
|
# at least 256M). docs/operations.md sizes it by player count.
|
||||||
# FELIS_VELOCITY_FORK_JAR path to a Felis-Legacy Velocity fork build to install as the
|
# FELIS_VELOCITY_FORK_JAR path to a Felis-Legacy Velocity fork build to install as the
|
||||||
@@ -2741,12 +2743,13 @@ install_velocity_service() {
|
|||||||
# fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy;
|
# fork reads this list from -Dfelis.legacy-forwarding.servers and forwards those servers legacy;
|
||||||
# every other backend keeps modern+secret untouched.
|
# every other backend keeps modern+secret untouched.
|
||||||
#
|
#
|
||||||
# The list is a JVM system property, so it is fixed for the life of the proxy process and a
|
# This value is the floor of the list. The felis-velocity plugin adds every server whose
|
||||||
# change needs a Velocity restart. FELIS_LEGACY_FORWARDING_SERVERS makes that reachable
|
# MinecraftServer CR is labelled felis.lolicon.best/forwarding=legacy by rewriting the same
|
||||||
# without editing this script, which is as far as a startup property can go. Having it follow
|
# property on each server-list refresh (LegacyForwarding.java), and drops it again when the
|
||||||
# the MinecraftServer CRs instead is a larger change: the forwarding decision lives in the
|
# label goes; the floor always stays in. A fork carrying patch 0004 re-reads the property on
|
||||||
# fork's patch to Velocity core, not in the Felis plugin, so core would need to read state the
|
# every backend connection, so a label applies from the next connection. A fork with 0003
|
||||||
# plugin owns and refreshes.
|
# alone reads it once, after the plugin's first refresh, so a label applies at the next proxy
|
||||||
|
# restart. Stock Velocity ignores it, and the plugin logs a warning for a labelled server.
|
||||||
#
|
#
|
||||||
# The -D below is double-quoted in ExecStart on purpose. The fork trims each element, so it
|
# The -D below is double-quoted in ExecStart on purpose. The fork trims each element, so it
|
||||||
# accepts "legacy18, legacy112", but systemd splits ExecStart on whitespace before java ever
|
# accepts "legacy18, legacy112", but systemd splits ExecStart on whitespace before java ever
|
||||||
|
|||||||
@@ -482,6 +482,12 @@ components:
|
|||||||
playerCountUnknown:
|
playerCountUnknown:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Present and true while the operator cannot read the player count over RCON; idle auto-stop waits until it can.
|
description: Present and true while the operator cannot read the player count over RCON; idle auto-stop waits until it can.
|
||||||
|
legacyForwarding:
|
||||||
|
type: boolean
|
||||||
|
description: >-
|
||||||
|
Present and true when the CR carries the label felis.lolicon.best/forwarding=legacy.
|
||||||
|
The proxy then forwards this server's players BungeeCord-style in the handshake
|
||||||
|
address instead of modern forwarding (Felis-Legacy Velocity fork only).
|
||||||
|
|
||||||
FleetServer:
|
FleetServer:
|
||||||
description: One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView).
|
description: One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView).
|
||||||
|
|||||||
@@ -383,6 +383,38 @@ version, in this order, each step one release:
|
|||||||
3. A later release stops serving `v1alpha1`. Felis itself reads through one Go type at a
|
3. A later release stops serving `v1alpha1`. Felis itself reads through one Go type at a
|
||||||
time, so the operator and felis-api switch in the release that moves storage.
|
time, so the operator and felis-api switch in the release that moves storage.
|
||||||
|
|
||||||
|
### Legacy-forwarded backends [VM-VERIFIED]
|
||||||
|
|
||||||
|
A 1.8-era backend sits behind ViaVersion, which drops modern forwarding's login plugin
|
||||||
|
message on the way down to protocol 47, so the proxy has to hand that server the
|
||||||
|
player's identity BungeeCord-style, in the handshake address. Only the Felis-Legacy
|
||||||
|
Velocity fork can do that per server. Mark the server's CR and the proxy picks it up at
|
||||||
|
its next server-list refresh (every 15 s):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding=legacy
|
||||||
|
kubectl -n minecraft label minecraftserver <name> felis.lolicon.best/forwarding- # back to modern
|
||||||
|
journalctl -u felis-velocity | grep 'legacy forwarding list'
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer's `FELIS_LEGACY_FORWARDING_SERVERS` (default `legacy18`) stays in the list
|
||||||
|
whatever the labels say. What a label does depends on the proxy the host runs:
|
||||||
|
|
||||||
|
| Proxy | A label applies |
|
||||||
|
|---|---|
|
||||||
|
| Fork with patch 0004 (`build-velocity.sh` default arm) | from the next connection to that server |
|
||||||
|
| Fork with 0003 alone (`--deployed`) | at the next `systemctl restart felis-velocity` |
|
||||||
|
| Stock Velocity | never; the log line is a warning naming the server |
|
||||||
|
|
||||||
|
On the test VM (fork with 0004) labelling a server logged `legacy forwarding list is now
|
||||||
|
[legacy18,resolvecheck]` 12 s later, and removing the label logged the list back to
|
||||||
|
`[legacy18]`. The fork's own test (`FelisLegacyForwardingTest`) covers the next
|
||||||
|
connection following the rewritten list.
|
||||||
|
|
||||||
|
Legacy forwarding carries no secret. A marked server believes any identity that reaches
|
||||||
|
its game port, which `felis-allow-game-from-velocity` limits to the proxy and the node
|
||||||
|
itself; anything else running on the node can reach it too.
|
||||||
|
|
||||||
## 5. Disaster recovery
|
## 5. Disaster recovery
|
||||||
|
|
||||||
The procedures are in §16: what a database bundle holds, restoring one on the same host,
|
The procedures are in §16: what a database bundle holds, restoring one on the same host,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type ServerInfo struct {
|
|||||||
// PlayerCountUnknown is true while the operator cannot read the player
|
// PlayerCountUnknown is true while the operator cannot read the player
|
||||||
// count over RCON; idle auto-stop waits until it can.
|
// count over RCON; idle auto-stop waits until it can.
|
||||||
PlayerCountUnknown bool `json:"playerCountUnknown,omitempty"`
|
PlayerCountUnknown bool `json:"playerCountUnknown,omitempty"`
|
||||||
|
// LegacyForwarding mirrors the CR's forwarding=legacy label: the proxy
|
||||||
|
// forwards this server's players in the handshake address (#15).
|
||||||
|
LegacyForwarding bool `json:"legacyForwarding,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateServerInput is the validated, structured create-server form (spec §15).
|
// CreateServerInput is the validated, structured create-server form (spec §15).
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ func serverInfo(ms *v1alpha1.MinecraftServer) *ServerInfo {
|
|||||||
IdleStopSeconds: idleStopSeconds(ms),
|
IdleStopSeconds: idleStopSeconds(ms),
|
||||||
PlayerCountUnknown: ms.Status.Phase == v1alpha1.PhaseRunning &&
|
PlayerCountUnknown: ms.Status.Phase == v1alpha1.PhaseRunning &&
|
||||||
meta.IsStatusConditionFalse(ms.Status.Conditions, v1alpha1.ConditionPlayersCounted),
|
meta.IsStatusConditionFalse(ms.Status.Conditions, v1alpha1.ConditionPlayersCounted),
|
||||||
|
LegacyForwarding: ms.Labels[v1alpha1.LabelForwarding] == v1alpha1.ForwardingLegacy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -253,3 +254,39 @@ func TestSubdomainOf(t *testing.T) {
|
|||||||
t.Fatalf("SubdomainOf(no subdomain) = %v, want nil", got)
|
t.Fatalf("SubdomainOf(no subdomain) = %v, want nil", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The forwarding=legacy label reaches the proxy as legacyForwarding on the server
|
||||||
|
// list (#15); any other value, and no label, leaves the server on modern
|
||||||
|
// forwarding and keeps the key out of the JSON.
|
||||||
|
func TestServerListCarriesLegacyForwarding(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
if err := v1alpha1.AddToScheme(scheme); err != nil {
|
||||||
|
t.Fatalf("scheme: %v", err)
|
||||||
|
}
|
||||||
|
labeled := func(name, value string) *v1alpha1.MinecraftServer {
|
||||||
|
ms := testServer(name, name)
|
||||||
|
ms.Labels = map[string]string{v1alpha1.LabelForwarding: value}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
k := NewK8sCluster(fake.NewClientBuilder().WithScheme(scheme).WithObjects(
|
||||||
|
labeled("legacy18", "legacy"), labeled("shouty", "LEGACY"), labeled("modern", "modern"),
|
||||||
|
testServer("plain", "plain"),
|
||||||
|
).Build(), "minecraft")
|
||||||
|
infos, err := k.ListServers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListServers: %v", err)
|
||||||
|
}
|
||||||
|
for _, i := range infos {
|
||||||
|
b, err := json.Marshal(i)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := i.Name == "legacy18"
|
||||||
|
if i.LegacyForwarding != want || strings.Contains(string(b), `"legacyForwarding":true`) != want {
|
||||||
|
t.Errorf("%s: legacyForwarding = %v, JSON %s; want %v", i.Name, i.LegacyForwarding, b, want)
|
||||||
|
}
|
||||||
|
if !want && strings.Contains(string(b), "legacyForwarding") {
|
||||||
|
t.Errorf("%s: JSON carries legacyForwarding although it is off: %s", i.Name, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,22 @@ const (
|
|||||||
// LabelSystemRole identifies setup-owned system servers. Its value is the
|
// LabelSystemRole identifies setup-owned system servers. Its value is the
|
||||||
// reserved role name (for example, "login" or "lobby").
|
// reserved role name (for example, "login" or "lobby").
|
||||||
LabelSystemRole = GroupName + "/system-role"
|
LabelSystemRole = GroupName + "/system-role"
|
||||||
|
// LabelForwarding set to ForwardingLegacy makes the proxy hand this server the
|
||||||
|
// player's identity BungeeCord-style, in the handshake address, instead of the
|
||||||
|
// proxy-wide modern forwarding (#15). It is for a 1.8-era backend behind
|
||||||
|
// ViaVersion, which drops modern forwarding's login plugin message; such a
|
||||||
|
// backend trusts whatever reaches its game port, so nothing but the proxy may.
|
||||||
|
// Only the Felis-Legacy Velocity fork acts on it. A label rather than a spec
|
||||||
|
// field: the CRD needs no change, so an existing install takes it as is; the
|
||||||
|
// operator copies no CR label onto the pod, so setting it never restarts the
|
||||||
|
// server; and felis-api never writes it, so marking a server takes kubectl on
|
||||||
|
// the cluster, which fits a switch that drops the forwarding secret.
|
||||||
|
LabelForwarding = GroupName + "/forwarding"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ForwardingLegacy is the LabelForwarding value that selects legacy forwarding.
|
||||||
|
const ForwardingLegacy = "legacy"
|
||||||
|
|
||||||
// DesiredState is the operator-facing intent toggle (spec §4 spec.desiredState).
|
// DesiredState is the operator-facing intent toggle (spec §4 spec.desiredState).
|
||||||
type DesiredState string
|
type DesiredState string
|
||||||
|
|
||||||
|
|||||||
@@ -2346,6 +2346,8 @@ export interface components {
|
|||||||
idleStopSeconds: number;
|
idleStopSeconds: number;
|
||||||
/** @description Present and true while the operator cannot read the player count over RCON; idle auto-stop waits until it can. */
|
/** @description Present and true while the operator cannot read the player count over RCON; idle auto-stop waits until it can. */
|
||||||
playerCountUnknown?: boolean;
|
playerCountUnknown?: boolean;
|
||||||
|
/** @description Present and true when the CR carries the label felis.lolicon.best/forwarding=legacy. The proxy then forwards this server's players BungeeCord-style in the handshake address instead of modern forwarding (Felis-Legacy Velocity fork only). */
|
||||||
|
legacyForwarding?: boolean;
|
||||||
};
|
};
|
||||||
/** @description One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView). */
|
/** @description One row of the fleet-wide admin read (internal/api/handlers_user.go fleetServerView). */
|
||||||
FleetServer: components["schemas"]["ServerInfo"] & {
|
FleetServer: components["schemas"]["ServerInfo"] & {
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export interface ServerStatus {
|
|||||||
idleStopSeconds: number;
|
idleStopSeconds: number;
|
||||||
/** True while the operator cannot read the player count; idle stop waits. */
|
/** True while the operator cannot read the player count; idle stop waits. */
|
||||||
playerCountUnknown?: boolean;
|
playerCountUnknown?: boolean;
|
||||||
|
/** True when the CR is labelled forwarding=legacy: the proxy forwards this
|
||||||
|
* server's players BungeeCord-style (a 1.8 backend behind ViaVersion). */
|
||||||
|
legacyForwarding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WhitelistResult projects GET /servers/{name}/access/whitelist (spec §7 access).
|
/** WhitelistResult projects GET /servers/{name}/access/whitelist (spec §7 access).
|
||||||
|
|||||||
@@ -34,10 +34,19 @@ public final class ServerView {
|
|||||||
private final String endpointAddress;
|
private final String endpointAddress;
|
||||||
private final int playersOnline;
|
private final int playersOnline;
|
||||||
private final int playersMax;
|
private final int playersMax;
|
||||||
|
private final boolean legacyForwarding;
|
||||||
|
|
||||||
public ServerView(String name, String subdomain, String phase, boolean ready,
|
public ServerView(String name, String subdomain, String phase, boolean ready,
|
||||||
String autostartPolicy, String desiredState, String endpointMode,
|
String autostartPolicy, String desiredState, String endpointMode,
|
||||||
String endpointAddress, int playersOnline, int playersMax) {
|
String endpointAddress, int playersOnline, int playersMax) {
|
||||||
|
this(name, subdomain, phase, ready, autostartPolicy, desiredState, endpointMode,
|
||||||
|
endpointAddress, playersOnline, playersMax, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerView(String name, String subdomain, String phase, boolean ready,
|
||||||
|
String autostartPolicy, String desiredState, String endpointMode,
|
||||||
|
String endpointAddress, int playersOnline, int playersMax,
|
||||||
|
boolean legacyForwarding) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.subdomain = subdomain;
|
this.subdomain = subdomain;
|
||||||
this.phase = phase;
|
this.phase = phase;
|
||||||
@@ -48,6 +57,7 @@ public final class ServerView {
|
|||||||
this.endpointAddress = endpointAddress;
|
this.endpointAddress = endpointAddress;
|
||||||
this.playersOnline = playersOnline;
|
this.playersOnline = playersOnline;
|
||||||
this.playersMax = playersMax;
|
this.playersMax = playersMax;
|
||||||
|
this.legacyForwarding = legacyForwarding;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** fromJson builds a view from a parsed felis-api object, tolerating absent fields. */
|
/** fromJson builds a view from a parsed felis-api object, tolerating absent fields. */
|
||||||
@@ -62,7 +72,8 @@ public final class ServerView {
|
|||||||
str(o, "endpointMode"),
|
str(o, "endpointMode"),
|
||||||
str(o, "endpointAddress"),
|
str(o, "endpointAddress"),
|
||||||
intval(o, "playersOnline"),
|
intval(o, "playersOnline"),
|
||||||
intval(o, "playersMax"));
|
intval(o, "playersMax"),
|
||||||
|
bool(o, "legacyForwarding"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +123,7 @@ public final class ServerView {
|
|||||||
field(b, "endpointAddress", v.endpointAddress, false);
|
field(b, "endpointAddress", v.endpointAddress, false);
|
||||||
b.append(",\"playersOnline\":").append(v.playersOnline);
|
b.append(",\"playersOnline\":").append(v.playersOnline);
|
||||||
b.append(",\"playersMax\":").append(v.playersMax);
|
b.append(",\"playersMax\":").append(v.playersMax);
|
||||||
|
b.append(",\"legacyForwarding\":").append(v.legacyForwarding);
|
||||||
b.append('}');
|
b.append('}');
|
||||||
}
|
}
|
||||||
return b.append("]}").toString();
|
return b.append("]}").toString();
|
||||||
@@ -167,6 +179,15 @@ public final class ServerView {
|
|||||||
return playersMax;
|
return playersMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* legacyForwarding is true when the server's CR is labelled
|
||||||
|
* {@code felis.lolicon.best/forwarding=legacy}: the proxy forwards its players
|
||||||
|
* BungeeCord-style instead of modern forwarding ({@code LegacyForwarding}).
|
||||||
|
*/
|
||||||
|
public boolean legacyForwarding() {
|
||||||
|
return legacyForwarding;
|
||||||
|
}
|
||||||
|
|
||||||
private static String str(Map<?, ?> o, String key) {
|
private static String str(Map<?, ?> o, String key) {
|
||||||
Object v = o.get(key);
|
Object v = o.get(key);
|
||||||
return v instanceof String ? (String) v : null;
|
return v instanceof String ? (String) v : null;
|
||||||
|
|||||||
+10
-1
@@ -20,7 +20,8 @@
|
|||||||
# lasts and when it ends, the link-status outage
|
# lasts and when it ends, the link-status outage
|
||||||
# fallback fails closed outside its window, a proxy restarted during an API
|
# fallback fails closed outside its window, a proxy restarted during an API
|
||||||
# outage routes on the last saved server list (and only until a fetch
|
# outage routes on the last saved server list (and only until a fetch
|
||||||
# succeeds), /invite prompts cannot double-fire
|
# succeeds), a CR marked forwarding=legacy joins the fork's legacy-forwarding
|
||||||
|
# list above the installer's floor, /invite prompts cannot double-fire
|
||||||
# or outlive their TTL, the invite card really is a green/red clickable
|
# or outlive their TTL, the invite card really is a green/red clickable
|
||||||
# prompt, and the op-login approval card names the account and leaves its
|
# prompt, and the op-login approval card names the account and leaves its
|
||||||
# name for the admin to type. InviteCardTest and OpApprovalCardTest need the
|
# name for the admin to type. InviteCardTest and OpApprovalCardTest need the
|
||||||
@@ -127,6 +128,14 @@ javac -d "$work/list-classes" \
|
|||||||
plugins/velocity/test/best/lolicon/felis/velocity/ServerListSourceTest.java
|
plugins/velocity/test/best/lolicon/felis/velocity/ServerListSourceTest.java
|
||||||
java -cp "$work/list-classes" best.lolicon.felis.velocity.ServerListSourceTest
|
java -cp "$work/list-classes" best.lolicon.felis.velocity.ServerListSourceTest
|
||||||
|
|
||||||
|
echo "==> LegacyForwardingTest (CR-marked servers reach the fork's legacy-forwarding list, velocity)"
|
||||||
|
mkdir -p "$work/legacy-classes"
|
||||||
|
javac -d "$work/legacy-classes" \
|
||||||
|
plugins/shared/src/main/java/best/lolicon/felis/link/*.java \
|
||||||
|
plugins/velocity/src/main/java/best/lolicon/felis/velocity/LegacyForwarding.java \
|
||||||
|
plugins/velocity/test/best/lolicon/felis/velocity/LegacyForwardingTest.java
|
||||||
|
java -cp "$work/legacy-classes" best.lolicon.felis.velocity.LegacyForwardingTest
|
||||||
|
|
||||||
echo "==> ApiPoolTest (bounded felis-api pool, non-overlapping repeats, velocity)"
|
echo "==> ApiPoolTest (bounded felis-api pool, non-overlapping repeats, velocity)"
|
||||||
mkdir -p "$work/pool-classes"
|
mkdir -p "$work/pool-classes"
|
||||||
javac -d "$work/pool-classes" \
|
javac -d "$work/pool-classes" \
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ public final class FelisVelocityPlugin {
|
|||||||
private FelisApiClient apiClient;
|
private FelisApiClient apiClient;
|
||||||
private ServerRegistry registry;
|
private ServerRegistry registry;
|
||||||
private ServerListSource serverList;
|
private ServerListSource serverList;
|
||||||
|
private LegacyForwarding legacyForwarding;
|
||||||
private WaitingRouter router;
|
private WaitingRouter router;
|
||||||
private boolean onlineMode;
|
private boolean onlineMode;
|
||||||
private boolean routingActive;
|
private boolean routingActive;
|
||||||
@@ -158,6 +159,8 @@ public final class FelisVelocityPlugin {
|
|||||||
this.apiClient = new FelisApiClient(config.linkConfig());
|
this.apiClient = new FelisApiClient(config.linkConfig());
|
||||||
this.registry = new ServerRegistry(proxy, logger, config.rootDomain());
|
this.registry = new ServerRegistry(proxy, logger, config.rootDomain());
|
||||||
this.serverList = new ServerListSource(apiClient::listServers, dataDirectory.resolve(SERVER_LIST_FILE));
|
this.serverList = new ServerListSource(apiClient::listServers, dataDirectory.resolve(SERVER_LIST_FILE));
|
||||||
|
this.legacyForwarding = new LegacyForwarding(System.getProperty(LegacyForwarding.PROPERTY),
|
||||||
|
LegacyForwarding.detect(proxy.getClass().getClassLoader()));
|
||||||
this.router = new WaitingRouter(proxy, logger, apiClient, registry, this,
|
this.router = new WaitingRouter(proxy, logger, apiClient, registry, this,
|
||||||
config.loginServer(), config.lobbyServer());
|
config.loginServer(), config.lobbyServer());
|
||||||
MotdResponder motd = new MotdResponder(registry);
|
MotdResponder motd = new MotdResponder(registry);
|
||||||
@@ -178,6 +181,9 @@ public final class FelisVelocityPlugin {
|
|||||||
repeating(REGISTRATION_REFRESH, this::refreshRegistrations);
|
repeating(REGISTRATION_REFRESH, this::refreshRegistrations);
|
||||||
repeating(WAIT_POLL, router::tick);
|
repeating(WAIT_POLL, router::tick);
|
||||||
repeating(STATS_INTERVAL, this::logStats);
|
repeating(STATS_INTERVAL, this::logStats);
|
||||||
|
// Velocity accepts no connection until this handler returns, so the first
|
||||||
|
// refresh above reached even a fork that reads the legacy list only once.
|
||||||
|
legacyForwarding.accepting();
|
||||||
|
|
||||||
this.routingActive = true;
|
this.routingActive = true;
|
||||||
logger.info("Felis routing ready: rootDomain={}, login={}, lobby={}. /link, /felis and /invite registered.",
|
logger.info("Felis routing ready: rootDomain={}, login={}, lobby={}. /link, /felis and /invite registered.",
|
||||||
@@ -285,6 +291,14 @@ public final class FelisVelocityPlugin {
|
|||||||
}
|
}
|
||||||
ServerListSource.Result r = serverList.next();
|
ServerListSource.Result r = serverList.next();
|
||||||
if (r.servers != null) {
|
if (r.servers != null) {
|
||||||
|
// The forwarding mode goes first, so a newly registered legacy backend is
|
||||||
|
// never reachable under modern forwarding.
|
||||||
|
LegacyForwarding.Update lf = legacyForwarding.apply(r.servers);
|
||||||
|
if (lf != null && lf.warning) {
|
||||||
|
logger.warn(lf.message);
|
||||||
|
} else if (lf != null) {
|
||||||
|
logger.info(lf.message);
|
||||||
|
}
|
||||||
registry.refresh(r.servers);
|
registry.refresh(r.servers);
|
||||||
}
|
}
|
||||||
if (r.failure == null) {
|
if (r.failure == null) {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package best.lolicon.felis.velocity;
|
||||||
|
|
||||||
|
import best.lolicon.felis.link.ServerView;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LegacyForwarding keeps the Felis-Legacy fork's per-server legacy-forwarding list, the
|
||||||
|
* system property {@value #PROPERTY}, in step with felis-api's server list (#15). A
|
||||||
|
* server whose CR carries {@code felis.lolicon.best/forwarding=legacy} joins the list,
|
||||||
|
* and leaves it when the label goes. The value the proxy was started with (bootstrap's
|
||||||
|
* {@code -D}, {@code legacy18} by default) is a floor that always stays in, so an
|
||||||
|
* install upgraded without labelling its CRs forwards exactly as before.
|
||||||
|
*
|
||||||
|
* <p>The fork reads the property itself; this class only rewrites it. What a rewrite
|
||||||
|
* achieves depends on the proxy build, which {@link #detect(ClassLoader)} tells apart:
|
||||||
|
* <ul>
|
||||||
|
* <li>stock Velocity has one proxy-wide mode and never reads the property, so a
|
||||||
|
* marked server keeps modern forwarding;</li>
|
||||||
|
* <li>a fork carrying patch 0003 alone parses the property once, when the first
|
||||||
|
* player reaches a backend. The first rewrite runs inside ProxyInitializeEvent,
|
||||||
|
* which Velocity waits for before it accepts a connection, so that one lands;
|
||||||
|
* a later change needs a proxy restart;</li>
|
||||||
|
* <li>a fork carrying 0004 as well re-reads the property on every backend
|
||||||
|
* connection, so a change applies from the next one.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
final class LegacyForwarding {
|
||||||
|
static final String PROPERTY = "felis.legacy-forwarding.servers";
|
||||||
|
static final String CONNECTION_CLASS = "com.velocitypowered.proxy.connection.backend.VelocityServerConnection";
|
||||||
|
|
||||||
|
/** Proxy is what the running Velocity build does with {@link #PROPERTY}. */
|
||||||
|
enum Proxy {
|
||||||
|
STOCK,
|
||||||
|
FORK_READS_ONCE,
|
||||||
|
FORK_LIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update is what one {@link #apply} changed, worded for the proxy log. */
|
||||||
|
static final class Update {
|
||||||
|
final String value;
|
||||||
|
final List<String> added;
|
||||||
|
final List<String> removed;
|
||||||
|
/** True when the running proxy forwards by the new list from the next connection. */
|
||||||
|
final boolean effective;
|
||||||
|
/** True when the change leaves a server forwarded other than its CR asks. */
|
||||||
|
final boolean warning;
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
private Update(String value, List<String> added, List<String> removed, Proxy proxy, boolean effective) {
|
||||||
|
this.value = value;
|
||||||
|
this.added = added;
|
||||||
|
this.removed = removed;
|
||||||
|
this.effective = effective;
|
||||||
|
String change = "legacy forwarding list is now [" + value + "] (added " + added + ", removed " + removed + ")";
|
||||||
|
if (effective) {
|
||||||
|
this.warning = false;
|
||||||
|
this.message = "Felis: " + change + ".";
|
||||||
|
} else if (proxy == Proxy.FORK_READS_ONCE) {
|
||||||
|
this.warning = true;
|
||||||
|
this.message = "Felis: " + change + ", but this Felis-Legacy build read the list when the first player "
|
||||||
|
+ "reached a backend and keeps it until it restarts. Restart felis-velocity to apply it.";
|
||||||
|
} else {
|
||||||
|
this.warning = !added.isEmpty();
|
||||||
|
this.message = "Felis: " + change + ". This proxy is stock Velocity, which forwards every backend the "
|
||||||
|
+ "same way and ignores the list"
|
||||||
|
+ (added.isEmpty() ? "." : ": " + added + " keep modern forwarding until felis-velocity runs "
|
||||||
|
+ "the Felis-Legacy fork.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Set<String> floor;
|
||||||
|
private final Proxy proxy;
|
||||||
|
private Set<String> current;
|
||||||
|
private boolean accepting;
|
||||||
|
|
||||||
|
LegacyForwarding(String startupValue, Proxy proxy) {
|
||||||
|
this.floor = parse(startupValue);
|
||||||
|
this.current = floor;
|
||||||
|
this.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* accepting marks the end of ProxyInitializeEvent: from here on a fork that reads
|
||||||
|
* the list once may already have read it.
|
||||||
|
*/
|
||||||
|
synchronized void accepting() {
|
||||||
|
accepting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apply rewrites the property for a server list felis-api answered with (or the
|
||||||
|
* saved one restored at startup), and returns what changed, or null when nothing did.
|
||||||
|
*/
|
||||||
|
synchronized Update apply(Collection<ServerView> servers) {
|
||||||
|
Set<String> next = new TreeSet<>(floor);
|
||||||
|
for (ServerView v : servers) {
|
||||||
|
if (v.legacyForwarding() && v.name() != null && !v.name().isEmpty()) {
|
||||||
|
next.add(v.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (next.equals(current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> added = new ArrayList<>();
|
||||||
|
for (String n : next) {
|
||||||
|
if (!current.contains(n)) {
|
||||||
|
added.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> removed = new ArrayList<>();
|
||||||
|
for (String n : current) {
|
||||||
|
if (!next.contains(n)) {
|
||||||
|
removed.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String value = String.join(",", next);
|
||||||
|
System.setProperty(PROPERTY, value);
|
||||||
|
current = next;
|
||||||
|
boolean effective = proxy == Proxy.FORK_LIVE || (proxy == Proxy.FORK_READS_ONCE && !accepting);
|
||||||
|
return new Update(value, added, removed, proxy, effective);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** parse reads the property's comma-separated form the way the fork does. */
|
||||||
|
static Set<String> parse(String value) {
|
||||||
|
Set<String> out = new TreeSet<>();
|
||||||
|
if (value == null) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (String s : value.split(",")) {
|
||||||
|
String t = s.trim();
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
out.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Proxy detect(ClassLoader loader) {
|
||||||
|
return detect(loader, CONNECTION_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* detect looks for the methods the fork patches add to the backend connection class.
|
||||||
|
* The class is loaded without being initialized: initializing it would run a 0003-only
|
||||||
|
* fork's static parse of the property before {@link #apply} had rewritten it.
|
||||||
|
*/
|
||||||
|
static Proxy detect(ClassLoader loader, String className) {
|
||||||
|
boolean perServer = false;
|
||||||
|
boolean live = false;
|
||||||
|
try {
|
||||||
|
Class<?> c = Class.forName(className, false, loader);
|
||||||
|
for (Method m : c.getDeclaredMethods()) {
|
||||||
|
perServer |= m.getName().equals("effectiveForwardingMode");
|
||||||
|
live |= m.getName().equals("felisLegacyForwardingServers");
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | LinkageError e) {
|
||||||
|
return Proxy.STOCK;
|
||||||
|
}
|
||||||
|
if (live && perServer) {
|
||||||
|
return Proxy.FORK_LIVE;
|
||||||
|
}
|
||||||
|
return perServer ? Proxy.FORK_READS_ONCE : Proxy.STOCK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package best.lolicon.felis.velocity;
|
||||||
|
|
||||||
|
import best.lolicon.felis.link.ServerView;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LegacyForwardingTest covers how a CR's forwarding=legacy label reaches the fork's
|
||||||
|
* {@code felis.legacy-forwarding.servers} property (#15): marked servers join and leave
|
||||||
|
* it, the startup value stays in as a floor, an unchanged list rewrites nothing, and the
|
||||||
|
* report says whether the running proxy acts on the change (stock, a fork that reads
|
||||||
|
* the list once, a fork that re-reads it). Detection must not initialize the proxy's
|
||||||
|
* connection class. Framework free: a failed assertion throws and the process exits
|
||||||
|
* non-zero.
|
||||||
|
*
|
||||||
|
* <p>Run: {@code javac -d <out> shared/src/main/java/best/lolicon/felis/link/*.java
|
||||||
|
* velocity/src/main/java/best/lolicon/felis/velocity/LegacyForwarding.java
|
||||||
|
* velocity/test/best/lolicon/felis/velocity/LegacyForwardingTest.java && java -cp <out>
|
||||||
|
* best.lolicon.felis.velocity.LegacyForwardingTest}.
|
||||||
|
*/
|
||||||
|
public final class LegacyForwardingTest {
|
||||||
|
|
||||||
|
private static int checks;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
markedServersJoinAndLeaveAboveTheFloor();
|
||||||
|
anUnchangedListRewritesNothing();
|
||||||
|
aForkThatReadsOnceTakesOnlyTheStartupList();
|
||||||
|
stockVelocityReportsMarkedServersAsIgnored();
|
||||||
|
detectionTellsTheBuildsApartWithoutInitializing();
|
||||||
|
System.out.println("LegacyForwardingTest OK (" + checks + " checks)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markedServersJoinAndLeaveAboveTheFloor() {
|
||||||
|
System.setProperty(LegacyForwarding.PROPERTY, " legacy18 ,");
|
||||||
|
LegacyForwarding lf = new LegacyForwarding(System.getProperty(LegacyForwarding.PROPERTY),
|
||||||
|
LegacyForwarding.Proxy.FORK_LIVE);
|
||||||
|
|
||||||
|
LegacyForwarding.Update u = lf.apply(List.of(view("beta", true), view("modern", false), view("alpha", true)));
|
||||||
|
assertEq("marked servers join the floor, sorted", "alpha,beta,legacy18", System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
assertEq("update carries the value", "alpha,beta,legacy18", u.value);
|
||||||
|
assertEq("added", List.of("alpha", "beta"), u.added);
|
||||||
|
assertEq("nothing removed", List.of(), u.removed);
|
||||||
|
assertEq("a live fork acts on it", true, u.effective);
|
||||||
|
assertEq("no warning", false, u.warning);
|
||||||
|
|
||||||
|
// beta loses its label; the floor's own server is listed unmarked and stays in.
|
||||||
|
u = lf.apply(List.of(view("alpha", true), view("beta", false), view("legacy18", false)));
|
||||||
|
assertEq("an unlabelled server leaves, the floor stays", "alpha,legacy18",
|
||||||
|
System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
assertEq("removed", List.of("beta"), u.removed);
|
||||||
|
assertEq("nothing added", List.of(), u.added);
|
||||||
|
|
||||||
|
u = lf.apply(List.of());
|
||||||
|
assertEq("an empty list falls back to the floor", "legacy18", System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
assertEq("removed alpha", List.of("alpha"), u.removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void anUnchangedListRewritesNothing() {
|
||||||
|
System.setProperty(LegacyForwarding.PROPERTY, "legacy18");
|
||||||
|
LegacyForwarding lf = new LegacyForwarding("legacy18", LegacyForwarding.Proxy.FORK_LIVE);
|
||||||
|
assertEq("floor alone is no change", null, lf.apply(List.of(view("modern", false))));
|
||||||
|
|
||||||
|
lf.apply(List.of(view("old", true)));
|
||||||
|
System.setProperty(LegacyForwarding.PROPERTY, "sentinel");
|
||||||
|
assertEq("same marks again is no change", null, lf.apply(List.of(view("old", true), view("modern", false))));
|
||||||
|
assertEq("the property is left alone", "sentinel", System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
|
||||||
|
System.clearProperty(LegacyForwarding.PROPERTY);
|
||||||
|
LegacyForwarding none = new LegacyForwarding(null, LegacyForwarding.Proxy.FORK_LIVE);
|
||||||
|
assertEq("no floor and no marks is no change", null, none.apply(List.of(view("modern", false))));
|
||||||
|
assertEq("the property stays unset", null, System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
assertEq("a nameless marked entry is skipped", null, none.apply(List.of(view(null, true), view("", true))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void aForkThatReadsOnceTakesOnlyTheStartupList() {
|
||||||
|
LegacyForwarding lf = new LegacyForwarding("legacy18", LegacyForwarding.Proxy.FORK_READS_ONCE);
|
||||||
|
LegacyForwarding.Update u = lf.apply(List.of(view("old", true)));
|
||||||
|
assertEq("before accepting, the rewrite lands", true, u.effective);
|
||||||
|
assertEq("no warning before accepting", false, u.warning);
|
||||||
|
|
||||||
|
lf.accepting();
|
||||||
|
u = lf.apply(List.of(view("old", true), view("older", true)));
|
||||||
|
assertEq("the property still follows the CRs", "legacy18,old,older", System.getProperty(LegacyForwarding.PROPERTY));
|
||||||
|
assertEq("after accepting, it waits for a restart", false, u.effective);
|
||||||
|
assertEq("which is a warning", true, u.warning);
|
||||||
|
assertEq("naming the restart", true, u.message.contains("Restart felis-velocity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stockVelocityReportsMarkedServersAsIgnored() {
|
||||||
|
LegacyForwarding lf = new LegacyForwarding("", LegacyForwarding.Proxy.STOCK);
|
||||||
|
LegacyForwarding.Update u = lf.apply(List.of(view("old", true)));
|
||||||
|
assertEq("stock never acts on it", false, u.effective);
|
||||||
|
assertEq("a marked server on stock is a warning", true, u.warning);
|
||||||
|
assertEq("naming the server and the fork", true,
|
||||||
|
u.message.contains("[old] keep modern forwarding") && u.message.contains("Felis-Legacy fork"));
|
||||||
|
|
||||||
|
u = lf.apply(List.of(view("old", false)));
|
||||||
|
assertEq("dropping a mark on stock is no warning", false, u.warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void detectionTellsTheBuildsApartWithoutInitializing() {
|
||||||
|
ClassLoader cl = LegacyForwardingTest.class.getClassLoader();
|
||||||
|
String p = LegacyForwardingTest.class.getName() + "$";
|
||||||
|
assertEq("no connection class", LegacyForwarding.Proxy.STOCK, LegacyForwarding.detect(cl, p + "Absent"));
|
||||||
|
assertEq("the real name is absent here too", LegacyForwarding.Proxy.STOCK, LegacyForwarding.detect(cl));
|
||||||
|
assertEq("stock connection", LegacyForwarding.Proxy.STOCK, LegacyForwarding.detect(cl, p + "StockConnection"));
|
||||||
|
assertEq("0003 alone", LegacyForwarding.Proxy.FORK_READS_ONCE, LegacyForwarding.detect(cl, p + "ReadsOnceConnection"));
|
||||||
|
assertEq("0003 + 0004", LegacyForwarding.Proxy.FORK_LIVE, LegacyForwarding.detect(cl, p + "LiveConnection"));
|
||||||
|
assertEq("the reload hook alone is not the fork", LegacyForwarding.Proxy.STOCK,
|
||||||
|
LegacyForwarding.detect(cl, p + "HookOnlyConnection"));
|
||||||
|
assertEq("no connection class was initialized", 0, initialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- fixtures ----
|
||||||
|
|
||||||
|
// Each fixture class counts its own initialization; detect must leave it at zero.
|
||||||
|
static int initialized;
|
||||||
|
|
||||||
|
static final class StockConnection {
|
||||||
|
static {
|
||||||
|
initialized++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object startHandshake() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class ReadsOnceConnection {
|
||||||
|
static {
|
||||||
|
initialized++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object effectiveForwardingMode() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class LiveConnection {
|
||||||
|
static {
|
||||||
|
initialized++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object effectiveForwardingMode() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static java.util.Set<String> felisLegacyForwardingServers() {
|
||||||
|
return java.util.Set.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class HookOnlyConnection {
|
||||||
|
static {
|
||||||
|
initialized++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static java.util.Set<String> felisLegacyForwardingServers() {
|
||||||
|
return java.util.Set.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerView view(String name, boolean legacy) {
|
||||||
|
return new ServerView(name, name, "Running", true, null, "Running", "ClusterIP", "10.43.0.9:25565", 0, 20, legacy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertEq(String what, Object want, Object got) {
|
||||||
|
if (want == null ? got != null : !want.equals(got)) {
|
||||||
|
throw new AssertionError(what + ": want " + want + ", got " + got);
|
||||||
|
}
|
||||||
|
checks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ public final class ServerListSourceTest {
|
|||||||
theSavedListIsHandedOutOnce(dir.resolve("c"));
|
theSavedListIsHandedOutOnce(dir.resolve("c"));
|
||||||
noSavedListMeansNothingToRestore(dir.resolve("d"));
|
noSavedListMeansNothingToRestore(dir.resolve("d"));
|
||||||
aMalformedSavedListIsReportedAndIgnored(dir.resolve("e"));
|
aMalformedSavedListIsReportedAndIgnored(dir.resolve("e"));
|
||||||
|
theLegacyForwardingMarkIsSaved(dir.resolve("f"));
|
||||||
theApiEnvelopeParses();
|
theApiEnvelopeParses();
|
||||||
} finally {
|
} finally {
|
||||||
try (Stream<Path> walk = Files.walk(dir)) {
|
try (Stream<Path> walk = Files.walk(dir)) {
|
||||||
@@ -148,15 +149,32 @@ public final class ServerListSourceTest {
|
|||||||
assertEq("rewritten", true, Files.readString(file, StandardCharsets.UTF_8).contains("\"10.43.0.17:25565\""));
|
assertEq("rewritten", true, Files.readString(file, StandardCharsets.UTF_8).contains("\"10.43.0.17:25565\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A proxy restarted during an outage must keep forwarding a marked 1.8 backend the
|
||||||
|
// legacy way (#15), so the mark rides the saved list like every other field.
|
||||||
|
private static void theLegacyForwardingMarkIsSaved(Path dir) throws Exception {
|
||||||
|
Path file = fresh(dir);
|
||||||
|
ServerView legacy = new ServerView("legacy18", "old", "Running", true,
|
||||||
|
null, "Running", "ClusterIP", "10.43.7.1:25565", 0, 20, true);
|
||||||
|
answer = List.of(legacy, SURVIVAL);
|
||||||
|
new ServerListSource(ServerListSourceTest::fetch, file).next();
|
||||||
|
answer = null;
|
||||||
|
ServerListSource.Result r = new ServerListSource(ServerListSourceTest::fetch, file).next();
|
||||||
|
assertEq("restored", true, r.restored);
|
||||||
|
assertEq("the mark survives", true, r.servers.get(0).legacyForwarding());
|
||||||
|
assertEq("an unmarked server stays unmarked", false, r.servers.get(1).legacyForwarding());
|
||||||
|
}
|
||||||
|
|
||||||
// The exact shape GET /api/v1/servers answers with, extra fields included.
|
// The exact shape GET /api/v1/servers answers with, extra fields included.
|
||||||
private static void theApiEnvelopeParses() {
|
private static void theApiEnvelopeParses() {
|
||||||
List<ServerView> v = ServerView.listFromJson("{\"servers\":[{\"name\":\"lobby\",\"subdomain\":\"\","
|
List<ServerView> v = ServerView.listFromJson("{\"servers\":[{\"name\":\"lobby\",\"subdomain\":\"\","
|
||||||
+ "\"phase\":\"Running\",\"ready\":true,\"desiredState\":\"Running\",\"endpointMode\":\"ClusterIP\","
|
+ "\"phase\":\"Running\",\"ready\":true,\"desiredState\":\"Running\",\"endpointMode\":\"ClusterIP\","
|
||||||
+ "\"endpointAddress\":\"10.43.1.5:25565\",\"playersOnline\":1,\"playersMax\":100,"
|
+ "\"endpointAddress\":\"10.43.1.5:25565\",\"playersOnline\":1,\"playersMax\":100,"
|
||||||
+ "\"owner\":\"x\"},\"junk\"]}");
|
+ "\"owner\":\"x\"},\"junk\",{\"name\":\"legacy18\",\"legacyForwarding\":true}]}");
|
||||||
assertEq("non-object entries skipped", 1, v.size());
|
assertEq("non-object entries skipped", 2, v.size());
|
||||||
assertEq("lobby name", "lobby", v.get(0).name());
|
assertEq("lobby name", "lobby", v.get(0).name());
|
||||||
assertEq("lobby max", 100, v.get(0).playersMax());
|
assertEq("lobby max", 100, v.get(0).playersMax());
|
||||||
|
assertEq("an absent mark is false", false, v.get(0).legacyForwarding());
|
||||||
|
assertEq("the mark parses", true, v.get(1).legacyForwarding());
|
||||||
try {
|
try {
|
||||||
ServerView.listFromJson("[]");
|
ServerView.listFromJson("[]");
|
||||||
} catch (IllegalArgumentException expected) {
|
} catch (IllegalArgumentException expected) {
|
||||||
|
|||||||
Reference in new issue
Block a user