Skip to content

Commit 3874320

Browse files
authored
feat: add first-party support for Minestom servers (#634)
* feat(shulker-server-agent): create agent library for Minestom * feat(shulker-crds): add Minestom in version channels * feat(shulker-operator): mimic Paper server when channel is Minestom * fix(shulker-server-agent): add support for BungeeCord proxy * feat(shulker-operator): validate MinecraftServerSpec on reconciling
1 parent c41c2e2 commit 3874320

File tree

15 files changed

+305
-16
lines changed

15 files changed

+305
-16
lines changed

Cargo.lock

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle.kts

+3-1
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,15 @@ subprojects {
249249
}
250250
} else if (project.name == "shulker-server-agent") {
251251
val commonSourceSet = sourceSets.create("common")
252-
setOf("paper").forEach { providerName ->
252+
setOf("paper", "minestom").forEach { providerName ->
253253
registerPluginProvider(providerName, commonSourceSet)
254254
}
255255

256256
dependencies {
257257
"commonCompileOnly"(libs.adventure.api)
258258
"paperCompileOnly"(libs.folia.api)
259+
"minestomCompileOnly"(libs.minestom)
260+
"minestomImplementation"(libs.snakeyaml)
259261
}
260262
}
261263
}

kube/helm/templates/crds/shulkermc.io_minecraftserverfleets.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -2298,6 +2298,7 @@ spec:
22982298
enum:
22992299
- Paper
23002300
- Folia
2301+
- Minestom
23012302
type: string
23022303
customJar:
23032304
description: Reference to a server JAR file to download and use instead of the built-in one

kube/helm/templates/crds/shulkermc.io_minecraftservers.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,7 @@ spec:
20692069
enum:
20702070
- Paper
20712071
- Folia
2072+
- Minestom
20722073
type: string
20732074
customJar:
20742075
description: Reference to a server JAR file to download and use instead of the built-in one

packages/shulker-crds/src/v1alpha1/minecraft_server.rs

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub enum MinecraftServerVersion {
6666
#[default]
6767
Paper,
6868
Folia,
69+
Minestom,
6970
}
7071

7172
#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]

packages/shulker-kube-utils/src/reconcilers/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ pub mod status;
55

