diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac9c8d2b..334f8259 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,8 +18,8 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: java-version: "17" distribution: "adopt" diff --git a/README-JP.md b/README-JP.md index f62ffa7a..2ebcc728 100644 --- a/README-JP.md +++ b/README-JP.md @@ -16,6 +16,7 @@ Pluginは [Releases](https://github.com/AzisabaNetwork/Kuvel/releases/latest) からダウンロードできます。 `Kuvel.jar` をダウンロードしVelocityに導入してください。ダウンロード後、コンフィグの設定を行ってください。 ```yml +namespace: "" redis: group-name: "develop" # Redisサーバーが同じかつgroup-nameが同じサーバー間でのみ名前同期が行われます connection: @@ -29,6 +30,14 @@ label-selectors: - "kuvel.azisaba.net/enable-server-discovery=true" ``` +環境変数を指定してKuvelを設定することもできます。環境変数はconfig.ymlよりも優先され、以下の項目が設定可能です: +- `KUVEL_NAMESPACE` +- `KUVEL_REDIS_GROUPNAME` +- `KUVEL_REDIS_CONNECTION_HOSTNAME` +- `KUVEL_REDIS_CONNECTION_PORT` +- `KUVEL_REDIS_CONNECTION_USERNAME` +- `KUVEL_REDIS_CONNECTION_PASSWORD` + Kuvelがサーバーを監視するためには、Kubernetesに対して権限を要求しなければなりません。VelocityのPodに対してPodとReplicaSetのget/list/watchを許可してください ```yml @@ -167,4 +176,4 @@ Kubernetesクラスター内ではPodがほぼ同時に作成されることが ## ライセンス -[GNU General Public License v3.0](LICENSE) \ No newline at end of file +[GNU General Public License v3.0](LICENSE) diff --git a/README.md b/README.md index fd0678ad..2fb2230a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ from [Releases](https://github.com/AzisabaNetwork/Kuvel/releases/latest). Downlo install it into Velocity plugins directory. The config file requires initial setup as seen below. ```yml +# The kubernetes namespace to use for the server discovery. +namespace: "" # Server name synchronization by Redis is required in load-balanced environments using multiple Velocity instances. redis: group-name: "develop" @@ -33,6 +35,10 @@ label-selectors: - "kuvel.azisaba.net/enable-server-discovery=true" ``` +Alternatively you can use environment variables to configure Kuvel. The environment variable will override + the config.yml and are `KUVEL_NAMESPACE`, `KUVEL_REDIS_GROUPNAME`, `KUVEL_REDIS_CONNECTION_HOSTNAME`, +`KUVEL_REDIS_CONNECTION_PORT`, `KUVEL_REDIS_CONNECTION_USERNAME`, and `KUVEL_REDIS_CONNECTION_PASSWORD`. + In order for Kuvel to monitor the server, you must request permission from Kubernetes. For Velocity pods, please allow get/list/watch to Pods and ReplicaSets. diff --git a/pom.xml b/pom.xml index b4723bd0..c6201bc1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ net.azisaba Kuvel - 3.0.0-rc.2 + 3.0.0-rc3 jar ${project.artifactId} @@ -32,7 +32,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.13.0 ${java.version} ${java.version} @@ -41,7 +41,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.4.1 + 3.6.0 package @@ -74,28 +74,34 @@ com.velocitypowered velocity-api - 3.1.0 + 3.1.1 provided + + io.fabric8 + kubernetes-client-api + 6.13.3 + io.fabric8 kubernetes-client - 6.3.1 + 6.13.3 + runtime redis.clients jedis - 4.3.1 + 5.1.4 org.apache.commons commons-lang3 - 3.12.0 + 3.16.0 org.projectlombok lombok - 1.18.26 + 1.18.34 provided diff --git a/src/main/java/net/azisaba/kuvel/Kuvel.java b/src/main/java/net/azisaba/kuvel/Kuvel.java index 6296b22f..d5c8171c 100644 --- a/src/main/java/net/azisaba/kuvel/Kuvel.java +++ b/src/main/java/net/azisaba/kuvel/Kuvel.java @@ -5,13 +5,19 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.KubernetesClientBuilder; -import java.util.Map.Entry; + +import java.io.File; +import java.nio.file.Path; +import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; + +import io.fabric8.kubernetes.client.KubernetesClientBuilder; import lombok.Getter; import net.azisaba.kuvel.config.KuvelConfig; import net.azisaba.kuvel.discovery.impl.redis.RedisLoadBalancerDiscovery; @@ -25,7 +31,7 @@ @Plugin( id = "kuvel", name = "Kuvel", - version = "3.0.0-rc", + version = "3.0.0-rc2", url = "https://github.com/AzisabaNetwork/Kuvel", description = "Server-discovery Velocity plugin for Minecraft servers running in a Kubernetes cluster.", @@ -35,6 +41,7 @@ public class Kuvel { private final ProxyServer proxy; private final Logger logger; + private final File dataDirectory; private KubernetesClient client; private KuvelServiceHandler kuvelServiceHandler; @@ -45,9 +52,10 @@ public class Kuvel { private KuvelConfig kuvelConfig; @Inject - public Kuvel(ProxyServer server, Logger logger) { + public Kuvel(ProxyServer server, org.slf4j.Logger logger, @DataDirectory Path dataDirectory) { this.proxy = server; this.logger = logger; + this.dataDirectory = dataDirectory.toFile(); } @Subscribe @@ -64,8 +72,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { try { kuvelConfig.load(); } catch (Exception e) { - logger.severe("Failed to load config file. Plugin feature will be disabled."); - e.printStackTrace(); + logger.error("Failed to load config file. Plugin feature will be disabled.", e); return; } @@ -75,11 +82,11 @@ public void onProxyInitialization(ProxyInitializeEvent event) { } getLogger().info("Loaded " + kuvelConfig.getLabelSelectors().size() + " selectors:"); - for (Entry entry : kuvelConfig.getLabelSelectors().entrySet()) { + for (Map.Entry entry : kuvelConfig.getLabelSelectors().entrySet()) { getLogger().info(" - " + entry.getKey() + ": " + entry.getValue()); } - kuvelServiceHandler = new KuvelServiceHandler(this, client); + kuvelServiceHandler = new KuvelServiceHandler(this, client, kuvelConfig.getNamespace()); Objects.requireNonNull(kuvelConfig.getRedisConnectionData()); Objects.requireNonNull(kuvelConfig.getProxyGroupName()); @@ -105,6 +112,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { new RedisLoadBalancerDiscovery( client, this, + kuvelConfig.getNamespace(), kuvelConfig.getRedisConnectionData().createJedisPool(), kuvelConfig.getProxyGroupName(), redisConnectionLeader, @@ -114,6 +122,7 @@ public void onProxyInitialization(ProxyInitializeEvent event) { new RedisServerDiscovery( client, this, + kuvelConfig.getNamespace(), kuvelConfig.getRedisConnectionData().createJedisPool(), kuvelConfig.getProxyGroupName(), redisConnectionLeader, diff --git a/src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java b/src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java index e72dac80..5f3905a2 100644 --- a/src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java +++ b/src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java @@ -6,8 +6,6 @@ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; -import io.fabric8.kubernetes.client.dsl.PodResource; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashMap; @@ -16,6 +14,8 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; + +import io.fabric8.kubernetes.client.dsl.PodResource; import lombok.Getter; import lombok.RequiredArgsConstructor; import net.azisaba.kuvel.discovery.LoadBalancerDiscovery; @@ -30,6 +30,7 @@ public class KuvelServiceHandler { private final Kuvel plugin; private final KubernetesClient client; + private final String namespace; private final HashMap loadBalancerServerMap = new HashMap<>(); private final UidAndServerNameMap podUidAndServerNameMap = new UidAndServerNameMap(); @@ -128,7 +129,7 @@ public Optional getLoadBalancer(String serverName) { private void updateLoadBalancerEndpoints(LoadBalancer loadBalancer) { // TODO: This may be replaced by more improved function FilterWatchListDeletable request = client.pods() - .inAnyNamespace(); + .inNamespace(namespace); for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); @@ -264,7 +265,7 @@ public boolean registerPod(Pod pod, String serverName) { */ public void registerPod(String podUid, String serverName) { FilterWatchListDeletable request = client.pods() - .inAnyNamespace(); + .inNamespace(namespace); for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); diff --git a/src/main/java/net/azisaba/kuvel/config/KuvelConfig.java b/src/main/java/net/azisaba/kuvel/config/KuvelConfig.java index 4aa2f90d..5bcf49a3 100644 --- a/src/main/java/net/azisaba/kuvel/config/KuvelConfig.java +++ b/src/main/java/net/azisaba/kuvel/config/KuvelConfig.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Locale; +import java.util.Map; import javax.annotation.Nullable; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -16,8 +17,9 @@ public class KuvelConfig { private final Kuvel plugin; - private static final String CONFIG_FILE_PATH = "./plugins/Kuvel/config.yml"; + private static final String CONFIG_FILE_NAME = "config.yml"; + @Nullable private String namespace; private boolean redisEnabled; @Nullable private RedisConnectionData redisConnectionData; @@ -26,25 +28,54 @@ public class KuvelConfig { private final HashMap labelSelectors = new HashMap<>(); public void load() throws IOException { - VelocityConfigLoader conf = VelocityConfigLoader.load(new File(CONFIG_FILE_PATH)); + File uppercaseDataFolder = new File(plugin.getDataDirectory().getParentFile(), "Kuvel"); + if (uppercaseDataFolder.exists() && !plugin.getDataDirectory().exists()) { + if (uppercaseDataFolder.renameTo(plugin.getDataDirectory())) { + plugin + .getLogger() + .info( + "Successfully renamed the data folder to use a lowercase name."); + } else { + plugin + .getLogger() + .warn( + "Failed to rename the data folder to be lowercase. Please manually rename the data folder to 'kuvel'."); + } + } + + VelocityConfigLoader conf = VelocityConfigLoader.load(new File(plugin.getDataDirectory(), CONFIG_FILE_NAME)); conf.saveDefaultConfig(); - String hostname = conf.getString("redis.connection.hostname"); + Map env = System.getenv(); + + namespace = env.getOrDefault("KUVEL_NAMESPACE", conf.getString("namespace", null)); + + String hostname = env.getOrDefault("KUVEL_REDIS_CONNECTION_HOSTNAME", conf.getString("redis.connection.hostname")); int port = conf.getInt("redis.connection.port", -1); - String username = conf.getString("redis.connection.username"); - String password = conf.getString("redis.connection.password"); + if (env.containsKey("KUVEL_REDIS_CONNECTION_PORT")) { + try { + port = Integer.parseInt(env.get("KUVEL_REDIS_CONNECTION_PORT")); + } catch (NumberFormatException e) { + plugin + .getLogger() + .warn( + "Invalid port number for Redis connection specified in KUVEL_REDIS_CONNECTION_PORT environment variable. Using port " + port + " from config.yml."); + } + } + String username = env.getOrDefault("KUVEL_REDIS_CONNECTION_USERNAME", conf.getString("redis.connection.username")); + String password = env.getOrDefault("KUVEL_REDIS_CONNECTION_PASSWORD", conf.getString("redis.connection.password")); if (hostname == null || port <= 0) { redisEnabled = false; plugin .getLogger() - .warning( + .warn( "Redis is enabled, but hostname or port is invalid. Redis sync will be disabled."); } else { redisConnectionData = new RedisConnectionData(hostname, port, username, password); } - proxyGroupName = conf.getString("redis.group-name", null); + proxyGroupName = env.getOrDefault("KUVEL_REDIS_GROUPNAME", conf.getString("redis.group-name", null)); if (conf.isSet("label-selectors")) { conf.getStringList("label-selectors").forEach(s -> { diff --git a/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisLoadBalancerDiscovery.java b/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisLoadBalancerDiscovery.java index 926738f1..e63348ba 100644 --- a/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisLoadBalancerDiscovery.java +++ b/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisLoadBalancerDiscovery.java @@ -6,18 +6,18 @@ import io.fabric8.kubernetes.api.model.apps.ReplicaSet; import io.fabric8.kubernetes.api.model.apps.ReplicaSetList; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; -import io.fabric8.kubernetes.client.dsl.RollableScalableResource; import java.net.InetSocketAddress; import java.util.ArrayDeque; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; + +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.RollableScalableResource; import lombok.RequiredArgsConstructor; import net.azisaba.kuvel.Kuvel; import net.azisaba.kuvel.KuvelServiceHandler; @@ -36,6 +36,7 @@ public class RedisLoadBalancerDiscovery implements LoadBalancerDiscovery { private final KubernetesClient client; private final Kuvel plugin; + private final String namespace; private final JedisPool jedisPool; private final String groupName; private final RedisConnectionLeader redisConnectionLeader; @@ -56,9 +57,9 @@ public void start() { Runnable runnable = () -> { FilterWatchListDeletable> request = client.apps() - .replicaSets().inAnyNamespace(); + .replicaSets().inNamespace(namespace); - for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { + for (Map.Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); } @@ -242,9 +243,9 @@ public void registerLoadBalancersForStartup() { } FilterWatchListDeletable> request = client.apps() - .replicaSets().inAnyNamespace(); + .replicaSets().inNamespace(namespace); - for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { + for (Map.Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); } @@ -273,9 +274,9 @@ public void registerLoadBalancersForStartup() { private ReplicaSet getReplicaSetFromUid(String uid) { FilterWatchListDeletable> request = client.apps() - .replicaSets().inAnyNamespace(); + .replicaSets().inNamespace(namespace); - for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { + for (Map.Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); } diff --git a/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisServerDiscovery.java b/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisServerDiscovery.java index 8449f824..23fecee7 100644 --- a/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisServerDiscovery.java +++ b/src/main/java/net/azisaba/kuvel/discovery/impl/redis/RedisServerDiscovery.java @@ -4,8 +4,6 @@ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodList; import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; -import io.fabric8.kubernetes.client.dsl.PodResource; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; @@ -13,10 +11,14 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; + +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.PodResource; import lombok.RequiredArgsConstructor; import net.azisaba.kuvel.Kuvel; import net.azisaba.kuvel.KuvelServiceHandler; @@ -34,6 +36,7 @@ public class RedisServerDiscovery implements ServerDiscovery { private final KubernetesClient client; private final Kuvel plugin; + private final String namespace; private final JedisPool jedisPool; private final String groupName; private final RedisConnectionLeader redisConnectionLeader; @@ -52,7 +55,7 @@ public void start() { Runnable runnable = () -> { FilterWatchListDeletable request = client.pods() - .inAnyNamespace(); + .inNamespace(namespace); for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); @@ -115,7 +118,7 @@ public HashMap getServersForStartup() { jedis.hgetAll(RedisKeys.LOAD_BALANCERS_PREFIX.getKey() + groupName); FilterWatchListDeletable request = client.pods() - .inAnyNamespace(); + .inNamespace(namespace); for (Entry e : plugin.getKuvelConfig().getLabelSelectors().entrySet()) { request = request.withLabel(e.getKey(), e.getValue()); @@ -161,16 +164,16 @@ public HashMap getServersForStartup() { verb = "Found"; } - for (String podUid : podIdToServerNameMap.keySet()) { - plugin - .getLogger() - .info(verb + " server: " + podIdToServerNameMap.get(podUid) + " (" + podUid + ")"); - } - HashMap servers = new HashMap<>(); for (Entry entry : podIdToServerNameMap.entrySet()) { + plugin + .getLogger() + .info(verb + " server: " + entry.getValue() + " (" + entry.getKey() + ")"); Pod pod = getPodByUid(entry.getKey()); if (pod == null) { + plugin + .getLogger() + .warn("Pod " + entry.getKey() + " for server " + entry.getValue() + " not found"); continue; } diff --git a/src/main/java/net/azisaba/kuvel/redis/RedisConnectionLeader.java b/src/main/java/net/azisaba/kuvel/redis/RedisConnectionLeader.java index 0444a6f4..187784d2 100644 --- a/src/main/java/net/azisaba/kuvel/redis/RedisConnectionLeader.java +++ b/src/main/java/net/azisaba/kuvel/redis/RedisConnectionLeader.java @@ -120,6 +120,7 @@ private void runDiscoveryTask() { new RedisLoadBalancerDiscovery( plugin.getClient(), plugin, + plugin.getKuvelConfig().getNamespace(), plugin.getKuvelConfig().getRedisConnectionData().createJedisPool(), plugin.getKuvelConfig().getProxyGroupName(), this, @@ -131,6 +132,7 @@ private void runDiscoveryTask() { new RedisServerDiscovery( plugin.getClient(), plugin, + plugin.getKuvelConfig().getNamespace(), plugin.getKuvelConfig().getRedisConnectionData().createJedisPool(), plugin.getKuvelConfig().getProxyGroupName(), this, diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index a1e06117..eef9b19d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,3 +1,5 @@ +# The kubernetes namespace to use for the server discovery. +namespace: "" # Server name synchronization by Redis is required in load-balanced environments using multiple Velocity. redis: group-name: "develop"