Skip to content

Commit

Permalink
feat: support providing external servers (#631)
Browse files Browse the repository at this point in the history
* feat(shulker-crds): add external_servers to cluster

* feat(shulker-operator): create configmap with external servers and mount on proxies

* feat(shulker-proxy-agent): watch external servers and register them

* style: fix lint
  • Loading branch information
jeremylvln authored Aug 25, 2024
1 parent 111ca37 commit 86b4804
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 78 deletions.
22 changes: 22 additions & 0 deletions kube/helm/templates/crds/shulkermc.io_minecraftclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ spec:
properties:
spec:
properties:
externalServers:
description: List of servers that should be registered on the proxies that are not managed by Shulker
items:
properties:
address:
description: Address of the server, may contain a port after a colon
type: string
name:
description: Name of the server, as the proxies will register it. Allowed names only are lowercased, dash-separated alphanumerical string
pattern: ^[a-z0-9\-]+$
type: string
tags:
description: Tags associated to the server
items:
type: string
type: array
required:
- address
- name
type: object
nullable: true
type: array
networkAdmins:
description: List of player UUIDs that are automatically promoted as network administrators, which are granted all the permissions by default on all the proxies and servers
items:
Expand Down
22 changes: 22 additions & 0 deletions packages/shulker-crds/src/v1alpha1/minecraft_cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ pub struct MinecraftClusterSpec {
/// for the different Shulker components
#[serde(skip_serializing_if = "Option::is_none")]
pub redis: Option<MinecraftClusterRedisSpec>,

/// List of servers that should be registered on the proxies
/// that are not managed by Shulker
#[serde(skip_serializing_if = "Option::is_none")]
pub external_servers: Option<Vec<MinecraftClusterExternalServerSpec>>,
}

#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
Expand Down Expand Up @@ -72,6 +77,23 @@ impl MinecraftClusterRedisProvidedSpec {
}
}

#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MinecraftClusterExternalServerSpec {
/// Name of the server, as the proxies will register it.
/// Allowed names only are lowercased, dash-separated
/// alphanumerical string
#[schemars(regex(pattern = r"^[a-z0-9\-]+$"))]
pub name: String,

/// Address of the server, may contain a port after a colon
pub address: String,

/// Tags associated to the server
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}