66
#[derive(Error, Debug)]
77
pub enum BuilderReconcilerError {
8+
#[error("builder {0} rejected validation of spec: {1}")]
9+
ValidationError(&'static str, String),
10+
811
#[error("builder {0} failed to build resource: {1}")]
912
BuilderError(&'static str, #[source] anyhow::Error),
1013

packages/shulker-operator/assets/server-init-fs.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ set -euo pipefail
33
set -o xtrace
44

55
cp "${SHULKER_CONFIG_DIR}/server.properties" "${SHULKER_SERVER_CONFIG_DIR}/server.properties"
6-
if [ "${SHULKER_VERSION_CHANNEL}" == "Paper" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Folia" ]; then
6+
if [ "${SHULKER_VERSION_CHANNEL}" == "Paper" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Folia" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Minestom" ]; then
77
cp "${SHULKER_CONFIG_DIR}/bukkit-config.yml" "${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml"
88
cp "${SHULKER_CONFIG_DIR}/spigot-config.yml" "${SHULKER_SERVER_CONFIG_DIR}/spigot.yml"
99
mkdir -p "${SHULKER_SERVER_CONFIG_DIR}/config"

packages/shulker-operator/src/reconcilers/minecraft_server/gameserver.rs

+59-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use kube::ResourceExt;
2121
use lazy_static::lazy_static;
2222
use shulker_crds::v1alpha1::minecraft_cluster::MinecraftCluster;
2323
use shulker_crds::v1alpha1::minecraft_server::MinecraftServerVersion;
24+
use shulker_kube_utils::reconcilers::BuilderReconcilerError;
2425
use url::Url;
2526

2627
use crate::agent::AgentConfig;
@@ -91,6 +92,8 @@ impl<'a> ResourceBuilder<'a> for GameServerBuilder {
9192
_existing_game_server: Option<&Self::ResourceType>,
9293
context: Option<GameServerBuilderContext<'a>>,
9394
) -> Result<Self::ResourceType, anyhow::Error> {
95+
GameServerBuilder::validate_spec(context.as_ref().unwrap(), minecraft_server)?;
96+
9497
let game_server = GameServer {
9598
metadata: ObjectMeta {
9699
name: Some(name.to_string()),
@@ -127,6 +130,22 @@ impl<'a> GameServerBuilder {
127130
}
128131
}
129132

133+
pub fn validate_spec(
134+
_context: &GameServerBuilderContext<'a>,
135+
minecraft_server: &MinecraftServer,
136+
) -> Result<(), BuilderReconcilerError> {
137+
if minecraft_server.spec.version.channel == MinecraftServerVersion::Minestom
138+
&& minecraft_server.spec.version.custom_jar.is_none()
139+
{
140+
return Err(BuilderReconcilerError::ValidationError(
141+
std::any::type_name::<GameServerBuilder>(),
142+
"a Minestom-based server requires a custom JAR to be provided".to_string(),
143+
));
144+
}
145+
146+
Ok(())
147+
}
148+
130149
pub async fn get_game_server_spec(
131150
resourceref_resolver: &ResourceRefResolver,
132151
context: &GameServerBuilderContext<'a>,
@@ -565,6 +584,7 @@ impl<'a> GameServerBuilder {
565584
MinecraftServerVersion::Paper | MinecraftServerVersion::Folia => {
566585
Some("paper".to_string())
567586
}
587+
MinecraftServerVersion::Minestom => None,
568588
};
569589

570590
let mut plugin_refs: Vec<Url> = vec![];
@@ -596,7 +616,7 @@ impl<'a> GameServerBuilder {
596616

597617
fn get_type_from_version_channel(channel: &MinecraftServerVersion) -> String {
598618
match channel {
599-
MinecraftServerVersion::Paper => "PAPER".to_string(),
619+
MinecraftServerVersion::Paper | MinecraftServerVersion::Minestom => "PAPER".to_string(),
600620
MinecraftServerVersion::Folia => "FOLIA".to_string(),
601621
}
602622
}
@@ -611,8 +631,11 @@ mod tests {
611631
use k8s_openapi::api::core::v1::{
612632
ContainerPort, EmptyDirVolumeSource, LocalObjectReference, Volume, VolumeMount,
613633
};
614-
use shulker_crds::{resourceref::ResourceRefSpec, schemas::ImageOverrideSpec};
615-
use shulker_kube_utils::reconcilers::builder::ResourceBuilder;
634+
use shulker_crds::{
635+
resourceref::ResourceRefSpec, schemas::ImageOverrideSpec,
636+
v1alpha1::minecraft_server::MinecraftServerVersion,
637+
};
638+
use shulker_kube_utils::reconcilers::{builder::ResourceBuilder, BuilderReconcilerError};
616639

617640
use crate::{
618641
agent::AgentConfig,
@@ -624,6 +647,8 @@ mod tests {
624647
resources::resourceref_resolver::ResourceRefResolver,
625648
};
626649

650+
use super::GameServerBuilder;
651+
627652
#[test]
628653
fn name_contains_server_name() {
629654
// W
@@ -633,6 +658,37 @@ mod tests {
633658
assert_eq!(name, "my-server");
634659
}
635660

661+
#[test]
662+
fn validate_rejects_minestom_without_custom_jar() {
663+
// G
664+
let mut server = TEST_SERVER.clone();
665+
server.spec.version.channel = MinecraftServerVersion::Minestom;
666+
server.spec.version.custom_jar = None;
667+
let context = super::GameServerBuilderContext {
668+
cluster: &TEST_CLUSTER,
669+
agent_config: &AgentConfig {
670+
maven_repository: constants::SHULKER_PLUGIN_REPOSITORY.to_string(),
671+
version: constants::SHULKER_PLUGIN_VERSION.to_string(),
672+
},
673+
};
674+
675+
// W
676+
let is_valid = GameServerBuilder::validate_spec(&context, &server);
677+
678+
// T
679+
match is_valid.unwrap_err() {
680+
BuilderReconcilerError::ValidationError(_, message) => {
681+
assert_eq!(
682+
message,
683+
"a Minestom-based server requires a custom JAR to be provided"
684+
);
685+
}
686+
_ => {
687+
unreachable!("Error mismatch")
688+
}
689+
}
690+
}
691+
636692
#[tokio::test]
637693
async fn build_snapshot() {
638694
// G

packages/shulker-operator/src/reconcilers/minecraft_server/snapshots/shulker_operator__reconcilers__minecraft_server__config_map__tests__build_snapshot.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apiVersion: v1
66
kind: ConfigMap
77
data:
88
bukkit-config.yml: "settings:\n allow-end: false\nauto-updater:\n enabled: false\n\n"
9-
init-fs.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\ncp \"${SHULKER_CONFIG_DIR}/server.properties\" \"${SHULKER_SERVER_CONFIG_DIR}/server.properties\"\nif [ \"${SHULKER_VERSION_CHANNEL}\" == \"Paper\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Folia\" ]; then\n cp \"${SHULKER_CONFIG_DIR}/bukkit-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml\"\n cp \"${SHULKER_CONFIG_DIR}/spigot-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/spigot.yml\"\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/config\"\n cp \"${SHULKER_CONFIG_DIR}/paper-global-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/config/paper-global.yml\"\nfi\n\nif [ ! -z \"${SERVER_WORLD_URL:-}\" ]; then\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${SERVER_WORLD_URL}\" -O - | tar -xzv)\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PLUGIN_URLS:-}\" ]; then\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/plugins\"\n for plugin_url in ${SHULKER_SERVER_PLUGIN_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}/plugins\" && wget \"${plugin_url}\")\n done\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PATCH_URLS:-}\" ]; then\n for patch_url in ${SHULKER_SERVER_PATCH_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${patch_url}\" -O - | tar -xzv)\n done\nfi\n"
9+
init-fs.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\ncp \"${SHULKER_CONFIG_DIR}/server.properties\" \"${SHULKER_SERVER_CONFIG_DIR}/server.properties\"\nif [ \"${SHULKER_VERSION_CHANNEL}\" == \"Paper\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Folia\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Minestom\" ]; then\n cp \"${SHULKER_CONFIG_DIR}/bukkit-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml\"\n cp \"${SHULKER_CONFIG_DIR}/spigot-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/spigot.yml\"\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/config\"\n cp \"${SHULKER_CONFIG_DIR}/paper-global-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/config/paper-global.yml\"\nfi\n\nif [ ! -z \"${SERVER_WORLD_URL:-}\" ]; then\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${SERVER_WORLD_URL}\" -O - | tar -xzv)\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PLUGIN_URLS:-}\" ]; then\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/plugins\"\n for plugin_url in ${SHULKER_SERVER_PLUGIN_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}/plugins\" && wget \"${plugin_url}\")\n done\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PATCH_URLS:-}\" ]; then\n for patch_url in ${SHULKER_SERVER_PATCH_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${patch_url}\" -O - | tar -xzv)\n done\nfi\n"
1010
paper-global-config.yml: "proxies:\n bungee-cord:\n online-mode: false\n velocity:\n enabled: true\n online-mode: true\n secret: ${CFG_VELOCITY_FORWARDING_SECRET}\n\n"
1111
server.properties: "allow-nether=true\nenforce-secure-profiles=true\nmax-players=42\nonline-mode=false\nprevent-proxy-connections=false\n"
1212
spigot-config.yml: "settings:\n bungeecord: false\n restart-on-crash: false\nadvancements:\n disable-saving: true\nplayers:\n disable-saving: true\nstats:\n disable-saving: true\nsave-user-cache-on-stop-only: true\n\n"

packages/shulker-operator/src/reconcilers/minecraft_server_fleet/snapshots/shulker_operator__reconcilers__minecraft_server_fleet__config_map__tests__build_snapshot.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apiVersion: v1
66
kind: ConfigMap
77
data:
88
bukkit-config.yml: "settings:\n allow-end: false\nauto-updater:\n enabled: false\n\n"
9-
init-fs.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\ncp \"${SHULKER_CONFIG_DIR}/server.properties\" \"${SHULKER_SERVER_CONFIG_DIR}/server.properties\"\nif [ \"${SHULKER_VERSION_CHANNEL}\" == \"Paper\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Folia\" ]; then\n cp \"${SHULKER_CONFIG_DIR}/bukkit-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml\"\n cp \"${SHULKER_CONFIG_DIR}/spigot-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/spigot.yml\"\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/config\"\n cp \"${SHULKER_CONFIG_DIR}/paper-global-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/config/paper-global.yml\"\nfi\n\nif [ ! -z \"${SERVER_WORLD_URL:-}\" ]; then\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${SERVER_WORLD_URL}\" -O - | tar -xzv)\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PLUGIN_URLS:-}\" ]; then\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/plugins\"\n for plugin_url in ${SHULKER_SERVER_PLUGIN_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}/plugins\" && wget \"${plugin_url}\")\n done\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PATCH_URLS:-}\" ]; then\n for patch_url in ${SHULKER_SERVER_PATCH_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${patch_url}\" -O - | tar -xzv)\n done\nfi\n"
9+
init-fs.sh: "#!/bin/sh\nset -euo pipefail\nset -o xtrace\n\ncp \"${SHULKER_CONFIG_DIR}/server.properties\" \"${SHULKER_SERVER_CONFIG_DIR}/server.properties\"\nif [ \"${SHULKER_VERSION_CHANNEL}\" == \"Paper\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Folia\" ] || [ \"${SHULKER_VERSION_CHANNEL}\" == \"Minestom\" ]; then\n cp \"${SHULKER_CONFIG_DIR}/bukkit-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml\"\n cp \"${SHULKER_CONFIG_DIR}/spigot-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/spigot.yml\"\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/config\"\n cp \"${SHULKER_CONFIG_DIR}/paper-global-config.yml\" \"${SHULKER_SERVER_CONFIG_DIR}/config/paper-global.yml\"\nfi\n\nif [ ! -z \"${SERVER_WORLD_URL:-}\" ]; then\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${SERVER_WORLD_URL}\" -O - | tar -xzv)\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PLUGIN_URLS:-}\" ]; then\n mkdir -p \"${SHULKER_SERVER_CONFIG_DIR}/plugins\"\n for plugin_url in ${SHULKER_SERVER_PLUGIN_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}/plugins\" && wget \"${plugin_url}\")\n done\nfi\n\nif [ ! -z \"${SHULKER_SERVER_PATCH_URLS:-}\" ]; then\n for patch_url in ${SHULKER_SERVER_PATCH_URLS//;/ }; do\n (cd \"${SHULKER_SERVER_CONFIG_DIR}\" && wget \"${patch_url}\" -O - | tar -xzv)\n done\nfi\n"
1010
paper-global-config.yml: "proxies:\n bungee-cord:\n online-mode: false\n velocity:\n enabled: true\n online-mode: true\n secret: ${CFG_VELOCITY_FORWARDING_SECRET}\n\n"
1111
server.properties: "allow-nether=true\nenforce-secure-profiles=true\nmax-players=42\nonline-mode=false\nprevent-proxy-connections=false\n"
1212
spigot-config.yml: "settings:\n bungeecord: false\n restart-on-crash: false\nadvancements:\n disable-saving: true\nplayers:\n disable-saving: true\nstats:\n disable-saving: true\nsave-user-cache-on-stop-only: true\n\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.shulkermc.serveragent.paper
2+
3+
import io.shulkermc.serveragent.ServerInterface
4+
import io.shulkermc.serveragent.platform.HookPostOrder
5+
import io.shulkermc.serveragent.platform.PlayerDisconnectHook
6+
import io.shulkermc.serveragent.platform.PlayerLoginHook
7+
import net.minestom.server.MinecraftServer
8+
import net.minestom.server.event.EventNode
9+
import net.minestom.server.event.player.PlayerDisconnectEvent
10+
import net.minestom.server.event.player.PlayerSpawnEvent
11+
import net.minestom.server.permission.Permission
12+
import net.minestom.server.timer.Task
13+
import net.minestom.server.timer.TaskSchedule
14+
import java.time.Duration
15+
import java.util.UUID
16+
import java.util.concurrent.TimeUnit
17+
18+
class ServerInterfaceMinestom : ServerInterface {
19+
companion object {
20+
private const val ADMIN_PERMISSION_LEVEL = 4
21+
}
22+
23+
private val eventNode = EventNode.all("shulker-server-agent-minestom")
24+
25+
override fun prepareNetworkAdminsPermissions(playerIds: List<UUID>) {
26+
this.eventNode.addListener(PlayerSpawnEvent::class.java) { event: PlayerSpawnEvent ->
27+
if (playerIds.contains(event.player.uuid)) {
28+
event.player.permissionLevel = ADMIN_PERMISSION_LEVEL
29+
event.player.addPermission(Permission("*"))
30+
}
31+
}
32+
}
33+
34+
override fun addPlayerJoinHook(
35+
hook: PlayerLoginHook,
36+
postOrder: HookPostOrder,
37+
) {
38+
this.eventNode.addListener(PlayerSpawnEvent::class.java) { _ -> hook() }
39+
}
40+
41+
override fun addPlayerQuitHook(
42+
hook: PlayerDisconnectHook,
43+
postOrder: HookPostOrder,
44+
) {
45+
this.eventNode.addListener(PlayerDisconnectEvent::class.java) { _ -> hook() }
46+
}
47+
48+
override fun getPlayerCount(): Int = MinecraftServer.getConnectionManager().onlinePlayers.size
49+
50+
override fun scheduleDelayedTask(
51+
delay: Long,
52+
timeUnit: TimeUnit,
53+
runnable: Runnable,
54+
): ServerInterface.ScheduledTask {
55+
val duration = Duration.ofNanos(timeUnit.toNanos(delay))
56+
val task =
57+
MinecraftServer.getSchedulerManager().scheduleTask(
58+
runnable,
59+
TaskSchedule.duration(duration),
60+
TaskSchedule.stop(),
61+
)
62+
63+
return MinestomScheduledTask(task)
64+
}
65+
66+
override fun scheduleRepeatingTask(
67+
delay: Long,
68+
interval: Long,
69+
timeUnit: TimeUnit,
70+
runnable: Runnable,
71+
): ServerInterface.ScheduledTask {
72+
val delayDuration = Duration.ofNanos(timeUnit.toNanos(delay))
73+
val intervalDuration = Duration.ofNanos(timeUnit.toNanos(interval))
74+
val task =
75+
MinecraftServer.getSchedulerManager().scheduleTask(
76+
runnable,
77+
TaskSchedule.duration(delayDuration),
78+
TaskSchedule.duration(intervalDuration),
79+
)
80+
81+
return MinestomScheduledTask(task)
82+
}
83+
84+
private class MinestomScheduledTask(private val minestomTask: Task) : ServerInterface.ScheduledTask {
85+
override fun cancel() {
86+
this.minestomTask.cancel()
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)