feat(homelab)!: migrate to svelte kit, add more server stats endpoints
This commit is contained in:
@@ -1 +0,0 @@
|
||||
RCON_PASSWORD=Kl5pONawkLEMSnfi
|
||||
1
nix/homelab/api/.gitignore
vendored
1
nix/homelab/api/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
target
|
||||
.env
|
||||
|
||||
163
nix/homelab/api/Cargo.lock
generated
163
nix/homelab/api/Cargo.lock
generated
@@ -234,6 +234,15 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -378,8 +387,12 @@ version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -461,6 +474,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.5"
|
||||
@@ -669,6 +688,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -691,6 +711,17 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
@@ -833,7 +864,9 @@ name = "homelab-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
"k8s-openapi",
|
||||
"kube",
|
||||
"nom",
|
||||
@@ -842,6 +875,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -984,6 +1018,30 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -1217,6 +1275,7 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -2124,9 +2183,21 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.1",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
@@ -2137,6 +2208,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -2236,6 +2319,23 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.4.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror 2.0.17",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
@@ -2290,6 +2390,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -2371,12 +2477,65 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -2650,9 +2809,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.12"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
||||
checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
|
||||
@@ -12,5 +12,8 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2.0.17"
|
||||
tokio = "1.49.0"
|
||||
kube = { version = "2.0.1", features = ["client", "runtime"] }
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
kube = { version = "2.0.1", features = ["client", "runtime", "ws"] }
|
||||
k8s-openapi = { version = "0.26", features = ["v1_32"] }
|
||||
chrono = "0.4"
|
||||
futures = "0.3"
|
||||
|
||||
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::error::Result;
|
||||
use crate::kubernetes;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RestoreRequest {
|
||||
pub backup_file: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RestoreResponse {
|
||||
pub server: String,
|
||||
pub job_name: String,
|
||||
pub backup_file: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// POST /api/minecraft/{server}/restore
|
||||
pub async fn create_restore(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<ServerPath>,
|
||||
body: web::Json<RestoreRequest>,
|
||||
) -> Result<HttpResponse> {
|
||||
let job_name =
|
||||
kubernetes::create_restore_job(&app_state.kube, &path.server, &body.backup_file).await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(RestoreResponse {
|
||||
server: path.server.clone(),
|
||||
job_name,
|
||||
backup_file: body.backup_file.clone(),
|
||||
status: "created".to_string(),
|
||||
}))
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
pub mod server_stats;
|
||||
pub mod server_uptime;
|
||||
pub mod world_size;
|
||||
|
||||
pub(self) const MINECRAFT_NAMESPACE: &str = "minecraft";
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{AppState, error::Result, rcon::parse_online_list};
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::server_uptime::{PodUptime, get_pod_uptime},
|
||||
error::Result,
|
||||
rcon::parse_online_list,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ServerStats {
|
||||
struct ServerStats {
|
||||
pub status: String,
|
||||
pub players_online: u16,
|
||||
pub max_players: u16,
|
||||
pub uptime: Option<String>,
|
||||
pub uptime: PodUptime,
|
||||
pub world_size: Option<String>,
|
||||
}
|
||||
|
||||
@@ -17,12 +22,13 @@ pub async fn get_server_stats(app_state: web::Data<AppState>) -> Result<HttpResp
|
||||
|
||||
let (_, (players_online, max_players)) =
|
||||
parse_online_list(&list_response).map_err(|e| crate::error::Error::Parse(e.to_string()))?;
|
||||
let uptime = get_pod_uptime(&app_state.kube, "main").await?;
|
||||
|
||||
let stats = ServerStats {
|
||||
status: "Online".to_string(),
|
||||
players_online,
|
||||
max_players,
|
||||
uptime: None,
|
||||
uptime,
|
||||
world_size: None,
|
||||
};
|
||||
|
||||
|
||||
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::{Api, Client, api::ListParams};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::MINECRAFT_NAMESPACE,
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub(super) struct PodUptime {
|
||||
pub seconds: i64,
|
||||
pub started_at: String,
|
||||
}
|
||||
|
||||
pub async fn get_uptime(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let server = path.into_inner();
|
||||
let uptime = get_pod_uptime(&app_state.kube, &server).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(uptime))
|
||||
}
|
||||
|
||||
pub(super) async fn find_minecraft_pod(client: &Client, server_name: &str) -> Result<Pod> {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let label_selector = format!("app=minecraft-{server_name}");
|
||||
let lp = ListParams::default().labels(&label_selector);
|
||||
|
||||
let pod_list = pods.list(&lp).await?;
|
||||
|
||||
pod_list
|
||||
.items
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Error::PodNotFound(format!("minecraft-{server_name}")))
|
||||
}
|
||||
|
||||
pub(super) async fn get_pod_uptime(client: &Client, server_name: &str) -> Result<PodUptime> {
|
||||
let pod = find_minecraft_pod(client, server_name).await?;
|
||||
|
||||
let start_time = pod
|
||||
.status
|
||||
.and_then(|s| s.start_time)
|
||||
.ok_or_else(|| Error::PodExec("pod has no start time".into()))?;
|
||||
let now = chrono::Utc::now();
|
||||
let duration = now.signed_duration_since(&start_time.0);
|
||||
|
||||
Ok(PodUptime {
|
||||
seconds: duration.num_seconds(),
|
||||
started_at: start_time.0.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::{Api, Client};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::{MINECRAFT_NAMESPACE, server_uptime::find_minecraft_pod},
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WorldSizeResponse {
|
||||
pub server: String,
|
||||
pub size: String,
|
||||
}
|
||||
|
||||
pub async fn get_world_size(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let server = path.into_inner();
|
||||
let size = get_size_inner(&app_state.kube, &server).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(WorldSizeResponse {
|
||||
server: server.clone(),
|
||||
size,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_size_inner(client: &Client, server_name: &str) -> Result<String> {
|
||||
let pod = find_minecraft_pod(client, server_name).await?;
|
||||
let pod_name = pod
|
||||
.metadata
|
||||
.name
|
||||
.ok_or_else(|| Error::PodNotFound("no pod name".into()))?;
|
||||
|
||||
let output = exec_in_pod(client, &pod_name, vec!["du", "-sh", "/data"]).await?;
|
||||
|
||||
let size = output
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn exec_in_pod(client: &Client, pod_name: &str, command: &str) {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
const params = AttachParams {
|
||||
stdin: true,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
@@ -9,12 +9,20 @@ pub enum Error {
|
||||
Rcon(#[from] rcon::Error),
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
#[error("kubernetes error: {0}")]
|
||||
Kube(#[from] kube::Error),
|
||||
#[error("pod not found: {0}")]
|
||||
PodNotFound(String),
|
||||
#[error("pod exec error: {0}")]
|
||||
PodExec(String),
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::Rcon(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
Self::PodNotFound(_) => StatusCode::NOT_FOUND,
|
||||
Self::Kube(_) | Self::PodExec(_) => StatusCode::BAD_GATEWAY,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
219
nix/homelab/api/src/kubernetes.rs
Normal file
219
nix/homelab/api/src/kubernetes.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use futures::TryStreamExt;
|
||||
use k8s_openapi::api::batch::v1::Job;
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::Client;
|
||||
use kube::api::{Api, AttachParams, ListParams, PostParams};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const NFS_SERVER: &str = "192.168.27.2";
|
||||
const NFS_BACKUP_PATH: &str = "/backup/minecraft";
|
||||
|
||||
/// Find the Minecraft server pod by server name (e.g., "main", "creative")
|
||||
|
||||
/// Execute a command in a pod and return stdout
|
||||
pub async fn exec_in_pod(client: &Client, pod_name: &str, command: Vec<&str>) -> Result<String> {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let ap = AttachParams {
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut attached = pods.exec(pod_name, command, &ap).await?;
|
||||
|
||||
let mut stdout_str = String::new();
|
||||
if let Some(stdout) = attached.stdout() {
|
||||
let bytes: Vec<u8> = tokio_util::io::ReaderStream::new(stdout)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.map_err(|e| Error::PodExec(e.to_string()))?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
stdout_str = String::from_utf8_lossy(&bytes).to_string();
|
||||
}
|
||||
|
||||
// Wait for exec to finish
|
||||
attached
|
||||
.join()
|
||||
.await
|
||||
.map_err(|e| Error::PodExec(e.to_string()))?;
|
||||
|
||||
Ok(stdout_str.trim().to_string())
|
||||
}
|
||||
|
||||
/// Get the world size by running du -sh /data in the pod
|
||||
/// Get pod uptime by calculating time since pod started
|
||||
|
||||
/// Create a restore job for a Minecraft server
|
||||
pub async fn create_restore_job(
|
||||
client: &Client,
|
||||
server_name: &str,
|
||||
backup_file: &str,
|
||||
) -> Result<String> {
|
||||
let jobs: Api<Job> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let job = build_restore_job(server_name, backup_file);
|
||||
let job_name = job
|
||||
.metadata
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
|
||||
jobs.create(&PostParams::default(), &job).await?;
|
||||
|
||||
Ok(job_name)
|
||||
}
|
||||
|
||||
fn build_restore_job(server_name: &str, backup_file: &str) -> Job {
|
||||
use k8s_openapi::api::core::v1::{
|
||||
Container, EnvVar, NFSVolumeSource, PersistentVolumeClaimVolumeSource, PodSecurityContext,
|
||||
PodSpec, PodTemplateSpec, Volume, VolumeMount,
|
||||
};
|
||||
|
||||
let job_name = format!(
|
||||
"minecraft-restore-{server_name}-{}",
|
||||
chrono::Utc::now().timestamp()
|
||||
);
|
||||
|
||||
let restore_script = r#"
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Minecraft World Restore Job"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
SERVER_NAME="${SERVER_NAME}"
|
||||
BACKUP_FILE="${BACKUP_FILE:-latest.tgz}"
|
||||
BACKUP_PATH="/backups/${BACKUP_FILE}"
|
||||
DATA_DIR="/data"
|
||||
|
||||
echo "Configuration:"
|
||||
echo " Server: ${SERVER_NAME}"
|
||||
echo " Backup file: ${BACKUP_FILE}"
|
||||
echo " Backup path: ${BACKUP_PATH}"
|
||||
echo " Data directory: ${DATA_DIR}"
|
||||
echo ""
|
||||
|
||||
if [ ! -f "${BACKUP_PATH}" ]; then
|
||||
echo "ERROR: Backup file not found: ${BACKUP_PATH}"
|
||||
echo ""
|
||||
echo "Available backups:"
|
||||
ls -lh /backups/ | grep "minecraft-${SERVER_NAME}" || echo " (none found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Backup file found"
|
||||
echo " Size: $(du -hL ${BACKUP_PATH} | cut -f1)"
|
||||
echo ""
|
||||
|
||||
if [ -d "${DATA_DIR}/world" ]; then
|
||||
echo "WARNING: Existing world data found!"
|
||||
echo "Removing existing world data..."
|
||||
rm -rf "${DATA_DIR}/world" "${DATA_DIR}/world_nether" "${DATA_DIR}/world_the_end"
|
||||
echo "Old world data removed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "Extracting backup..."
|
||||
tar -xzf "${BACKUP_PATH}" -C "${DATA_DIR}/"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Restore Complete!"
|
||||
echo "=========================================="
|
||||
"#;
|
||||
|
||||
Job {
|
||||
metadata: kube::core::ObjectMeta {
|
||||
name: Some(job_name),
|
||||
namespace: Some(MINECRAFT_NAMESPACE.to_string()),
|
||||
labels: Some(
|
||||
[("app".to_string(), "minecraft-restore".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
spec: Some(k8s_openapi::api::batch::v1::JobSpec {
|
||||
backoff_limit: Some(0),
|
||||
ttl_seconds_after_finished: Some(3600),
|
||||
template: PodTemplateSpec {
|
||||
metadata: Some(kube::core::ObjectMeta {
|
||||
labels: Some(
|
||||
[("app".to_string(), "minecraft-restore".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
restart_policy: Some("Never".to_string()),
|
||||
security_context: Some(PodSecurityContext {
|
||||
fs_group: Some(2000),
|
||||
run_as_user: Some(1000),
|
||||
run_as_group: Some(3000),
|
||||
..Default::default()
|
||||
}),
|
||||
containers: vec![Container {
|
||||
name: "restore".to_string(),
|
||||
image: Some("busybox:latest".to_string()),
|
||||
command: Some(vec!["sh".to_string(), "-c".to_string()]),
|
||||
args: Some(vec![restore_script.to_string()]),
|
||||
env: Some(vec![
|
||||
EnvVar {
|
||||
name: "SERVER_NAME".to_string(),
|
||||
value: Some(server_name.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
EnvVar {
|
||||
name: "BACKUP_FILE".to_string(),
|
||||
value: Some(backup_file.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
volume_mounts: Some(vec![
|
||||
VolumeMount {
|
||||
name: "data".to_string(),
|
||||
mount_path: "/data".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
VolumeMount {
|
||||
name: "backups".to_string(),
|
||||
mount_path: "/backups".to_string(),
|
||||
read_only: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
..Default::default()
|
||||
}],
|
||||
volumes: Some(vec![
|
||||
Volume {
|
||||
name: "data".to_string(),
|
||||
persistent_volume_claim: Some(PersistentVolumeClaimVolumeSource {
|
||||
claim_name: format!("minecraft-{server_name}-datadir"),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Volume {
|
||||
name: "backups".to_string(),
|
||||
nfs: Some(NFSVolumeSource {
|
||||
server: NFS_SERVER.to_string(),
|
||||
path: NFS_BACKUP_PATH.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ mod rcon;
|
||||
use std::env;
|
||||
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use kube::Client;
|
||||
|
||||
use crate::rcon::RconClient;
|
||||
|
||||
struct AppState {
|
||||
pub struct AppState {
|
||||
rcon: RconClient,
|
||||
kube: Client,
|
||||
}
|
||||
|
||||
struct Env {
|
||||
@@ -36,8 +38,16 @@ fn load_env() -> Env {
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let env = load_env();
|
||||
|
||||
// Initialize Kubernetes client
|
||||
// Uses in-cluster config when running in k3s, falls back to ~/.kube/config locally
|
||||
let kube_client = Client::try_default()
|
||||
.await
|
||||
.expect("failed to create Kubernetes client");
|
||||
|
||||
let app_state = web::Data::new(AppState {
|
||||
rcon: RconClient::new(env.rcon_password),
|
||||
kube: kube_client,
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
@@ -48,10 +58,24 @@ async fn main() -> std::io::Result<()> {
|
||||
web::get()
|
||||
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
|
||||
)
|
||||
.service(web::scope("/api").route(
|
||||
"/minecraft-server-stats",
|
||||
web::get().to(endpoints::server_stats::get_server_stats),
|
||||
))
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.route(
|
||||
"/minecraft-server-stats",
|
||||
web::get().to(endpoints::server_stats::get_server_stats),
|
||||
)
|
||||
// .route(
|
||||
// "/minecraft/{server}/world-size",
|
||||
// web::get().to(endpoints::kubernetes::get_world_size),
|
||||
// )
|
||||
.route(
|
||||
"/minecraft/{server}/uptime",
|
||||
web::get().to(endpoints::server_uptime::get_uptime),
|
||||
), // .route(
|
||||
// "/minecraft/{server}/restore",
|
||||
// web::post().to(endpoints::kubernetes::create_restore),
|
||||
// ),
|
||||
)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
|
||||
Reference in New Issue
Block a user