chore: initial commit for phase04

This commit is contained in:
hibna
2026-02-21 15:50:35 +03:00
parent d0c20581b6
commit 218452706c
15 changed files with 4310 additions and 8 deletions
+263
View File
@@ -0,0 +1,263 @@
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use bollard::container::{
Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions,
StopContainerOptions, StatsOptions, Stats,
};
use bollard::image::CreateImageOptions;
use bollard::models::{HostConfig, PortBinding};
use futures::StreamExt;
use tracing::info;
use crate::docker::DockerManager;
use crate::server::ServerSpec;
/// Container name prefix for all managed game servers.
const CONTAINER_PREFIX: &str = "gp_";
pub fn container_name(server_uuid: &str) -> String {
format!("{}{}", CONTAINER_PREFIX, server_uuid)
}
impl DockerManager {
/// Pull a Docker image if not already present.
pub async fn pull_image(&self, image: &str) -> Result<()> {
info!(image = %image, "Pulling Docker image");
let options = CreateImageOptions {
from_image: image,
..Default::default()
};
let mut stream = self.client().create_image(Some(options), None, None);
while let Some(result) = stream.next().await {
match result {
Ok(info) => {
if let Some(status) = &info.status {
tracing::debug!(status = %status, "Image pull progress");
}
}
Err(e) => return Err(e.into()),
}
}
info!(image = %image, "Image pulled successfully");
Ok(())
}
/// Create and configure a container for a game server.
pub async fn create_container(&self, spec: &ServerSpec) -> Result<String> {
let name = container_name(&spec.uuid);
// Build port bindings
let mut port_bindings: HashMap<String, Option<Vec<PortBinding>>> = HashMap::new();
for port_map in &spec.ports {
let container_port = format!("{}/{}", port_map.container_port, port_map.protocol);
port_bindings.insert(
container_port,
Some(vec![PortBinding {
host_ip: Some("0.0.0.0".to_string()),
host_port: Some(port_map.host_port.to_string()),
}]),
);
}
// Build exposed ports
let mut exposed_ports: HashMap<String, HashMap<(), ()>> = HashMap::new();
for port_map in &spec.ports {
let container_port = format!("{}/{}", port_map.container_port, port_map.protocol);
exposed_ports.insert(container_port, HashMap::new());
}
// Convert env map to Docker format
let env: Vec<String> = spec
.environment
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
let host_config = HostConfig {
memory: Some(spec.memory_limit),
memory_swap: Some(spec.memory_limit), // no swap
nano_cpus: Some((spec.cpu_limit as i64) * 10_000_000), // cpu_limit=100 means 1 core
port_bindings: Some(port_bindings),
network_mode: Some(self.network_name().to_string()),
binds: Some(vec![format!(
"{}:/data",
spec.data_path.display()
)]),
..Default::default()
};
let config = Config {
image: Some(spec.docker_image.clone()),
hostname: Some(spec.uuid.clone()),
env: Some(env),
exposed_ports: Some(exposed_ports),
host_config: Some(host_config),
working_dir: Some("/data".to_string()),
cmd: if spec.startup_command.is_empty() {
None
} else {
Some(
spec.startup_command
.split_whitespace()
.map(String::from)
.collect(),
)
},
tty: Some(true),
attach_stdin: Some(true),
attach_stdout: Some(true),
attach_stderr: Some(true),
open_stdin: Some(true),
..Default::default()
};
let options = CreateContainerOptions { name: name.as_str(), platform: None };
let response = self.client().create_container(Some(options), config).await?;
info!(container_id = %response.id, uuid = %spec.uuid, "Container created");
Ok(response.id)
}
/// Start a container.
pub async fn start_container(&self, server_uuid: &str) -> Result<()> {
let name = container_name(server_uuid);
self.client()
.start_container(&name, None::<StartContainerOptions<String>>)
.await?;
info!(uuid = %server_uuid, "Container started");
Ok(())
}
/// Stop a container gracefully.
pub async fn stop_container(&self, server_uuid: &str, timeout_secs: i64) -> Result<()> {
let name = container_name(server_uuid);
self.client()
.stop_container(
&name,
Some(StopContainerOptions {
t: timeout_secs,
}),
)
.await?;
info!(uuid = %server_uuid, "Container stopped");
Ok(())
}
/// Kill a container immediately.
pub async fn kill_container(&self, server_uuid: &str) -> Result<()> {
let name = container_name(server_uuid);
self.client()
.kill_container::<String>(&name, None)
.await?;
info!(uuid = %server_uuid, "Container killed");
Ok(())
}
/// Remove a container and its volumes.
pub async fn remove_container(&self, server_uuid: &str) -> Result<()> {
let name = container_name(server_uuid);
self.client()
.remove_container(
&name,
Some(RemoveContainerOptions {
force: true,
v: true,
..Default::default()
}),
)
.await?;
info!(uuid = %server_uuid, "Container removed");
Ok(())
}
/// Get container stats (CPU, memory, network).
pub async fn container_stats(
&self,
server_uuid: &str,
) -> Result<Stats> {
let name = container_name(server_uuid);
let mut stream = self.client().stats(
&name,
Some(StatsOptions {
stream: false,
one_shot: true,
..Default::default()
}),
);
match stream.next().await {
Some(Ok(stats)) => Ok(stats),
Some(Err(e)) => Err(e.into()),
None => Err(anyhow::anyhow!("No stats returned")),
}
}
/// Check if a container exists and return its state.
pub async fn container_state(
&self,
server_uuid: &str,
) -> Result<Option<String>> {
let name = container_name(server_uuid);
match self.client().inspect_container(&name, None).await {
Ok(info) => {
let state = info
.state
.and_then(|s| s.status)
.map(|s| format!("{:?}", s));
Ok(state)
}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 404, ..
}) => Ok(None),
Err(e) => Err(e.into()),
}
}
/// Stream container logs (stdout + stderr). Returns an owned stream.
pub fn stream_logs(
self: &Arc<Self>,
server_uuid: &str,
) -> impl futures::Stream<Item = Result<String, bollard::errors::Error>> + Send + 'static {
let name = container_name(server_uuid);
let options = LogsOptions::<String> {
follow: true,
stdout: true,
stderr: true,
tail: "100".to_string(),
..Default::default()
};
let client = self.client().clone();
client.logs(&name, Some(options)).map(|result| {
result.map(|output| output.to_string())
})
}
/// Send a command to a container via exec (attach to stdin).
pub async fn send_command(&self, server_uuid: &str, command: &str) -> Result<()> {
let name = container_name(server_uuid);
let exec = self
.client()
.create_exec(
&name,
bollard::exec::CreateExecOptions {
cmd: Some(vec!["sh", "-c", &format!("echo '{}' > /proc/1/fd/0", command)]),
attach_stdout: Some(true),
attach_stderr: Some(true),
..Default::default()
},
)
.await?;
self.client()
.start_exec(&exec.id, None::<bollard::exec::StartExecOptions>)
.await?;
Ok(())
}
}
+77
View File
@@ -0,0 +1,77 @@
use anyhow::Result;
use bollard::Docker;
use bollard::network::CreateNetworkOptions;
use tracing::info;
use crate::config::DockerConfig;
/// Manages the Docker client and network setup.
#[derive(Clone)]
pub struct DockerManager {
client: Docker,
network_name: String,
}
impl DockerManager {
pub async fn new(config: &DockerConfig) -> Result<Self> {
let client = Docker::connect_with_socket(
&config.socket,
120, // timeout
bollard::API_DEFAULT_VERSION,
)?;
// Verify connection
let version = client.version().await?;
info!(
docker_version = version.version.as_deref().unwrap_or("unknown"),
"Connected to Docker"
);
let manager = Self {
client,
network_name: config.network.clone(),
};
manager.ensure_network(&config.network_subnet).await?;
Ok(manager)
}
pub fn client(&self) -> &Docker {
&self.client
}
pub fn network_name(&self) -> &str {
&self.network_name
}
async fn ensure_network(&self, subnet: &str) -> Result<()> {
let networks = self.client.list_networks::<String>(None).await?;
let exists = networks
.iter()
.any(|n| n.name.as_deref() == Some(&self.network_name));
if !exists {
info!(network = %self.network_name, "Creating Docker network");
let ipam_config = bollard::models::IpamConfig {
subnet: Some(subnet.to_string()),
..Default::default()
};
let ipam = bollard::models::Ipam {
config: Some(vec![ipam_config]),
..Default::default()
};
self.client
.create_network(CreateNetworkOptions {
name: self.network_name.clone(),
driver: "bridge".to_string(),
ipam,
..Default::default()
})
.await?;
info!(network = %self.network_name, "Docker network created");
}
Ok(())
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod container;
pub mod manager;
pub use manager::DockerManager;