Skip to content

Commit bb92784

Browse files
authored
Merge pull request #62 from AzisabaNetwork/copilot/add-kuvel-command
Add `/kuvel` admin command for server registration visibility, edits, and repair
2 parents cacda49 + 0ac1939 commit bb92784

5 files changed

Lines changed: 252 additions & 2 deletions

File tree

README-JP.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ Kubernetesクラスター内ではPodがほぼ同時に作成されることが
190190

191191
1.xではこの機能の有効化は任意でしたが、2.0.0以降デフォルトで有効になりました。
192192

193+
## コマンド
194+
195+
`/kuvel` コマンドで、サーバーの登録状況の確認・変更・修正ができます。
196+
197+
- `/kuvel status` - 登録状況と不足登録数を表示
198+
- `/kuvel list <pods|loadbalancers>` - 現在のUIDとサーバー名の対応一覧を表示
199+
- `/kuvel register <podUid> <serverName>` - Pod UIDを指定して登録
200+
- `/kuvel unregister <podUid>` - Pod UIDを指定して登録解除
201+
- `/kuvel setname <podUid> <serverName>` - Podの登録名を変更
202+
- `/kuvel repair` - 不足登録の再登録と壊れた対応の整理を実行
203+
193204
## ライセンス
194205

195206
[GNU General Public License v3.0](LICENSE)

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,5 +191,16 @@ issue. Kuvel uses keys whose key name begins with `kuvel:`.
191191

192192
On 1.x, this feature was optional, but from 2.0.0, this setting is enabled by default.
193193

194+
## Commands
195+
196+
Use `/kuvel` to inspect, update, and repair server registration state.
197+
198+
- `/kuvel status` - show registration summary and missing registrations
199+
- `/kuvel list <pods|loadbalancers>` - list UID to server-name mappings
200+
- `/kuvel register <podUid> <serverName>` - register a pod by UID
201+
- `/kuvel unregister <podUid>` - unregister a pod by UID
202+
- `/kuvel setname <podUid> <serverName>` - change a pod's registered server name
203+
- `/kuvel repair` - re-register missing entries and clean broken mappings
204+
194205
## License
195206
[GNU General Public License v3.0](LICENSE)

src/main/java/net/azisaba/kuvel/Kuvel.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
1919
import lombok.Getter;
2020
import net.azisaba.kuvel.config.KuvelConfig;
21+
import net.azisaba.kuvel.command.KuvelCommand;
2122
import net.azisaba.kuvel.discovery.impl.redis.RedisLoadBalancerDiscovery;
2223
import net.azisaba.kuvel.discovery.impl.redis.RedisServerDiscovery;
2324
import net.azisaba.kuvel.listener.ChooseInitialServerListener;
@@ -151,6 +152,12 @@ public void onProxyInitialization(ProxyInitializeEvent event) {
151152
proxy
152153
.getEventManager()
153154
.register(this, new ChooseInitialServerListener(proxy, kuvelServiceHandler));
155+
156+
proxy
157+
.getCommandManager()
158+
.register(
159+
proxy.getCommandManager().metaBuilder("kuvel").plugin(this).build(),
160+
new KuvelCommand(this));
154161
}
155162

156163
@Subscribe

