From 1d71877af67e248096f6075ec3baf90a4f1ca965 Mon Sep 17 00:00:00 2001 From: Huskies Agent Date: Fri, 17 Jul 2026 10:39:35 +0000 Subject: [PATCH] =?UTF-8?q?huskies:=20merge=201182=20bug=20Sled=20uplinks?= =?UTF-8?q?=20all=20register=20as=20'workspace'=20=E2=80=94=20containers?= =?UTF-8?q?=20never=20get=20HUSKIES=5FPROJECT=5FNAME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/chat/transport/matrix/new_project.rs | 31 +++++++++++ .../chat/transport/matrix/project_rebuild.rs | 1 + server/src/gateway_relay.rs | 1 + server/src/http/gateway/websocket.rs | 51 +++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/server/src/chat/transport/matrix/new_project.rs b/server/src/chat/transport/matrix/new_project.rs index 1c1e50a6..23778380 100644 --- a/server/src/chat/transport/matrix/new_project.rs +++ b/server/src/chat/transport/matrix/new_project.rs @@ -763,6 +763,7 @@ async fn handle_adopt_project( let mut docker_args = project_docker_run_args( &container_name, + name, port, ssh_port, &pubkey, @@ -1150,6 +1151,7 @@ pub async fn handle_new_project( // string that is returned to the caller or written to a log. let mut docker_args = project_docker_run_args( &container_name, + name, port, ssh_port, &pubkey, @@ -1361,6 +1363,7 @@ pub async fn build_project_image( #[allow(clippy::too_many_arguments)] pub(crate) fn project_docker_run_args( container_name: &str, + project_name: &str, port: u16, ssh_port: u16, pubkey: &str, @@ -1389,6 +1392,8 @@ pub(crate) fn project_docker_run_args( "-e".into(), "HUSKIES_PORT=3001".into(), "-e".into(), + format!("HUSKIES_PROJECT_NAME={project_name}"), + "-e".into(), format!("HUSKIES_SSH_PUBKEY={pubkey}"), "-e".into(), format!("GIT_USER_NAME={git_user_name}"), @@ -1839,6 +1844,7 @@ mod tests { fn project_docker_args_include_huskies_host_env() { let args = project_docker_run_args( "huskies-myapp", + "myapp", 3100, 2200, "ssh-ed25519 AAAA...", @@ -1887,10 +1893,33 @@ mod tests { ); } + #[test] + fn project_docker_args_include_project_name_env() { + let args = project_docker_run_args( + "huskies-myapp", + "myapp", + 3100, + 2200, + "ssh-ed25519 AAAA...", + "Test User", + "test@example.com", + None, + "", + ); + let pairs: Vec<_> = args.windows(2).collect(); + assert!( + pairs + .iter() + .any(|w| w[0] == "-e" && w[1] == "HUSKIES_PROJECT_NAME=myapp"), + "expected -e HUSKIES_PROJECT_NAME=myapp in docker args, got: {args:?}" + ); + } + #[test] fn project_docker_args_no_gateway_url_when_empty() { let args = project_docker_run_args( "huskies-myapp", + "myapp", 3100, 2200, "ssh-ed25519 AAAA...", @@ -1910,6 +1939,7 @@ mod tests { let creds = std::path::Path::new("/home/user/.claude/.credentials.json"); let args = project_docker_run_args( "huskies-myapp", + "myapp", 3100, 2200, "ssh-ed25519 AAAA...", @@ -1930,6 +1960,7 @@ mod tests { fn project_docker_args_no_credentials_mount_when_none() { let args = project_docker_run_args( "huskies-myapp", + "myapp", 3100, 2200, "ssh-ed25519 AAAA...", diff --git a/server/src/chat/transport/matrix/project_rebuild.rs b/server/src/chat/transport/matrix/project_rebuild.rs index ca150434..793e8872 100644 --- a/server/src/chat/transport/matrix/project_rebuild.rs +++ b/server/src/chat/transport/matrix/project_rebuild.rs @@ -228,6 +228,7 @@ pub async fn handle_project_rebuild( let mut docker_args = super::new_project::project_docker_run_args( &container_name, + name, port, ssh_port, &pubkey, diff --git a/server/src/gateway_relay.rs b/server/src/gateway_relay.rs index f5f4127e..d0c109be 100644 --- a/server/src/gateway_relay.rs +++ b/server/src/gateway_relay.rs @@ -395,6 +395,7 @@ mod tests { // code path that sets HUSKIES_GATEWAY_URL on the sled container. let docker_args = project_docker_run_args( "huskies-sled-relay", + "sled-relay", 3200, 2300, "ssh-ed25519 AAAA...", diff --git a/server/src/http/gateway/websocket.rs b/server/src/http/gateway/websocket.rs index 59c62250..138b4c64 100644 --- a/server/src/http/gateway/websocket.rs +++ b/server/src/http/gateway/websocket.rs @@ -343,6 +343,12 @@ async fn run_sled_uplink_session( "[gateway/sled-uplink] Sled '{}' connected and registered", identity ); + if !state.projects.read().await.contains_key(&identity) { + crate::slog_warn!( + "[gateway/sled-uplink] Sled registered under name '{}', which matches no project in projects.toml", + identity + ); + } // Aggregator channel for perm responses produced by spawned per-request tasks. let (agg_tx, mut agg_rx) = @@ -734,4 +740,49 @@ mod tests { "with no sled tokens configured anywhere, the uplink must accept unauthenticated" ); } + + #[tokio::test] + async fn unconfigured_project_name_logs_warning() { + let mut projects = BTreeMap::new(); + projects.insert( + "other-project".to_string(), + ProjectEntry::with_url("http://other-project:3001"), + ); + let config = GatewayConfig { + projects, + sled_tokens: BTreeMap::new(), + release_channels: BTreeMap::new(), + }; + let (base_url, _state) = start_test_gateway(config).await; + let url = format!("{base_url}/api/sled-uplink?project=ghost-project"); + + let (mut ws_stream, _) = tokio_tungstenite::connect_async(url.as_str()) + .await + .expect("uplink connect should succeed when no tokens are configured"); + + let identity = crate::sled_uplink::UplinkEnvelope { + msg_type: "identity".to_string(), + req_id: String::new(), + payload: serde_json::json!({ "project": "ghost-project" }), + }; + ws_stream + .send(tokio_tungstenite::tungstenite::Message::Text( + serde_json::to_string(&identity).unwrap().into(), + )) + .await + .unwrap(); + + // Give the server task time to process the identity frame and register. + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + let warnings = crate::log_buffer::global().get_recent( + 50, + Some("ghost-project"), + Some(&crate::log_buffer::LogLevel::Warn), + ); + assert!( + warnings.iter().any(|w| w.contains("matches no project")), + "expected an unconfigured-project warning, got: {warnings:?}" + ); + } }