Skip to content

Commit

Permalink
feat: add first-party support for Minestom servers (#634)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jeremylvln authored Aug 26, 2024
1 parent c41c2e2 commit 3874320
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 16 deletions.
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,15 @@ subprojects {
}
} else if (project.name == "shulker-server-agent") {
val commonSourceSet = sourceSets.create("common")
setOf("paper").forEach { providerName ->
setOf("paper", "minestom").forEach { providerName ->
registerPluginProvider(providerName, commonSourceSet)
}

dependencies {
"commonCompileOnly"(libs.adventure.api)
"paperCompileOnly"(libs.folia.api)
"minestomCompileOnly"(libs.minestom)
"minestomImplementation"(libs.snakeyaml)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +2298,7 @@ spec:
enum:
- Paper
- Folia
- Minestom
type: string
customJar:
description: Reference to a server JAR file to download and use instead of the built-in one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,7 @@ spec:
enum:
- Paper
- Folia
- Minestom
type: string
customJar:
description: Reference to a server JAR file to download and use instead of the built-in one
Expand Down
1 change: 1 addition & 0 deletions packages/shulker-crds/src/v1alpha1/minecraft_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub enum MinecraftServerVersion {
#[default]
Paper,
Folia,
Minestom,
}

#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
Expand Down
3 changes: 3 additions & 0 deletions packages/shulker-kube-utils/src/reconcilers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ pub mod status;

#[derive(Error, Debug)]
pub enum BuilderReconcilerError {
#[error("builder {0} rejected validation of spec: {1}")]
ValidationError(&'static str, String),

#[error("builder {0} failed to build resource: {1}")]
BuilderError(&'static str, #[source] anyhow::Error),

Expand Down
2 changes: 1 addition & 1 deletion packages/shulker-operator/assets/server-init-fs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail
set -o xtrace

cp "${SHULKER_CONFIG_DIR}/server.properties" "${SHULKER_SERVER_CONFIG_DIR}/server.properties"
if [ "${SHULKER_VERSION_CHANNEL}" == "Paper" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Folia" ]; then
if [ "${SHULKER_VERSION_CHANNEL}" == "Paper" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Folia" ] || [ "${SHULKER_VERSION_CHANNEL}" == "Minestom" ]; then
cp "${SHULKER_CONFIG_DIR}/bukkit-config.yml" "${SHULKER_SERVER_CONFIG_DIR}/bukkit.yml"
cp "${SHULKER_CONFIG_DIR}/spigot-config.yml" "${SHULKER_SERVER_CONFIG_DIR}/spigot.yml"
mkdir -p "${SHULKER_SERVER_CONFIG_DIR}/config"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use kube::ResourceExt;
use lazy_static::lazy_static;
use shulker_crds::v1alpha1::minecraft_cluster::MinecraftCluster;
use shulker_crds::v1alpha1::minecraft_server::MinecraftServerVersion;
use shulker_kube_utils::reconcilers::BuilderReconcilerError;
use url::Url;

use crate::agent::AgentConfig;
Expand Down Expand Up @@ -91,6 +92,8 @@ impl<'a> ResourceBuilder<'a> for GameServerBuilder {
_existing_game_server: Option<&Self::ResourceType>,
context: Option<GameServerBuilderContext<'a>>,
) -> Result<Self::ResourceType, anyhow::Error> {
GameServerBuilder::validate_spec(context.as_ref().unwrap(), minecraft_server)?;

let game_server = GameServer {
metadata: ObjectMeta {
name: Some(name.to_string()),
Expand Down Expand Up @@ -127,6 +130,22 @@ impl<'a> GameServerBuilder {
}
}

pub fn validate_spec(
_context: &GameServerBuilderContext<'a>,
minecraft_server: &MinecraftServer,
) -> Result<(), BuilderReconcilerError> {
if minecraft_server.spec.version.channel == MinecraftServerVersion::Minestom
&& minecraft_server.spec.version.custom_jar.is_none()
{
return Err(BuilderReconcilerError::ValidationError(
std::any::type_name::<GameServerBuilder>(),
"a Minestom-based server requires a custom JAR to be provided".to_string(),
));
}

Ok(())
}

pub async fn get_game_server_spec(
resourceref_resolver: &ResourceRefResolver,
context: &GameServerBuilderContext<'a>,
Expand Down Expand Up @@ -565,6 +584,7 @@ impl<'a> GameServerBuilder {
MinecraftServerVersion::Paper | MinecraftServerVersion::Folia => {
Some("paper".to_string())
}
MinecraftServerVersion::Minestom => None,
};

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

fn get_type_from_version_channel(channel: &MinecraftServerVersion) -> String {
match channel {
MinecraftServerVersion::Paper => "PAPER".to_string(),
MinecraftServerVersion::Paper | MinecraftServerVersion::Minestom => "PAPER".to_string(),
MinecraftServerVersion::Folia => "FOLIA".to_string(),
}
}
Expand All @@ -611,8 +631,11 @@ mod tests {
use k8s_openapi::api::core::v1::{
ContainerPort, EmptyDirVolumeSource, LocalObjectReference, Volume, VolumeMount,
};
use shulker_crds::{resourceref::ResourceRefSpec, schemas::ImageOverrideSpec};
use shulker_kube_utils::reconcilers::builder::ResourceBuilder;
use shulker_crds::{
resourceref::ResourceRefSpec, schemas::ImageOverrideSpec,
v1alpha1::minecraft_server::MinecraftServerVersion,
};
use shulker_kube_utils::reconcilers::{builder::ResourceBuilder, BuilderReconcilerError};

use crate::{
agent::AgentConfig,
Expand All @@ -624,6 +647,8 @@ mod tests {
resources::resourceref_resolver::ResourceRefResolver,
};

use super::GameServerBuilder;

#[test]
fn name_contains_server_name() {
// W
Expand All @@ -633,6 +658,37 @@ mod tests {
assert_eq!(name, "my-server");
}

#[test]
fn validate_rejects_minestom_without_custom_jar() {
// G
let mut server = TEST_SERVER.clone();
server.spec.version.channel = MinecraftServerVersion::Minestom;
server.spec.version.custom_jar = None;
let context = super::GameServerBuilderContext {
cluster: &TEST_CLUSTER,
agent_config: &AgentConfig {
maven_repository: constants::SHULKER_PLUGIN_REPOSITORY.to_string(),
version: constants::SHULKER_PLUGIN_VERSION.to_string(),
},
};

// W
let is_valid = GameServerBuilder::validate_spec(&context, &server);

// T
match is_valid.unwrap_err() {
BuilderReconcilerError::ValidationError(_, message) => {
assert_eq!(
message,
"a Minestom-based server requires a custom JAR to be provided"
);
}
_ => {
unreachable!("Error mismatch")
}
}
}

#[tokio::test]
async fn build_snapshot() {
// G
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ apiVersion: v1
kind: ConfigMap
data:
bukkit-config.yml: "settings:\n allow-end: false\nauto-updater:\n enabled: false\n\n"
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"
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"
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"
server.properties: "allow-nether=true\nenforce-secure-profiles=true\nmax-players=42\nonline-mode=false\nprevent-proxy-connections=false\n"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ apiVersion: v1
kind: ConfigMap
data:
bukkit-config.yml: "settings:\n allow-end: false\nauto-updater:\n enabled: false\n\n"
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"
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"
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"
server.properties: "allow-nether=true\nenforce-secure-profiles=true\nmax-players=42\nonline-mode=false\nprevent-proxy-connections=false\n"
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.shulkermc.serveragent.paper

import io.shulkermc.serveragent.ServerInterface
import io.shulkermc.serveragent.platform.HookPostOrder
import io.shulkermc.serveragent.platform.PlayerDisconnectHook
import io.shulkermc.serveragent.platform.PlayerLoginHook
import net.minestom.server.MinecraftServer
import net.minestom.server.event.EventNode
import net.minestom.server.event.player.PlayerDisconnectEvent
import net.minestom.server.event.player.PlayerSpawnEvent
import net.minestom.server.permission.Permission
import net.minestom.server.timer.Task
import net.minestom.server.timer.TaskSchedule
import java.time.Duration
import java.util.UUID
import java.util.concurrent.TimeUnit

class ServerInterfaceMinestom : ServerInterface {
companion object {
private const val ADMIN_PERMISSION_LEVEL = 4
}

private val eventNode = EventNode.all("shulker-server-agent-minestom")

override fun prepareNetworkAdminsPermissions(playerIds: List<UUID>) {
this.eventNode.addListener(PlayerSpawnEvent::class.java) { event: PlayerSpawnEvent ->
if (playerIds.contains(event.player.uuid)) {
event.player.permissionLevel = ADMIN_PERMISSION_LEVEL
event.player.addPermission(Permission("*"))
}
}
}

override fun addPlayerJoinHook(
hook: PlayerLoginHook,
postOrder: HookPostOrder,
) {
this.eventNode.addListener(PlayerSpawnEvent::class.java) { _ -> hook() }
}

override fun addPlayerQuitHook(
hook: PlayerDisconnectHook,
postOrder: HookPostOrder,
) {
this.eventNode.addListener(PlayerDisconnectEvent::class.java) { _ -> hook() }
}

override fun getPlayerCount(): Int = MinecraftServer.getConnectionManager().onlinePlayers.size

override fun scheduleDelayedTask(
delay: Long,
timeUnit: TimeUnit,
runnable: Runnable,
): ServerInterface.ScheduledTask {
val duration = Duration.ofNanos(timeUnit.toNanos(delay))
val task =
MinecraftServer.getSchedulerManager().scheduleTask(
runnable,
TaskSchedule.duration(duration),
TaskSchedule.stop(),
)

return MinestomScheduledTask(task)
}

override fun scheduleRepeatingTask(
delay: Long,
interval: Long,
timeUnit: TimeUnit,
runnable: Runnable,
): ServerInterface.ScheduledTask {
val delayDuration = Duration.ofNanos(timeUnit.toNanos(delay))
val intervalDuration = Duration.ofNanos(timeUnit.toNanos(interval))
val task =
MinecraftServer.getSchedulerManager().scheduleTask(
runnable,
TaskSchedule.duration(delayDuration),
TaskSchedule.duration(intervalDuration),
)

return MinestomScheduledTask(task)
}

private class MinestomScheduledTask(private val minestomTask: Task) : ServerInterface.ScheduledTask {
override fun cancel() {
this.minestomTask.cancel()
}
}
}
Loading

0 comments on commit 3874320

Please sign in to comment.