Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,6 @@ buildNumber.properties
# JDT-specific (Eclipse Java Development Tools)
.classpath

# End of https://www.toptal.com/developers/gitignore/api/maven,intellij+all
# End of https://www.toptal.com/developers/gitignore/api/maven,intellij+all

.vscode/
17 changes: 11 additions & 6 deletions README-JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,18 @@ labelはconfigで指定します。デフォルトでは、`kuvel.azisaba.net/en

また、一部の機能を使用するために以下のlabelを使用します。

| Label名 | 値 |
|:---------------------------------------:|:-------------------:|
| kuvel.azisaba.net/preferred-server-name | Velocityに登録したいサーバー名 |
| kuvel.azisaba.net/initial-server | true / false |
| kuvel.azisaba.net/disable-name-suffix | true / false |
| kuvel.azisaba.net/disable-load-balancer | true / false |
| Label名 | 値 |
|:---------------------------------------:|:----------------------------:|
| kuvel.azisaba.net/preferred-server-name | Velocityに登録したいサーバー名 |
| kuvel.azisaba.net/initial-server | true / false |
| kuvel.azisaba.net/disable-name-suffix | true / false |
| kuvel.azisaba.net/disable-load-balancer | true / false |
| kuvel.azisaba.net/forced-host | このサーバーにルーティングするホスト名 (forced hosts) |

サーバー名が63文字を超える場合は、ラベルではなく`kuvel.azisaba.net/preferred-server-name`のアノテーションを使用してください。
Podに`kuvel.azisaba.net/disable-name-suffix=true`を設定すると、`preferred-server-name`をそのまま登録します (`-1`などのsuffixを付与しません)。複数レプリカが検出された場合はエラーを出して追加Podを登録しません。単一レプリカのPod向けの設定です。
ReplicaSetまたはDeploymentのmetadataに`kuvel.azisaba.net/disable-load-balancer=true`を設定すると、そのReplicaSet用の仮想LoadBalancerサーバーは作成されません。配下のPod自体は通常どおり登録されます。
Pod、Deployment、またはReplicaSetに`kuvel.azisaba.net/forced-host`を設定すると、そのホスト名で接続したプレイヤーは対応するサーバーにルーティングされます。これはVelocityの[forced hosts](https://docs.papermc.io/velocity/configuration/#forced-hosts-section)機能をKubernetesのラベル/アノテーションで動的に設定するものです。

### Podの場合

Expand All @@ -108,6 +110,7 @@ metadata:
kuvel.azisaba.net/preferred-server-name: "test-server" # Kuvelがサーバーの命名をするために必要
# kuvel.azisaba.net/initial-server: "true" # 初期サーバーにする場合はコメントアウトを外す
# kuvel.azisaba.net/disable-name-suffix: "true" # preferred-server-nameをそのまま登録する (単一レプリカのみ)
# kuvel.azisaba.net/forced-host: "pvp.example.com" # pvp.example.comで接続したプレイヤーをこのサーバーに送る
spec:
containers:
- name: test-server
Expand All @@ -134,6 +137,7 @@ spec:
kuvel.azisaba.net/enable-server-discovery: "true" # KuvelがMinecraftサーバーを見つけるために必要 (Configに依存)
kuvel.azisaba.net/preferred-server-name: "test-server" # Kuvelがサーバーの命名をするために必要
# kuvel.azisaba.net/initial-server: "true" # 初期サーバーにする場合はコメントアウトを外す
# kuvel.azisaba.net/forced-host: "pvp.example.com" # pvp.example.comで接続したプレイヤーをこのサーバーに送る
spec:
containers:
- name: test-server
Expand All @@ -159,6 +163,7 @@ metadata:
kuvel.azisaba.net/preferred-server-name: "lobby"
# kuvel.azisaba.net/initial-server: "true" # このロードバランサーを初期サーバーにする場合はコメントアウトを外す
# kuvel.azisaba.net/disable-load-balancer: "true" # LoadBalancerエントリだけ無効にする場合はコメントアウトを外す
# kuvel.azisaba.net/forced-host: "lobby.example.com" # lobby.example.comで接続したプレイヤーをこのロードバランサーに送る
spec:
replicas: 3
selector:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ The following labels are also used for some other features.
| kuvel.azisaba.net/initial-server | true / false |
| kuvel.azisaba.net/disable-name-suffix | true / false |
| kuvel.azisaba.net/disable-load-balancer | true / false |
| kuvel.azisaba.net/forced-host | Hostname to route to this server (forced hosts) |

If server names longer than 63 characters are desired, the `kuvel.azisaba.net/preferred-server-name` annotation can be used instead of the label.
If `kuvel.azisaba.net/disable-name-suffix=true` is set on a Pod, Kuvel will register the server name exactly as `preferred-server-name` (no `-1` suffix). If multiple replicas are detected, Kuvel logs an error and skips the extra pods. This label is intended for single-replica Pods only.
If `kuvel.azisaba.net/disable-load-balancer=true` is set on a ReplicaSet or Deployment metadata, Kuvel will skip creating the virtual load balancer server for that ReplicaSet. The backing Pods are still registered normally.
If `kuvel.azisaba.net/forced-host` is set on a Pod, Deployment, or ReplicaSet, players connecting via that hostname will be routed to the corresponding server. This mirrors Velocity's [forced hosts](https://docs.papermc.io/velocity/configuration/#forced-hosts-section) feature but is configured dynamically through Kubernetes labels/annotations.

### Pod

Expand All @@ -106,6 +108,7 @@ metadata:
kuvel.azisaba.net/preferred-server-name: "test-server" # Required for Kuvel to name the server
# kuvel.azisaba.net/initial-server: "true" # Uncomment this line if you want to make this server the initial server.
# kuvel.azisaba.net/disable-name-suffix: "true" # Use the exact preferred name (no -1 suffix); single replica only.
# kuvel.azisaba.net/forced-host: "pvp.example.com" # Players connecting via pvp.example.com will be sent to this server.
spec:
containers:
- name: test-server
Expand All @@ -132,6 +135,7 @@ spec:
kuvel.azisaba.net/enable-server-discovery: "true" # Required for Kuvel to detect Minecraft servers. Depends on your config.
kuvel.azisaba.net/preferred-server-name: "test-server" # Required for Kuvel to name the server
# kuvel.azisaba.net/initial-server: "true" # Uncomment this line if you want to make this server the initial server.
# kuvel.azisaba.net/forced-host: "pvp.example.com" # Players connecting via pvp.example.com will be sent to this server.
spec:
containers:
- name: test-server
Expand All @@ -158,6 +162,7 @@ metadata:
kuvel.azisaba.net/preferred-server-name: "lobby"
# kuvel.azisaba.net/initial-server: "true" # Uncomment this line if you want to make this load balancer server the initial server.
# kuvel.azisaba.net/disable-load-balancer: "true" # Uncomment this line to disable only the load balancer entry.
# kuvel.azisaba.net/forced-host: "lobby.example.com" # Players connecting via lobby.example.com will be sent to this load balancer.
spec:
replicas: 3
selector:
Expand Down
38 changes: 37 additions & 1 deletion src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;

Expand All @@ -38,6 +41,7 @@ public class KuvelServiceHandler {
private final UidAndServerNameMap replicaSetUidAndServerNameMap = new UidAndServerNameMap();

private final List<String> initialServerNames = new ArrayList<>();
private final Map<String, List<String>> forcedHosts = new ConcurrentHashMap<>();

private final AtomicReference<ServerDiscovery> serverDiscovery = new AtomicReference<>();
private final AtomicReference<LoadBalancerDiscovery> loadBalancerDiscovery =
Expand All @@ -59,6 +63,10 @@ public void registerLoadBalancer(LoadBalancer loadBalancer) {
initialServerNames.add(serverName);
}

if (loadBalancer.getForcedHost() != null) {
addForcedHost(loadBalancer.getForcedHost(), serverName);
}

plugin
.getLogger()
.info(
Expand Down Expand Up @@ -101,6 +109,7 @@ public void unregisterLoadBalancer(LoadBalancer loadBalancer) {
replicaSetUidAndServerNameMap.unregister(loadBalancer.getReplicaSetUid());

initialServerNames.remove(serverName);
removeForcedHost(serverName);

plugin
.getLogger()
Expand Down Expand Up @@ -244,13 +253,26 @@ public boolean registerPod(Pod pod, String serverName) {
}
}

String labelKeyPrefix = plugin.getKuvelConfig().getLabelKeyPrefix();
String initialServerStr =
pod.getMetadata().getLabels().getOrDefault(
LabelKeys.INITIAL_SERVER.getKey(plugin.getKuvelConfig().getLabelKeyPrefix()), "false");
LabelKeys.INITIAL_SERVER.getKey(labelKeyPrefix), "false");
if (Boolean.parseBoolean(initialServerStr)) {
initialServerNames.add(serverName);
}

String forcedHost =
pod.getMetadata().getLabels().getOrDefault(
LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
if (forcedHost == null) {
forcedHost =
pod.getMetadata().getAnnotations().getOrDefault(
LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
}
if (forcedHost != null) {
addForcedHost(forcedHost, serverName);
}

plugin
.getLogger()
.info("Registered server: " + serverName + " (" + pod.getMetadata().getUid() + ")");
Expand Down Expand Up @@ -315,6 +337,7 @@ public void unregisterPod(String podUid) {
}

initialServerNames.remove(serverName);
removeForcedHost(serverName);

plugin.getLogger().info("Unregistered server: " + serverName + " (" + podUid + ")");
}
Expand All @@ -337,4 +360,17 @@ public void unregisterPod(Pod pod) {
public boolean isPodRegistered(String podId) {
return podUidAndServerNameMap.getServerNameFromUid(podId) != null;
}

private void addForcedHost(String hostname, String serverName) {
forcedHosts.computeIfAbsent(hostname, k -> new CopyOnWriteArrayList<>()).add(serverName);
}

private void removeForcedHost(String serverName) {
forcedHosts.values().forEach(list -> list.remove(serverName));
forcedHosts.entrySet().removeIf(entry -> entry.getValue().isEmpty());
}

public Map<String, List<String>> getForcedHosts() {
return forcedHosts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
.getLabels()
.getOrDefault(LabelKeys.INITIAL_SERVER.getKey(labelKeyPrefix), "false")
.equalsIgnoreCase("true");
String forcedHost =
metadata
.getLabels()
.getOrDefault(LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
if (forcedHost == null) {
forcedHost =
metadata
.getAnnotations()
.getOrDefault(LabelKeys.FORCED_HOST.getKey(labelKeyPrefix), null);
}
if (isDisableLoadBalancer(metadata)) {
return;
}
Expand Down Expand Up @@ -169,7 +179,7 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
kuvelServiceHandler.getReplicaSetUidAndServerNameMap().register(uid, serverName);
jedis.hset(RedisKeys.LOAD_BALANCERS_PREFIX.getKey() + groupName, uid, serverName);

redisConnectionLeader.publishNewLoadBalancer(uid, serverName, initialServer);
redisConnectionLeader.publishNewLoadBalancer(uid, serverName, initialServer, forcedHost);

RegisteredServer server =
plugin
Expand All @@ -182,7 +192,8 @@ private void registerOrIgnore(ReplicaSet replicaSet, boolean isFetchedFromRedis)
server,
new RoundRobinLoadBalancingStrategy(),
uid,
initialServer));
initialServer,
forcedHost));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -19,6 +20,21 @@ public class ChooseInitialServerListener {

@Subscribe
public void onInitialServerChoose(PlayerChooseInitialServerEvent event) {
Optional<InetSocketAddress> virtualHost = event.getPlayer().getVirtualHost();
if (virtualHost.isPresent()) {
String hostname = virtualHost.get().getHostString();
List<String> forcedServerNames = handler.getForcedHosts().get(hostname);
if (forcedServerNames != null && !forcedServerNames.isEmpty()) {
for (String serverName : forcedServerNames) {
Optional<RegisteredServer> optionalServer = proxy.getServer(serverName);
if (optionalServer.isPresent()) {
event.setInitialServer(optionalServer.get());
return;
}
}
}
}

if (handler.getInitialServerNames().isEmpty()) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class LoadBalancer {
private final String replicaSetUid;

private final boolean isInitialServer;
private final String forcedHost;
private final List<String> endpointServers = new ArrayList<>();

public void addEndpoint(String serverName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ public void leaveLeader() {
}

public void publishNewLoadBalancer(
String replicaSetUid, String serverName, boolean initialServer) {
String replicaSetUid, String serverName, boolean initialServer, String forcedHost) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(
RedisKeys.LOAD_BALANCER_ADDED_NOTIFY_PREFIX.getKey() + groupName,
replicaSetUid + ":" + serverName + ":" + initialServer);
replicaSetUid + ":" + serverName + ":" + initialServer + ":" + (forcedHost != null ? forcedHost : ""));
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/net/azisaba/kuvel/redis/RedisSubscriber.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ public void onPMessage(String pattern, String channel, String message) {
String replicaSetUid = message.split(":")[0];
String serverName = message.split(":")[1];
boolean initialServer = Boolean.parseBoolean(message.split(":")[2]);
String forcedHost = message.split(":").length > 3 ? message.split(":")[3] : null;
if (forcedHost != null && forcedHost.isEmpty()) {
forcedHost = null;
}

RegisteredServer server =
plugin
Expand All @@ -56,7 +60,8 @@ public void onPMessage(String pattern, String channel, String message) {
server,
new RoundRobinLoadBalancingStrategy(),
replicaSetUid,
initialServer);
initialServer,
forcedHost);
kuvelServiceHandler.registerLoadBalancer(loadBalancer);
} else if (channel.startsWith(RedisKeys.POD_DELETED_NOTIFY_PREFIX.getKey())) {
kuvelServiceHandler.unregisterPod(message);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/net/azisaba/kuvel/util/LabelKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum LabelKeys {
INITIAL_SERVER("initial-server"),
DISABLE_NAME_SUFFIX("disable-name-suffix"),
DISABLE_LOAD_BALANCER("disable-load-balancer"),
FORCED_HOST("forced-host"),
;

private final String key;
Expand Down