From 474c033fd40cd38e4e8fb15d2284a581c4b7d0a7 Mon Sep 17 00:00:00 2001 From: Lu Yang Date: Fri, 25 Oct 2024 22:18:01 +0100 Subject: [PATCH] add workspace port label --- lapdev-api/src/workspace.rs | 1 + lapdev-common/src/devcontainer.rs | 11 ++- lapdev-common/src/lib.rs | 8 +- lapdev-conductor/src/scheduler.rs | 1 + lapdev-conductor/src/server.rs | 30 +++++++- lapdev-dashboard/src/workspace.rs | 8 +- lapdev-db/src/entities/workspace.rs | 1 + lapdev-db/src/entities/workspace_port.rs | 1 + ...m20231106_100804_create_workspace_table.rs | 2 + ...0316_194115_create_workspace_port_table.rs | 4 +- lapdev-enterprise/src/auto_start_stop.rs | 2 + lapdev-enterprise/src/quota.rs | 1 + lapdev-guest-agent/src/lib.rs | 6 +- lapdev-ws/src/server.rs | 75 +++++++++++-------- lapdev-ws/src/service.rs | 35 +++++---- 15 files changed, 125 insertions(+), 61 deletions(-) diff --git a/lapdev-api/src/workspace.rs b/lapdev-api/src/workspace.rs index 9e029bc..ca6020f 100644 --- a/lapdev-api/src/workspace.rs +++ b/lapdev-api/src/workspace.rs @@ -291,6 +291,7 @@ pub async fn workspace_ports( port: p.port as u16, shared: p.shared, public: p.public, + label: p.label, }) .collect::>(), ) diff --git a/lapdev-common/src/devcontainer.rs b/lapdev-common/src/devcontainer.rs index 9181e6c..c6f2958 100644 --- a/lapdev-common/src/devcontainer.rs +++ b/lapdev-common/src/devcontainer.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; pub type DevContainerCwd = std::path::PathBuf; @@ -18,7 +18,8 @@ pub struct DevContainerConfig { #[serde(default)] pub run_args: Vec, pub docker_compose_file: Option, - + #[serde(default)] + pub ports_attributes: HashMap, pub service: Option, } @@ -45,3 +46,9 @@ pub struct BuildConfig { #[serde(default)] pub args: HashMap, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PortAttribute { + pub label: Option, +} diff --git a/lapdev-common/src/lib.rs b/lapdev-common/src/lib.rs index 6d40944..43b1f0b 100644 --- a/lapdev-common/src/lib.rs +++ b/lapdev-common/src/lib.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use chrono::{DateTime, FixedOffset}; +use devcontainer::PortAttribute; use serde::{Deserialize, Serialize}; use strum_macros::EnumString; use uuid::Uuid; @@ -143,10 +144,14 @@ pub struct RepoContent { #[derive(Serialize, Deserialize, Debug, Clone)] pub enum RepoBuildOutput { - Compose(Vec), + Compose { + services: Vec, + ports_attributes: HashMap, + }, Image { image: String, info: ContainerImageInfo, + ports_attributes: HashMap, }, } @@ -321,6 +326,7 @@ pub struct WorkspacePort { pub port: u16, pub shared: bool, pub public: bool, + pub label: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] diff --git a/lapdev-conductor/src/scheduler.rs b/lapdev-conductor/src/scheduler.rs index a692dca..eb5eb83 100644 --- a/lapdev-conductor/src/scheduler.rs +++ b/lapdev-conductor/src/scheduler.rs @@ -425,6 +425,7 @@ mod tests { compose_parent: ActiveValue::Set(None), auto_stop: ActiveValue::Set(None), build_output: ActiveValue::Set(None), + updated_at: ActiveValue::Set(None), deleted_at: ActiveValue::Set(None), env: ActiveValue::Set(None), last_inactivity: ActiveValue::Set(None), diff --git a/lapdev-conductor/src/server.rs b/lapdev-conductor/src/server.rs index 78ca518..418216f 100644 --- a/lapdev-conductor/src/server.rs +++ b/lapdev-conductor/src/server.rs @@ -1146,6 +1146,7 @@ impl Conductor { let ws = entities::workspace::ActiveModel { id: ActiveValue::Set(workspace_id), deleted_at: ActiveValue::Set(None), + updated_at: ActiveValue::Set(None), name: ActiveValue::Set(name.clone()), created_at: ActiveValue::Set(now.into()), status: ActiveValue::Set(WorkspaceStatus::New.to_string()), @@ -1671,8 +1672,11 @@ impl Conductor { .iter() .map(|(k, v)| format!("{k}={v}")) .collect::>(); - let (is_compose, images) = match &output { - RepoBuildOutput::Compose(services) => ( + let (is_compose, images, ports_attributes) = match &output { + RepoBuildOutput::Compose { + services, + ports_attributes, + } => ( true, services .iter() @@ -1685,8 +1689,13 @@ impl Conductor { ) }) .collect(), + ports_attributes, ), - RepoBuildOutput::Image { image, info } => ( + RepoBuildOutput::Image { + image, + info, + ports_attributes, + } => ( false, vec![( None, @@ -1694,6 +1703,7 @@ impl Conductor { info.config.env.clone().unwrap_or_default(), vec![], )], + ports_attributes, ), }; @@ -1822,6 +1832,7 @@ impl Conductor { id: ActiveValue::Set(Uuid::new_v4()), name: ActiveValue::Set(workspace_name), created_at: ActiveValue::Set(Utc::now().into()), + updated_at: ActiveValue::Set(None), deleted_at: ActiveValue::Set(None), status: ActiveValue::Set(WorkspaceStatus::Running.to_string()), repo_url: ActiveValue::Set(ws.repo_url.clone()), @@ -1863,6 +1874,11 @@ impl Conductor { host_port: ActiveValue::Set(host_port as i32), shared: ActiveValue::Set(false), public: ActiveValue::Set(false), + label: ActiveValue::Set( + ports_attributes + .get(&port.to_string()) + .and_then(|a| a.label.clone()), + ), } .insert(&self.db.conn) .await?; @@ -2192,7 +2208,7 @@ impl Conductor { if let Some(output) = output { match output { - RepoBuildOutput::Compose(services) => { + RepoBuildOutput::Compose { services, .. } => { services.into_iter().map(|s| s.image).collect() } RepoBuildOutput::Image { image, .. } => vec![image], @@ -2335,6 +2351,7 @@ impl Conductor { .await?; let update_ws = entities::workspace::ActiveModel { id: ActiveValue::Set(workspace.id), + updated_at: ActiveValue::Set(Some(now.into())), status: ActiveValue::Set(WorkspaceStatus::Starting.to_string()), ..Default::default() }; @@ -2410,6 +2427,7 @@ impl Conductor { let update_ws = entities::workspace::ActiveModel { id: ActiveValue::Set(workspace.id), status: ActiveValue::Set(WorkspaceStatus::Stopping.to_string()), + updated_at: ActiveValue::Set(Some(now.into())), ..Default::default() }; let ws = update_ws.update(&txn).await?; @@ -2496,6 +2514,7 @@ impl Conductor { status: ActiveValue::Set(status.to_string()), usage_id: ActiveValue::Set(usage.map(|u| u.id)), last_inactivity: ActiveValue::Set(None), + updated_at: ActiveValue::Set(Some(now.into())), ..Default::default() } .update(&txn) @@ -2513,6 +2532,7 @@ impl Conductor { let status = WorkspaceStatus::Failed; entities::workspace::ActiveModel { id: ActiveValue::Set(ws.id), + updated_at: ActiveValue::Set(Some(now.into())), status: ActiveValue::Set(WorkspaceStatus::Failed.to_string()), ..Default::default() } @@ -2553,6 +2573,7 @@ impl Conductor { entities::workspace::ActiveModel { id: ActiveValue::Set(ws.id), status: ActiveValue::Set(status.to_string()), + updated_at: ActiveValue::Set(Some(now.into())), usage_id: ActiveValue::Set(None), ..Default::default() } @@ -2571,6 +2592,7 @@ impl Conductor { entities::workspace::ActiveModel { id: ActiveValue::Set(ws.id), status: ActiveValue::Set(status.to_string()), + updated_at: ActiveValue::Set(Some(now.into())), ..Default::default() } .update(&self.db.conn) diff --git a/lapdev-dashboard/src/workspace.rs b/lapdev-dashboard/src/workspace.rs index 8e9f1fd..0a804f3 100644 --- a/lapdev-dashboard/src/workspace.rs +++ b/lapdev-dashboard/src/workspace.rs @@ -1441,7 +1441,13 @@ fn WorkspacePortView( view! {
- {p.port} + { + if let Some(label) = p.label.as_ref() { + format!("{label} ({})", p.port) + } else { + p.port.to_string() + } + }
, pub deleted_at: Option, pub organization_id: Uuid, pub user_id: Uuid, diff --git a/lapdev-db/src/entities/workspace_port.rs b/lapdev-db/src/entities/workspace_port.rs index 777afbc..4f7767e 100644 --- a/lapdev-db/src/entities/workspace_port.rs +++ b/lapdev-db/src/entities/workspace_port.rs @@ -12,6 +12,7 @@ pub struct Model { pub host_port: i32, pub shared: bool, pub public: bool, + pub label: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/lapdev-db/src/migration/m20231106_100804_create_workspace_table.rs b/lapdev-db/src/migration/m20231106_100804_create_workspace_table.rs index 3946bec..bcb21cd 100644 --- a/lapdev-db/src/migration/m20231106_100804_create_workspace_table.rs +++ b/lapdev-db/src/migration/m20231106_100804_create_workspace_table.rs @@ -27,6 +27,7 @@ impl MigrationTrait for Migration { .timestamp_with_time_zone() .not_null(), ) + .col(ColumnDef::new(Workspace::UpdatedAt).timestamp_with_time_zone()) .col(ColumnDef::new(Workspace::DeletedAt).timestamp_with_time_zone()) .col(ColumnDef::new(Workspace::OrganizationId).uuid().not_null()) .col(ColumnDef::new(Workspace::UserId).uuid().not_null()) @@ -141,6 +142,7 @@ pub enum Workspace { Table, Id, CreatedAt, + UpdatedAt, DeletedAt, OrganizationId, UserId, diff --git a/lapdev-db/src/migration/m20240316_194115_create_workspace_port_table.rs b/lapdev-db/src/migration/m20240316_194115_create_workspace_port_table.rs index 68fb519..f3bdf3b 100644 --- a/lapdev-db/src/migration/m20240316_194115_create_workspace_port_table.rs +++ b/lapdev-db/src/migration/m20240316_194115_create_workspace_port_table.rs @@ -23,6 +23,8 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(WorkspacePort::Port).integer().not_null()) .col(ColumnDef::new(WorkspacePort::HostPort).integer().not_null()) .col(ColumnDef::new(WorkspacePort::Shared).boolean().not_null()) + .col(ColumnDef::new(WorkspacePort::Public).boolean().not_null()) + .col(ColumnDef::new(WorkspacePort::Label).string()) .foreign_key( ForeignKey::create() .from_tbl(WorkspacePort::Table) @@ -41,7 +43,6 @@ impl MigrationTrait for Migration { .table(WorkspacePort::Table) .col(WorkspacePort::WorkspaceId) .col(WorkspacePort::Port) - .col(WorkspacePort::Public) .to_owned(), ) .await?; @@ -59,4 +60,5 @@ enum WorkspacePort { HostPort, Shared, Public, + Label, } diff --git a/lapdev-enterprise/src/auto_start_stop.rs b/lapdev-enterprise/src/auto_start_stop.rs index f2ba294..45c97df 100644 --- a/lapdev-enterprise/src/auto_start_stop.rs +++ b/lapdev-enterprise/src/auto_start_stop.rs @@ -218,6 +218,7 @@ pub mod tests { entities::workspace::ActiveModel { id: ActiveValue::Set(Uuid::new_v4()), + updated_at: ActiveValue::Set(None), deleted_at: ActiveValue::Set(None), name: ActiveValue::Set(utils::rand_string(10)), created_at: ActiveValue::Set(Utc::now().into()), @@ -261,6 +262,7 @@ pub mod tests { let ws = entities::workspace::ActiveModel { id: ActiveValue::Set(Uuid::new_v4()), + updated_at: ActiveValue::Set(None), deleted_at: ActiveValue::Set(None), name: ActiveValue::Set(utils::rand_string(10)), created_at: ActiveValue::Set(Utc::now().into()), diff --git a/lapdev-enterprise/src/quota.rs b/lapdev-enterprise/src/quota.rs index 285b95f..f51c3f9 100644 --- a/lapdev-enterprise/src/quota.rs +++ b/lapdev-enterprise/src/quota.rs @@ -399,6 +399,7 @@ pub mod tests { ) -> Result { let ws = entities::workspace::ActiveModel { id: ActiveValue::Set(Uuid::new_v4()), + updated_at: ActiveValue::Set(None), deleted_at: ActiveValue::Set(None), name: ActiveValue::Set(utils::rand_string(10)), created_at: ActiveValue::Set(Utc::now().into()), diff --git a/lapdev-guest-agent/src/lib.rs b/lapdev-guest-agent/src/lib.rs index 858c82e..2098fd1 100644 --- a/lapdev-guest-agent/src/lib.rs +++ b/lapdev-guest-agent/src/lib.rs @@ -60,7 +60,7 @@ fn run_sshd() -> Result<(), LapdevGuestAgentError> { .arg("-D") .arg("-o") .arg("AcceptEnv=*") - .status()?; + .output()?; Ok(()) } @@ -82,7 +82,7 @@ fn run_cmd(cmd: Vec) -> Result<(), LapdevGuestAgentError> { return Err(LapdevGuestAgentError::Cmds("empty cmd".to_string())); } thread::spawn(move || { - if let Err(e) = Command::new("sh").arg("-c").arg(cmd.join(" ")).status() { + if let Err(e) = Command::new("sh").arg("-c").arg(cmd.join(" ")).output() { eprintln!("run cmd error: {e:?}"); } }); @@ -97,6 +97,6 @@ fn run_ide_cmds() -> Result<(), LapdevGuestAgentError> { if cmds.is_empty() { return Err(LapdevGuestAgentError::Cmds("empty cmd".to_string())); } - Command::new(&cmds[0]).args(&cmds[1..]).status()?; + Command::new(&cmds[0]).args(&cmds[1..]).output()?; Ok(()) } diff --git a/lapdev-ws/src/server.rs b/lapdev-ws/src/server.rs index 5555f09..26bebd3 100644 --- a/lapdev-ws/src/server.rs +++ b/lapdev-ws/src/server.rs @@ -19,7 +19,7 @@ use lapdev_common::{ devcontainer::{ DevContainerCmd, DevContainerConfig, DevContainerCwd, DevContainerLifeCycleCmd, }, - BuildTarget, ContainerImageInfo, CpuCore, GitBranch, ProjectRequest, RepoBuildInfo, + utils, BuildTarget, ContainerImageInfo, CpuCore, GitBranch, ProjectRequest, RepoBuildInfo, RepoBuildOutput, RepoComposeService, }; use lapdev_rpc::{ @@ -292,29 +292,30 @@ impl WorkspaceServer { Ok(()) } - fn podman_socket(&self, uid: &str) -> String { - format!("/run/user/{uid}/podman/podman.sock") - } + pub async fn get_podman_socket(&self, osuser: &str) -> Result { + let _ = self.os_user_uid(osuser).await?; + let name = format!("/tmp/{osuser}-{}.sock", utils::rand_string(10)); + let _ = Command::new("su") + .arg("-") + .arg(osuser) + .arg("-c") + .arg(format!("podman system service --time=60 unix://{name}")) + .spawn()?; - async fn check_podman_socket(&self, osuser: &str, uid: &str) { - tracing::debug!("check podman socket"); - if !tokio::fs::try_exists(self.podman_socket(uid)) - .await - .unwrap_or(false) - { - tracing::debug!("podman socket doens't exist, start system service"); - let osuser = osuser.to_string(); - tokio::spawn(async move { - let _ = Command::new("su") - .arg("-") - .arg(osuser) - .arg("-c") - .arg("podman system service --time=0") - .output() - .await; - }); - tokio::time::sleep(Duration::from_secs(1)).await; + let mut n = 0; + loop { + if tokio::fs::try_exists(&name).await.unwrap_or(false) { + break; + } + if n > 10 { + return Err(ApiError::InternalError( + "can't get podman socket".to_string(), + )); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + n += 1; } + Ok(PathBuf::from(name)) } async fn _os_user_uid(&self, osuser: &str) -> Result { @@ -322,7 +323,6 @@ impl WorkspaceServer { if let Ok(output) = output { if output.status.success() { let uid = String::from_utf8(output.stdout)?.trim().to_string(); - self.check_podman_socket(osuser, &uid).await; return Ok(uid); } } @@ -667,8 +667,7 @@ driver = "overlay" osuser: &str, image: &str, ) -> Result { - let uid = self.os_user_uid(osuser).await?; - let socket = &format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.get_podman_socket(osuser).await?; let url = Uri::new(socket, &format!("/images/{image}/json")); let client = unix_client(); let req = hyper::Request::builder() @@ -755,6 +754,7 @@ driver = "overlay" &self, conductor_client: &ConductorServiceClient, info: &RepoBuildInfo, + config: &DevContainerConfig, compose_file: &Path, tag: &str, ) -> Result { @@ -782,7 +782,10 @@ driver = "overlay" }); } } - Ok(RepoBuildOutput::Compose(services)) + Ok(RepoBuildOutput::Compose { + services, + ports_attributes: config.ports_attributes.clone(), + }) } pub fn repo_target_image_tag(&self, target: &BuildTarget) -> String { @@ -825,7 +828,7 @@ driver = "overlay" cmd: &DevContainerLifeCycleCmd, ) -> Result<()> { match output { - RepoBuildOutput::Compose(services) => { + RepoBuildOutput::Compose { services, .. } => { let cmd = match cmd { DevContainerLifeCycleCmd::Simple(cmd) => { DevContainerCmd::Simple(cmd.to_string()) @@ -973,8 +976,14 @@ driver = "overlay" })?; let tag = self.repo_target_image_tag(&info.target); let output = if let Some(compose_file) = &config.docker_compose_file { - self.build_compose(conductor_client, info, &cwd.join(compose_file), &tag) - .await? + self.build_compose( + conductor_client, + info, + &config, + &cwd.join(compose_file), + &tag, + ) + .await? } else if let Some(build) = config.build.as_ref() { let build = AdvancedBuildStep { context: build.context.clone().unwrap_or(".".to_string()), @@ -987,6 +996,7 @@ driver = "overlay" RepoBuildOutput::Image { image: tag.clone(), info, + ports_attributes: config.ports_attributes.clone(), } } else if let Some(image) = config.image.as_ref() { let info = self @@ -995,6 +1005,7 @@ driver = "overlay" RepoBuildOutput::Image { image: tag.clone(), info, + ports_attributes: config.ports_attributes.clone(), } } else { return Err(ApiError::RepositoryInvalid( @@ -1174,8 +1185,7 @@ driver = "overlay" } pub async fn delete_image(&self, osuser: &str, image: &str) -> Result<(), ApiError> { - let uid = self.os_user_uid(osuser).await?; - let socket = format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.get_podman_socket(osuser).await?; let client = unix_client(); { @@ -1199,8 +1209,7 @@ driver = "overlay" } pub async fn delete_network(&self, osuser: &str, network: &str) -> Result<(), ApiError> { - let uid = self.os_user_uid(osuser).await?; - let socket = format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.get_podman_socket(osuser).await?; let client = unix_client(); { diff --git a/lapdev-ws/src/service.rs b/lapdev-ws/src/service.rs index 6a5babb..16e97db 100644 --- a/lapdev-ws/src/service.rs +++ b/lapdev-ws/src/service.rs @@ -88,10 +88,9 @@ impl WorkspaceService for WorkspaceRpcService { .await?; let client = unix_client(); - let uid = self.server.os_user_uid(&ws_req.osuser).await?; - let socket = &format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.server.get_podman_socket(&ws_req.osuser).await?; if ws_req.create_network { - let url = Uri::new(socket, "/networks/create"); + let url = Uri::new(&socket, "/networks/create"); let body = serde_json::to_string(&NewContainerNetwork { name: ws_req.network_name.clone(), })?; @@ -157,7 +156,7 @@ impl WorkspaceService for WorkspaceRpcService { ])?; let url = Uri::new( - socket, + &socket, &format!("/containers/create?name={}", ws_req.workspace_name), ); let mut env = ws_req.env.clone(); @@ -266,11 +265,10 @@ impl WorkspaceService for WorkspaceRpcService { _context: context::Context, ws_req: StartWorkspaceRequest, ) -> Result { - let uid = self.server.os_user_uid(&ws_req.osuser).await?; - let socket = &format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.server.get_podman_socket(&ws_req.osuser).await?; let client = unix_client(); let url = Uri::new( - socket, + &socket, &format!("/containers/{}/start", ws_req.workspace_name), ); let req = hyper::Request::builder() @@ -286,7 +284,7 @@ impl WorkspaceService for WorkspaceRpcService { } let url = Uri::new( - socket, + &socket, &format!("/containers/{}/json", ws_req.workspace_name), ); let req = hyper::Request::builder() @@ -309,8 +307,7 @@ impl WorkspaceService for WorkspaceRpcService { _context: context::Context, req: DeleteWorkspaceRequest, ) -> Result<(), ApiError> { - let uid = self.server.os_user_uid(&req.osuser).await?; - let socket = format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.server.get_podman_socket(&req.osuser).await?; let client = unix_client(); { @@ -370,8 +367,7 @@ impl WorkspaceService for WorkspaceRpcService { _context: context::Context, ws_req: StopWorkspaceRequest, ) -> Result<(), ApiError> { - let uid = self.server.os_user_uid(&ws_req.osuser).await?; - let socket = &format!("/run/user/{uid}/podman/podman.sock"); + let socket = self.server.get_podman_socket(&ws_req.osuser).await?; let client = unix_client(); let url = Uri::new( socket, @@ -470,6 +466,7 @@ impl WorkspaceService for WorkspaceRpcService { RepoBuildOutput::Image { image, info: ContainerImageInfo::default(), + ports_attributes: Default::default(), } } @@ -482,7 +479,9 @@ impl WorkspaceService for WorkspaceRpcService { target: BuildTarget, ) -> Result<(), ApiError> { let images = match output { - RepoBuildOutput::Compose(services) => services.into_iter().map(|s| s.image).collect(), + RepoBuildOutput::Compose { services, .. } => { + services.into_iter().map(|s| s.image).collect() + } RepoBuildOutput::Image { image, .. } => vec![image], }; @@ -606,7 +605,9 @@ impl WorkspaceService for WorkspaceRpcService { } let images = match output { - RepoBuildOutput::Compose(services) => services.into_iter().map(|s| s.image).collect(), + RepoBuildOutput::Compose { services, .. } => { + services.into_iter().map(|s| s.image).collect() + } RepoBuildOutput::Image { image, .. } => vec![image], }; @@ -652,7 +653,7 @@ impl WorkspaceService for WorkspaceRpcService { if let Some(output) = output { let images = match output { - RepoBuildOutput::Compose(services) => { + RepoBuildOutput::Compose { services, .. } => { services.into_iter().map(|s| s.image).collect() } RepoBuildOutput::Image { image, .. } => vec![image], @@ -689,7 +690,9 @@ impl WorkspaceService for WorkspaceRpcService { let mut files = vec!["repo.tar.zst".to_string()]; let images = match output { - RepoBuildOutput::Compose(services) => services.into_iter().map(|s| s.image).collect(), + RepoBuildOutput::Compose { services, .. } => { + services.into_iter().map(|s| s.image).collect() + } RepoBuildOutput::Image { image, .. } => vec![image], }; for image in images {