/// The status object of `MinecraftCluster`
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[serde(rename_all = "camelCase")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::collections::BTreeMap;

use k8s_openapi::api::core::v1::ConfigMap;
use kube::core::ObjectMeta;
use kube::Api;
use kube::Client;
use kube::ResourceExt;

use shulker_crds::v1alpha1::minecraft_cluster::MinecraftCluster;
use shulker_kube_utils::reconcilers::builder::ResourceBuilder;

use super::MinecraftClusterReconciler;

pub struct ExternalServersConfigMapBuilder {
client: Client,
}

#[async_trait::async_trait]
impl<'a> ResourceBuilder<'a> for ExternalServersConfigMapBuilder {
type OwnerType = MinecraftCluster;
type ResourceType = ConfigMap;
type Context = ();

fn name(cluster: &Self::OwnerType) -> String {
format!("{}-external-servers", cluster.name_any())
}

fn api(&self, cluster: &Self::OwnerType) -> kube::Api<Self::ResourceType> {
Api::namespaced(self.client.clone(), cluster.namespace().as_ref().unwrap())
}

fn is_needed(&self, cluster: &Self::OwnerType) -> bool {
cluster
.spec
.external_servers
.as_ref()
.map_or(false, |list| !list.is_empty())
}

async fn build(
&self,
cluster: &Self::OwnerType,
name: &str,
_existing_config_map: Option<&Self::ResourceType>,
_context: Option<Self::Context>,
) -> Result<Self::ResourceType, anyhow::Error> {
let config_map = ConfigMap {
metadata: ObjectMeta {
name: Some(name.to_string()),
namespace: Some(cluster.namespace().unwrap().clone()),
labels: Some(MinecraftClusterReconciler::get_labels(
cluster,
"external-servers".to_string(),
"proxy".to_string(),
)),
..ObjectMeta::default()
},
data: Some(BTreeMap::from([(
"external-servers.yaml".to_string(),
ExternalServersConfigMapBuilder::get_content_from_server_list(cluster),
)])),
..ConfigMap::default()
};

Ok(config_map)
}
}

impl ExternalServersConfigMapBuilder {
pub fn new(client: Client) -> Self {
ExternalServersConfigMapBuilder { client }
}

fn get_content_from_server_list(cluster: &MinecraftCluster) -> String {
serde_yaml::to_string(cluster.spec.external_servers.as_ref().unwrap()).unwrap()
}
}

#[cfg(test)]
mod tests {
use shulker_kube_utils::reconcilers::builder::ResourceBuilder;

use crate::reconcilers::minecraft_cluster::fixtures::{create_client_mock, TEST_CLUSTER};

#[test]
fn name_contains_cluster_name() {
// W
let name = super::ExternalServersConfigMapBuilder::name(&TEST_CLUSTER);

// T
assert_eq!(name, "my-cluster-external-servers");
}

#[tokio::test]
async fn build_snapshot() {
// G
let client = create_client_mock();
let builder = super::ExternalServersConfigMapBuilder::new(client);
let name = super::ExternalServersConfigMapBuilder::name(&TEST_CLUSTER);

// W
let config_map = builder
.build(&TEST_CLUSTER, &name, None, None)
.await
.unwrap();

// T
insta::assert_yaml_snapshot!(config_map);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use http::{Request, Response};
use kube::client::Body;
use kube::{core::ObjectMeta, Client};
use lazy_static::lazy_static;
use shulker_crds::v1alpha1::minecraft_cluster::{MinecraftCluster, MinecraftClusterSpec};
use shulker_crds::v1alpha1::minecraft_cluster::{
MinecraftCluster, MinecraftClusterExternalServerSpec, MinecraftClusterSpec,
};

lazy_static! {
pub static ref TEST_CLUSTER: MinecraftCluster = MinecraftCluster {
Expand All @@ -13,7 +15,12 @@ lazy_static! {
},
spec: MinecraftClusterSpec {
network_admins: None,
redis: None
redis: None,
external_servers: Some(vec![MinecraftClusterExternalServerSpec {
name: "my-external-server".to_string(),
address: "127.0.0.1:25565".to_string(),
tags: vec!["game".to_string()]
}])
},
status: None,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::{collections::BTreeMap, sync::Arc, time::Duration};

use external_servers_config_map::ExternalServersConfigMapBuilder;
use futures::StreamExt;
use k8s_openapi::api::{
apps::v1::StatefulSet,
core::v1::{Secret, Service, ServiceAccount},
core::v1::{ConfigMap, Secret, Service, ServiceAccount},
rbac::v1::{Role, RoleBinding},
};
use kube::{
Expand Down Expand Up @@ -35,6 +36,7 @@ use self::{

use super::Result;

pub mod external_servers_config_map;
mod forwarding_secret;
mod headless_service;
mod minecraft_server_role;
Expand Down Expand Up @@ -65,6 +67,7 @@ struct MinecraftClusterReconciler {
minecraft_server_role_binding_builder: MinecraftServerRoleBindingBuilder,
redis_service_builder: RedisServiceBuilder,
redis_stateful_set_builder: RedisStatefulSetBuilder,
external_servers_config_map_builder: ExternalServersConfigMapBuilder,
}

impl MinecraftClusterReconciler {
Expand Down Expand Up @@ -111,6 +114,13 @@ impl MinecraftClusterReconciler {
reconcile_builder(&self.redis_stateful_set_builder, cluster.as_ref(), None)
.await
.map_err(ReconcilerError::BuilderError)?;
reconcile_builder(
&self.external_servers_config_map_builder,
cluster.as_ref(),
None,
)
.await
.map_err(ReconcilerError::BuilderError)?;

Ok(Action::requeue(Duration::from_secs(5 * 60)))
}
Expand Down Expand Up @@ -210,9 +220,15 @@ pub async fn run(client: Client) {
),
redis_service_builder: RedisServiceBuilder::new(client.clone()),
redis_stateful_set_builder: RedisStatefulSetBuilder::new(client.clone()),

external_servers_config_map_builder: ExternalServersConfigMapBuilder::new(client.clone()),
};

Controller::new(clusters_api, Config::default().any_semantic())
.owns(
Api::<ConfigMap>::all(client.clone()),
Config::default().any_semantic(),
)
.owns(
Api::<Secret>::all(client.clone()),
Config::default().any_semantic(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: packages/shulker-operator/src/reconcilers/minecraft_cluster/external_servers_config_map.rs
expression: config_map
---
apiVersion: v1
kind: ConfigMap
data:
external-servers.yaml: "- name: my-external-server\n address: 127.0.0.1:25565\n tags:\n - game\n"
metadata:
labels:
app.kubernetes.io/component: proxy
app.kubernetes.io/instance: external-servers-my-cluster
app.kubernetes.io/managed-by: shulker-operator
app.kubernetes.io/name: external-servers
app.kubernetes.io/part-of: cluster-my-cluster
minecraftcluster.shulkermc.io/name: my-cluster
name: my-cluster-external-servers
namespace: default
Loading

0 comments on commit 86b4804

Please sign in to comment.