src/main/java/net/azisaba/kuvel/KuvelServiceHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public boolean registerPod(Pod pod, String serverName) {
262262
* @param podUid The pod uid to register.
263263
* @param serverName The name of the server.
264264
*/
265-
public void registerPod(String podUid, String serverName) {
265+
public boolean registerPod(String podUid, String serverName) {
266266
FilterWatchListDeletable<Pod, PodList, PodResource> request = client.pods()
267267
.inNamespace(namespace);
268268

@@ -277,7 +277,7 @@ public void registerPod(String podUid, String serverName) {
277277
.filter(p -> p.getMetadata().getUid().equals(podUid))
278278
.findFirst();
279279

280-
pod.ifPresent(p -> registerPod(p, serverName));
280+
return pod.map(p -> registerPod(p, serverName)).orElse(false);
281281
}
282282

283283
/**
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package net.azisaba.kuvel.command;
2+
3+
import com.velocitypowered.api.command.CommandSource;
4+
import com.velocitypowered.api.command.SimpleCommand;
5+
import java.util.Map;
6+
import java.util.TreeMap;
7+
import net.azisaba.kuvel.Kuvel;
8+
import net.azisaba.kuvel.KuvelServiceHandler;
9+
import net.kyori.adventure.text.Component;
10+
11+
public class KuvelCommand implements SimpleCommand {
12+
13+
private final Kuvel plugin;
14+
15+
public KuvelCommand(Kuvel plugin) {
16+
this.plugin = plugin;
17+
}
18+
19+
@Override
20+
public void execute(Invocation invocation) {
21+
CommandSource source = invocation.source();
22+
if (!source.hasPermission("kuvel.command")) {
23+
source.sendMessage(Component.text("You don't have permission to use this command."));
24+
return;
25+
}
26+
27+
KuvelServiceHandler handler = plugin.getKuvelServiceHandler();
28+
if (handler == null) {
29+
source.sendMessage(Component.text("Kuvel is not initialized."));
30+
return;
31+
}
32+
33+
String[] args = invocation.arguments();
34+
if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
35+
sendHelp(source);
36+
return;
37+
}
38+
39+
if (args[0].equalsIgnoreCase("status")) {
40+
Map<String, String> podMap = handler.getPodUidAndServerNameMap().getAllMap();
41+
Map<String, String> replicaSetMap = handler.getReplicaSetUidAndServerNameMap().getAllMap();
42+
43+
long missingPodRegistrations =
44+
podMap.values().stream().filter(name -> plugin.getProxy().getServer(name).isEmpty()).count();
45+
long missingLoadBalancerRegistrations =
46+
replicaSetMap.values().stream()
47+
.filter(name -> plugin.getProxy().getServer(name).isEmpty())
48+
.count();
49+
50+
source.sendMessage(
51+
Component.text(
52+
"Kuvel status: leader="
53+
+ (plugin.getRedisConnectionLeader() != null
54+
&& plugin.getRedisConnectionLeader().isLeader())
55+
+ ", podMappings="
56+
+ podMap.size()
57+
+ ", loadBalancerMappings="
58+
+ replicaSetMap.size()
59+
+ ", initialServers="
60+
+ handler.getInitialServerNames().size()));
61+
source.sendMessage(
62+
Component.text(
63+
"Missing registrations: pods="
64+
+ missingPodRegistrations
65+
+ ", loadBalancers="
66+
+ missingLoadBalancerRegistrations));
67+
return;
68+
}
69+
70+
if (args[0].equalsIgnoreCase("list")) {
71+
if (args.length < 2) {
72+
source.sendMessage(Component.text("Usage: /kuvel list <pods|loadbalancers>"));
73+
return;
74+
}
75+
76+
if (args[1].equalsIgnoreCase("pods")) {
77+
TreeMap<String, String> map = new TreeMap<>(handler.getPodUidAndServerNameMap().getAllMap());
78+
source.sendMessage(Component.text("Registered pod mappings: " + map.size()));
79+
map.forEach((uid, serverName) -> source.sendMessage(Component.text("- " + uid + " -> " + serverName)));
80+
return;
81+
}
82+
83+
if (args[1].equalsIgnoreCase("loadbalancers")) {
84+
TreeMap<String, String> map =
85+
new TreeMap<>(handler.getReplicaSetUidAndServerNameMap().getAllMap());
86+
source.sendMessage(Component.text("Registered load balancer mappings: " + map.size()));
87+
map.forEach((uid, serverName) -> source.sendMessage(Component.text("- " + uid + " -> " + serverName)));
88+
return;
89+
}
90+
91+
source.sendMessage(Component.text("Usage: /kuvel list <pods|loadbalancers>"));
92+
return;
93+
}
94+
95+
if (args[0].equalsIgnoreCase("register")) {
96+
if (args.length < 3) {
97+
source.sendMessage(Component.text("Usage: /kuvel register <podUid> <serverName>"));
98+
return;
99+
}
100+
String mappedUid = handler.getPodUidAndServerNameMap().getUidFromServerName(args[2]);
101+
if (mappedUid != null && !mappedUid.equals(args[1])) {
102+
source.sendMessage(
103+
Component.text("Server name is already mapped to another pod uid: " + mappedUid));
104+
return;
105+
}
106+
if (plugin.getProxy().getServer(args[2]).isPresent()) {
107+
source.sendMessage(Component.text("Server name is already registered: " + args[2]));
108+
return;
109+
}
110+
111+
if (handler.registerPod(args[1], args[2])) {
112+
source.sendMessage(Component.text("Registered pod mapping: " + args[1] + " -> " + args[2]));
113+
} else {
114+
source.sendMessage(Component.text("Failed to register pod mapping."));
115+
}
116+
return;
117+
}
118+
119+
if (args[0].equalsIgnoreCase("unregister")) {
120+
if (args.length < 2) {
121+
source.sendMessage(Component.text("Usage: /kuvel unregister <podUid>"));
122+
return;
123+
}
124+
125+
if (!handler.isPodRegistered(args[1])) {
126+
source.sendMessage(Component.text("Pod uid is not currently registered: " + args[1]));
127+
return;
128+
}
129+
130+
handler.unregisterPod(args[1]);
131+
source.sendMessage(Component.text("Unregistered pod mapping: " + args[1]));
132+
return;
133+
}
134+
135+
if (args[0].equalsIgnoreCase("setname")) {
136+
if (args.length < 3) {
137+
source.sendMessage(Component.text("Usage: /kuvel setname <podUid> <serverName>"));
138+
return;
139+
}
140+
String oldName = handler.getPodUidAndServerNameMap().getServerNameFromUid(args[1]);
141+
if (oldName != null && args[2].equals(oldName)) {
142+
source.sendMessage(Component.text("Server name is already set for the pod uid."));
143+
return;
144+
}
145+
146+
String mappedUid = handler.getPodUidAndServerNameMap().getUidFromServerName(args[2]);
147+
if (mappedUid != null && !mappedUid.equals(args[1])) {
148+
source.sendMessage(
149+
Component.text("Server name is already mapped to another pod uid: " + mappedUid));
150+
return;
151+
}
152+
153+
if (plugin.getProxy().getServer(args[2]).isPresent() && mappedUid == null) {
154+
source.sendMessage(Component.text("Server name is already registered by another server."));
155+
return;
156+
}
157+
158+
if (oldName != null) {
159+
handler.unregisterPod(args[1]);
160+
}
161+
162+
if (handler.registerPod(args[1], args[2])) {
163+
String previousName = oldName == null ? "previously unregistered" : oldName;
164+
source.sendMessage(
165+
Component.text(
166+
"Updated pod mapping: " + args[1] + " -> " + args[2] + " (was " + previousName + ")"));
167+
} else {
168+
source.sendMessage(Component.text("Failed to update pod mapping."));
169+
}
170+
return;
171+
}
172+
173+
if (args[0].equalsIgnoreCase("repair")) {
174+
int repairedPodCount = 0;
175+
int cleanedPodCount = 0;
176+
for (Map.Entry<String, String> entry : handler.getPodUidAndServerNameMap().getAllMap().entrySet()) {
177+
if (plugin.getProxy().getServer(entry.getValue()).isPresent()) {
178+
continue;
179+
}
180+
boolean success = handler.registerPod(entry.getKey(), entry.getValue());
181+
if (success && plugin.getProxy().getServer(entry.getValue()).isPresent()) {
182+
repairedPodCount++;
183+
} else {
184+
handler.unregisterPod(entry.getKey());
185+
cleanedPodCount++;
186+
}
187+
}
188+
189+
int cleanedLoadBalancerCount = 0;
190+
for (Map.Entry<String, String> entry :
191+
handler.getReplicaSetUidAndServerNameMap().getAllMap().entrySet()) {
192+
if (plugin.getProxy().getServer(entry.getValue()).isPresent()) {
193+
continue;
194+
}
195+
handler.unregisterLoadBalancer(entry.getKey());
196+
cleanedLoadBalancerCount++;
197+
}
198+
199+
source.sendMessage(
200+
Component.text(
201+
"Repair complete: repairedPod="
202+
+ repairedPodCount
203+
+ ", cleanedPod="
204+
+ cleanedPodCount
205+
+ ", cleanedLoadBalancer="
206+
+ cleanedLoadBalancerCount));
207+
return;
208+
}
209+
210+
sendHelp(source);
211+
}
212+
213+
private void sendHelp(CommandSource source) {
214+
source.sendMessage(Component.text("/kuvel status"));
215+
source.sendMessage(Component.text("/kuvel list <pods|loadbalancers>"));
216+
source.sendMessage(Component.text("/kuvel register <podUid> <serverName>"));
217+
source.sendMessage(Component.text("/kuvel unregister <podUid>"));
218+
source.sendMessage(Component.text("/kuvel setname <podUid> <serverName>"));
219+
source.sendMessage(Component.text("/kuvel repair"));
220+
}
221+
}

0 commit comments

Comments
 (0)