summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/docker.rs39
-rw-r--r--src/models/mod.rs13
-rw-r--r--src/rcon.rs8
-rw-r--r--src/routes/backups.rs2
-rw-r--r--src/routes/instances.rs36
-rw-r--r--src/routes/players.rs2
6 files changed, 31 insertions, 69 deletions
diff --git a/src/docker.rs b/src/docker.rs
index b18bb47..6b8640a 100644
--- a/src/docker.rs
+++ b/src/docker.rs
@@ -1,7 +1,7 @@
1use bollard::Docker; 1use bollard::Docker;
2use bollard::container::{ 2use bollard::container::{
3 Config as ContainerConfig, CreateContainerOptions, ListContainersOptions, 3 Config as ContainerConfig, CreateContainerOptions, RemoveContainerOptions,
4 RemoveContainerOptions, StartContainerOptions, StopContainerOptions, 4 StartContainerOptions, StopContainerOptions,
5}; 5};
6use bollard::image::CreateImageOptions; 6use bollard::image::CreateImageOptions;
7use bollard::models::HostConfig; 7use bollard::models::HostConfig;
@@ -17,47 +17,12 @@ pub struct DockerManager {
17 config: Config, 17 config: Config,
18} 18}
19 19
20#[derive(Debug, Clone)]
21pub enum ContainerStatus {
22 Running,
23 Stopped,
24 Starting,
25 NotFound,
26 Error(String),
27}
28
29impl DockerManager { 20impl DockerManager {
30 pub fn new(config: Config) -> Result<Self, bollard::errors::Error> { 21 pub fn new(config: Config) -> Result<Self, bollard::errors::Error> {
31 let client = Docker::connect_with_local_defaults()?; 22 let client = Docker::connect_with_local_defaults()?;
32 Ok(Self { client, config }) 23 Ok(Self { client, config })
33 } 24 }
34 25
35 pub async fn container_status(&self) -> ContainerStatus {
36 let mut filters = HashMap::new();
37 filters.insert("name".to_string(), vec!["mc".to_string()]);
38
39 let options = ListContainersOptions {
40 all: true,
41 filters,
42 ..Default::default()
43 };
44
45 match self.client.list_containers(Some(options)).await {
46 Ok(containers) => {
47 if containers.is_empty() {
48 ContainerStatus::NotFound
49 } else {
50 match containers[0].state.as_deref() {
51 Some("running") => ContainerStatus::Running,
52 Some("exited") | Some("stopped") => ContainerStatus::Stopped,
53 _ => ContainerStatus::Starting,
54 }
55 }
56 }
57 Err(e) => ContainerStatus::Error(e.to_string()),
58 }
59 }
60
61 pub async fn start_container(&self, instance: &Instance) -> Result<(), String> { 26 pub async fn start_container(&self, instance: &Instance) -> Result<(), String> {
62 tracing::info!("Pulling itzg/minecraft-server:latest image..."); 27 tracing::info!("Pulling itzg/minecraft-server:latest image...");
63 self.pull_image().await?; 28 self.pull_image().await?;
diff --git a/src/models/mod.rs b/src/models/mod.rs
index c847ea3..0b083ff 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -29,16 +29,3 @@ pub struct Instance {
29 pub created_at: String, 29 pub created_at: String,
30 pub updated_at: String, 30 pub updated_at: String,
31} 31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Backup {
35 pub id: i64,
36 pub instance_id: i64,
37 pub filename: String,
38 pub created_at: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PlayerEntry {
43 pub username: String,
44}
diff --git a/src/rcon.rs b/src/rcon.rs
index b91fe9b..eb3b72a 100644
--- a/src/rcon.rs
+++ b/src/rcon.rs
@@ -112,22 +112,16 @@ impl RconClient {
112 .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?; 112 .map_err(|e| RconError::Response(format!("Failed to read payload: {e}")))?;
113 113
114 let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); 114 let request_id = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
115 let packet_type = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]);
116 115
117 let body_bytes = &payload[8..payload.len() - 2]; 116 let body_bytes = &payload[8..payload.len() - 2];
118 let body = String::from_utf8_lossy(body_bytes).to_string(); 117 let body = String::from_utf8_lossy(body_bytes).to_string();
119 118
120 Ok(RconPacket { 119 Ok(RconPacket { request_id, body })
121 request_id,
122 packet_type,
123 body,
124 })
125 } 120 }
126} 121}
127 122
128#[derive(Debug)] 123#[derive(Debug)]
129struct RconPacket { 124struct RconPacket {
130 request_id: i32, 125 request_id: i32,
131 packet_type: i32,
132 body: String, 126 body: String,
133} 127}
diff --git a/src/routes/backups.rs b/src/routes/backups.rs
index 6b4dd49..6f7617e 100644
--- a/src/routes/backups.rs
+++ b/src/routes/backups.rs
@@ -22,7 +22,6 @@ struct BackupListTemplate {
22 instance_name: String, 22 instance_name: String,
23 instance_id: i64, 23 instance_id: i64,
24 backups: Vec<BackupRow>, 24 backups: Vec<BackupRow>,
25 message: Option<String>,
26} 25}
27 26
28struct BackupRow { 27struct BackupRow {
@@ -78,7 +77,6 @@ async fn list_backups(
78 instance_name, 77 instance_name,
79 instance_id, 78 instance_id,
80 backups, 79 backups,
81 message: None,
82 }; 80 };
83 81
84 Ok(Html(template.render().unwrap_or_default())) 82 Ok(Html(template.render().unwrap_or_default()))
diff --git a/src/routes/instances.rs b/src/routes/instances.rs
index e4f62f0..8dd6a62 100644
--- a/src/routes/instances.rs
+++ b/src/routes/instances.rs
@@ -19,7 +19,6 @@ struct InstanceListTemplate {
19 is_superuser: bool, 19 is_superuser: bool,
20 password_change_required: bool, 20 password_change_required: bool,
21 instances: Vec<InstanceInfo>, 21 instances: Vec<InstanceInfo>,
22 has_active: bool,
23} 22}
24 23
25#[derive(Template)] 24#[derive(Template)]
@@ -42,7 +41,6 @@ struct EditInstanceTemplate {
42 instance: Instance, 41 instance: Instance,
43 current_seed: Option<String>, 42 current_seed: Option<String>,
44 max_memory_mb: u32, 43 max_memory_mb: u32,
45 error: Option<String>,
46} 44}
47 45
48struct InstanceInfo { 46struct InstanceInfo {
@@ -119,7 +117,6 @@ async fn list_instances(
119 .filter_map(|r| r.ok()) 117 .filter_map(|r| r.ok())
120 .collect(); 118 .collect();
121 119
122 let has_active = instances.iter().any(|i| i.is_active);
123 drop(stmt); 120 drop(stmt);
124 drop(conn); 121 drop(conn);
125 122
@@ -128,7 +125,6 @@ async fn list_instances(
128 is_superuser: account.is_superuser, 125 is_superuser: account.is_superuser,
129 password_change_required: account.password_change_required, 126 password_change_required: account.password_change_required,
130 instances, 127 instances,
131 has_active,
132 }; 128 };
133 129
134 Ok(Html(template.render().unwrap_or_default())) 130 Ok(Html(template.render().unwrap_or_default()))
@@ -283,7 +279,6 @@ async fn edit_form(
283 instance, 279 instance,
284 current_seed, 280 current_seed,
285 max_memory_mb: state.config.max_memory_mb, 281 max_memory_mb: state.config.max_memory_mb,
286 error: None,
287 }; 282 };
288 283
289 Ok(Html(template.render().unwrap_or_default())) 284 Ok(Html(template.render().unwrap_or_default()))
@@ -428,6 +423,7 @@ async fn activate_instance(
428 // Pull image + start container in background so the UI stays responsive 423 // Pull image + start container in background so the UI stays responsive
429 let docker = state.docker.clone(); 424 let docker = state.docker.clone();
430 let db = state.db.clone(); 425 let db = state.db.clone();
426 let rcon_pw = instance.rcon_password.clone();
431 tokio::spawn(async move { 427 tokio::spawn(async move {
432 { 428 {
433 let conn = db.conn(); 429 let conn = db.conn();
@@ -439,9 +435,33 @@ async fn activate_instance(
439 } 435 }
440 match docker.start_container(&instance).await { 436 match docker.start_container(&instance).await {
441 Ok(()) => { 437 Ok(()) => {
438 {
439 let conn = db.conn();
440 conn.execute(
441 "UPDATE instances SET container_state = 'booting' WHERE is_active = 1",
442 [],
443 )
444 .ok();
445 }
446 // Wait for Minecraft to be ready via RCON
447 let rcon = crate::rcon::RconClient::new(rcon_pw);
448 for _ in 0..30 {
449 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
450 if rcon.send_command_async("list".into()).await.is_ok() {
451 let conn = db.conn();
452 conn.execute(
453 "UPDATE instances SET container_state = 'running' WHERE is_active = 1",
454 [],
455 )
456 .ok();
457 tracing::info!("Server is ready (RCON responded)");
458 return;
459 }
460 }
461 tracing::error!("Server did not become ready within 60 seconds");
442 let conn = db.conn(); 462 let conn = db.conn();
443 conn.execute( 463 conn.execute(
444 "UPDATE instances SET container_state = 'running' WHERE is_active = 1", 464 "UPDATE instances SET container_state = 'error' WHERE is_active = 1",
445 [], 465 [],
446 ) 466 )
447 .ok(); 467 .ok();
@@ -458,7 +478,7 @@ async fn activate_instance(
458 } 478 }
459 }); 479 });
460 480
461 Ok(Redirect::to("/")) 481 Ok(Redirect::to("/instances"))
462} 482}
463 483
464async fn deactivate_instance( 484async fn deactivate_instance(
@@ -475,7 +495,7 @@ async fn deactivate_instance(
475 .ok(); 495 .ok();
476 drop(conn); 496 drop(conn);
477 497
478 Ok(Redirect::to("/")) 498 Ok(Redirect::to("/instances"))
479} 499}
480 500
481fn generate_rcon_password() -> String { 501fn generate_rcon_password() -> String {
diff --git a/src/routes/players.rs b/src/routes/players.rs
index b98a39d..5c90efb 100644
--- a/src/routes/players.rs
+++ b/src/routes/players.rs
@@ -20,7 +20,6 @@ struct PlayerManagementTemplate {
20 whitelist: Vec<String>, 20 whitelist: Vec<String>,
21 ops: Vec<String>, 21 ops: Vec<String>,
22 bans: Vec<String>, 22 bans: Vec<String>,
23 message: Option<String>,
24} 23}
25 24
26#[derive(Debug, Deserialize)] 25#[derive(Debug, Deserialize)]
@@ -70,7 +69,6 @@ async fn manage_players(
70 whitelist, 69 whitelist,
71 ops, 70 ops,
72 bans, 71 bans,
73 message: None,
74 }; 72 };
75 73
76 Ok(Html(template.render().unwrap_or_default())) 74 Ok(Html(template.render().unwrap_or_default()))