use super::{Identity, Template};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum IsolationLevel {
Podman,
Namespace,
#[default]
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpaceConfig {
pub isolation: IsolationLevel,
pub template: Option<String>,
pub workdir: PathBuf,
pub env: Vec<(String, String)>,
pub mounts: Vec<(PathBuf, PathBuf)>,
pub memory_limit: u64,
pub cpu_limit: f32,
pub network: NetworkConfig,
}
impl Default for SpaceConfig {
fn default() -> Self {
SpaceConfig {
isolation: IsolationLevel::None,
template: None,
workdir: PathBuf::from("."),
env: Vec::new(),
mounts: Vec::new(),
memory_limit: 0,
cpu_limit: 0.0,
network: NetworkConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NetworkConfig {
pub internet: bool,
pub host_access: bool,
pub exposed_ports: Vec<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSpace {
pub identity: Identity,
pub config: SpaceConfig,
pub container_id: Option<String>,
pub socket_path: Option<PathBuf>,
pub shell_pid: Option<u32>,
pub created_at: u64,
pub last_active: u64,
}
impl UserSpace {
pub fn new(identity: Identity, config: SpaceConfig) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
UserSpace {
identity,
config,
container_id: None,
socket_path: None,
shell_pid: None,
created_at: now,
last_active: now,
}
}
pub fn with_template(identity: Identity, template: &Template) -> Self {
let config = SpaceConfig {
template: Some(template.name.clone()),
isolation: template.default_isolation.clone(),
env: template.env.clone(),
..Default::default()
};
Self::new(identity, config)
}
pub async fn start(&mut self) -> Result<()> {
match self.config.isolation {
IsolationLevel::Podman => self.start_podman().await,
IsolationLevel::Namespace => self.start_namespace().await,
IsolationLevel::None => self.start_direct().await,
}
}
async fn start_podman(&mut self) -> Result<()> {
tracing::info!("Starting podman container for {}", self.identity);
Ok(())
}
async fn start_namespace(&mut self) -> Result<()> {
tracing::info!("Starting namespace for {}", self.identity);
Ok(())
}
async fn start_direct(&mut self) -> Result<()> {
tracing::info!("Starting direct space for {}", self.identity);
Ok(())
}
pub async fn stop(&mut self) -> Result<()> {
match self.config.isolation {
IsolationLevel::Podman => {
if let Some(ref id) = self.container_id {
tracing::info!("Stopping podman container {}", id);
}
}
IsolationLevel::Namespace => {
if let Some(pid) = self.shell_pid {
tracing::info!("Stopping namespace pid {}", pid);
}
}
IsolationLevel::None => {
}
}
self.container_id = None;
self.shell_pid = None;
Ok(())
}
pub async fn exec(&self, command: &[&str]) -> Result<String> {
match self.config.isolation {
IsolationLevel::Podman => {
if let Some(ref id) = self.container_id {
tracing::debug!("Exec in podman {}: {:?}", id, command);
}
}
IsolationLevel::Namespace => {
if let Some(pid) = self.shell_pid {
tracing::debug!("Exec in namespace {}: {:?}", pid, command);
}
}
IsolationLevel::None => {
tracing::debug!("Direct exec: {:?}", command);
}
}
Ok(String::new())
}
pub async fn share_terminal(&self, with: &Identity) -> Result<String> {
let session_id = format!(
"term-{}-{}",
self.identity.username,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
);
tracing::info!(
"Sharing terminal {} with {}",
session_id,
with.canonical()
);
Ok(session_id)
}
pub fn touch(&mut self) {
self.last_active = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
}
pub fn is_idle(&self, timeout_secs: u64) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
now - self.last_active > timeout_secs
}
}
#[derive(Debug, Default)]
pub struct SpaceManager {
spaces: std::collections::HashMap<String, UserSpace>,
}
impl SpaceManager {
pub fn new() -> Self {
SpaceManager {
spaces: std::collections::HashMap::new(),
}
}
pub async fn get_or_create(
&mut self,
identity: Identity,
config: SpaceConfig,
) -> Result<&mut UserSpace> {
let key = identity.canonical();
if !self.spaces.contains_key(&key) {
let mut space = UserSpace::new(identity, config);
space.start().await?;
self.spaces.insert(key.clone(), space);
}
Ok(self.spaces.get_mut(&key).unwrap())
}
pub fn get(&self, identity: &Identity) -> Option<&UserSpace> {
self.spaces.get(&identity.canonical())
}
pub async fn remove(&mut self, identity: &Identity) -> Result<()> {
let key = identity.canonical();
if let Some(mut space) = self.spaces.remove(&key) {
space.stop().await?;
}
Ok(())
}
pub fn list(&self) -> Vec<&UserSpace> {
self.spaces.values().collect()
}
pub async fn cleanup_idle(&mut self, timeout_secs: u64) -> Result<usize> {
let idle: Vec<String> = self
.spaces
.iter()
.filter(|(_, s)| s.is_idle(timeout_secs))
.map(|(k, _)| k.clone())
.collect();
let count = idle.len();
for key in idle {
if let Some(mut space) = self.spaces.remove(&key) {
space.stop().await?;
}
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_space_config_default() {
let config = SpaceConfig::default();
assert_eq!(config.isolation, IsolationLevel::None);
assert!(config.template.is_none());
}
#[test]
fn test_user_space_creation() {
let identity = Identity::local("test");
let space = UserSpace::new(identity.clone(), SpaceConfig::default());
assert_eq!(space.identity, identity);
assert!(space.container_id.is_none());
}
}