huskies: merge 1152 story Set HUSKIES_GATEWAY_URL on every sled container so 1136's relay actually spawns
This commit is contained in:
@@ -29,6 +29,9 @@ services:
|
|||||||
- HUSKIES_PORT=3001
|
- HUSKIES_PORT=3001
|
||||||
# Bind to all interfaces so Docker port forwarding works.
|
# Bind to all interfaces so Docker port forwarding works.
|
||||||
- HUSKIES_HOST=0.0.0.0
|
- HUSKIES_HOST=0.0.0.0
|
||||||
|
# Gateway URL so this sled's relay task forwards CRDT events to the gateway.
|
||||||
|
# Uses host.docker.internal so the container can reach the gateway on the host.
|
||||||
|
- HUSKIES_GATEWAY_URL=http://host.docker.internal:3000
|
||||||
# Optional: Matrix bot credentials (if using Matrix integration)
|
# Optional: Matrix bot credentials (if using Matrix integration)
|
||||||
- MATRIX_HOMESERVER=${MATRIX_HOMESERVER:-}
|
- MATRIX_HOMESERVER=${MATRIX_HOMESERVER:-}
|
||||||
- MATRIX_USER=${MATRIX_USER:-}
|
- MATRIX_USER=${MATRIX_USER:-}
|
||||||
|
|||||||
@@ -756,6 +756,7 @@ async fn handle_adopt_project(
|
|||||||
&git_user_name,
|
&git_user_name,
|
||||||
&git_user_email,
|
&git_user_email,
|
||||||
Some(&credentials_file),
|
Some(&credentials_file),
|
||||||
|
&resolve_gateway_url(),
|
||||||
);
|
);
|
||||||
|
|
||||||
docker_args.push("-v".into());
|
docker_args.push("-v".into());
|
||||||
@@ -1142,6 +1143,7 @@ pub async fn handle_new_project(
|
|||||||
&git_user_name,
|
&git_user_name,
|
||||||
&git_user_email,
|
&git_user_email,
|
||||||
Some(&credentials_file),
|
Some(&credentials_file),
|
||||||
|
&resolve_gateway_url(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// HTTPS push token: passed as env vars consumed by the entrypoint credential helper.
|
// HTTPS push token: passed as env vars consumed by the entrypoint credential helper.
|
||||||
@@ -1330,11 +1332,15 @@ pub async fn build_project_image(
|
|||||||
/// Without this the server defaults to `127.0.0.1` inside the container —
|
/// Without this the server defaults to `127.0.0.1` inside the container —
|
||||||
/// reachable only from within the container itself, not via `docker -p`.
|
/// reachable only from within the container itself, not via `docker -p`.
|
||||||
///
|
///
|
||||||
|
/// When `gateway_url` is non-empty, `-e HUSKIES_GATEWAY_URL=<url>` is added so
|
||||||
|
/// the sled's relay task connects back to the gateway and forwards CRDT events.
|
||||||
|
///
|
||||||
/// When `credentials_path` is `Some`, the file is bind-mounted read-only at
|
/// When `credentials_path` is `Some`, the file is bind-mounted read-only at
|
||||||
/// `/run/claude-credentials-src` so the container entrypoint can copy it into
|
/// `/run/claude-credentials-src` so the container entrypoint can copy it into
|
||||||
/// `/home/huskies/.claude/.credentials.json` with mode 0600. Mounting to an
|
/// `/home/huskies/.claude/.credentials.json` with mode 0600. Mounting to an
|
||||||
/// intermediate path (rather than directly to the destination) ensures the
|
/// intermediate path (rather than directly to the destination) ensures the
|
||||||
/// huskies user owns the copy regardless of the host user's UID.
|
/// huskies user owns the copy regardless of the host user's UID.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn project_docker_run_args(
|
pub(crate) fn project_docker_run_args(
|
||||||
container_name: &str,
|
container_name: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -1343,6 +1349,7 @@ pub(crate) fn project_docker_run_args(
|
|||||||
git_user_name: &str,
|
git_user_name: &str,
|
||||||
git_user_email: &str,
|
git_user_email: &str,
|
||||||
credentials_path: Option<&std::path::Path>,
|
credentials_path: Option<&std::path::Path>,
|
||||||
|
gateway_url: &str,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"run".into(),
|
"run".into(),
|
||||||
@@ -1364,6 +1371,10 @@ pub(crate) fn project_docker_run_args(
|
|||||||
"-e".into(),
|
"-e".into(),
|
||||||
format!("GIT_USER_EMAIL={git_user_email}"),
|
format!("GIT_USER_EMAIL={git_user_email}"),
|
||||||
];
|
];
|
||||||
|
if !gateway_url.is_empty() {
|
||||||
|
args.push("-e".into());
|
||||||
|
args.push(format!("HUSKIES_GATEWAY_URL={gateway_url}"));
|
||||||
|
}
|
||||||
if let Some(creds) = credentials_path {
|
if let Some(creds) = credentials_path {
|
||||||
args.push("-v".into());
|
args.push("-v".into());
|
||||||
args.push(format!(
|
args.push(format!(
|
||||||
@@ -1374,6 +1385,16 @@ pub(crate) fn project_docker_run_args(
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the gateway URL to inject into project sled containers.
|
||||||
|
///
|
||||||
|
/// Reads `HUSKIES_GATEWAY_URL` from the environment first; falls back to
|
||||||
|
/// `http://host.docker.internal:3000` so containers launched by the gateway
|
||||||
|
/// can always relay events back without explicit configuration.
|
||||||
|
pub(crate) fn resolve_gateway_url() -> String {
|
||||||
|
std::env::var("HUSKIES_GATEWAY_URL")
|
||||||
|
.unwrap_or_else(|_| "http://host.docker.internal:3000".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a failed `docker run` stderr into an actionable chat message.
|
/// Convert a failed `docker run` stderr into an actionable chat message.
|
||||||
///
|
///
|
||||||
/// When Docker cannot find the image locally it prints `Unable to find image`.
|
/// When Docker cannot find the image locally it prints `Unable to find image`.
|
||||||
@@ -1792,8 +1813,8 @@ mod tests {
|
|||||||
"Test User",
|
"Test User",
|
||||||
"test@example.com",
|
"test@example.com",
|
||||||
None,
|
None,
|
||||||
|
"http://host.docker.internal:3000",
|
||||||
);
|
);
|
||||||
// Find "-e" followed by "HUSKIES_HOST=0.0.0.0"
|
|
||||||
let pairs: Vec<_> = args.windows(2).collect();
|
let pairs: Vec<_> = args.windows(2).collect();
|
||||||
assert!(
|
assert!(
|
||||||
pairs
|
pairs
|
||||||
@@ -1807,6 +1828,31 @@ mod tests {
|
|||||||
.any(|w| w[0] == "-e" && w[1] == "HUSKIES_PORT=3001"),
|
.any(|w| w[0] == "-e" && w[1] == "HUSKIES_PORT=3001"),
|
||||||
"expected -e HUSKIES_PORT=3001 in docker args, got: {args:?}"
|
"expected -e HUSKIES_PORT=3001 in docker args, got: {args:?}"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
pairs
|
||||||
|
.iter()
|
||||||
|
.any(|w| w[0] == "-e"
|
||||||
|
&& w[1] == "HUSKIES_GATEWAY_URL=http://host.docker.internal:3000"),
|
||||||
|
"expected -e HUSKIES_GATEWAY_URL=http://host.docker.internal:3000 in docker args, got: {args:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_docker_args_no_gateway_url_when_empty() {
|
||||||
|
let args = project_docker_run_args(
|
||||||
|
"huskies-myapp",
|
||||||
|
3100,
|
||||||
|
2200,
|
||||||
|
"ssh-ed25519 AAAA...",
|
||||||
|
"Test User",
|
||||||
|
"test@example.com",
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!args.iter().any(|a| a.contains("HUSKIES_GATEWAY_URL")),
|
||||||
|
"expected no HUSKIES_GATEWAY_URL when gateway_url is empty, got: {args:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1820,6 +1866,7 @@ mod tests {
|
|||||||
"Test User",
|
"Test User",
|
||||||
"test@example.com",
|
"test@example.com",
|
||||||
Some(creds),
|
Some(creds),
|
||||||
|
"",
|
||||||
);
|
);
|
||||||
let pairs: Vec<_> = args.windows(2).collect();
|
let pairs: Vec<_> = args.windows(2).collect();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1839,6 +1886,7 @@ mod tests {
|
|||||||
"Test User",
|
"Test User",
|
||||||
"test@example.com",
|
"test@example.com",
|
||||||
None,
|
None,
|
||||||
|
"",
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!args.iter().any(|a| a.contains("claude-credentials-src")),
|
!args.iter().any(|a| a.contains("claude-credentials-src")),
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ pub async fn handle_project_rebuild(
|
|||||||
&git_user_name,
|
&git_user_name,
|
||||||
&git_user_email,
|
&git_user_email,
|
||||||
creds_opt,
|
creds_opt,
|
||||||
|
&super::new_project::resolve_gateway_url(),
|
||||||
);
|
);
|
||||||
|
|
||||||
docker_args.push("-v".into());
|
docker_args.push("-v".into());
|
||||||
|
|||||||
@@ -365,4 +365,115 @@ mod tests {
|
|||||||
received.event
|
received.event
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extends `relay_end_to_end_stage_transition_reaches_gateway_broadcast` to
|
||||||
|
/// cover the full wiring path: `project_docker_run_args` embeds
|
||||||
|
/// `HUSKIES_GATEWAY_URL` in the sled's argv; when that URL is used to start
|
||||||
|
/// the relay, a transition fired inside the sled reaches the gateway's CRDT
|
||||||
|
/// event_log within 1 second.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn project_docker_run_args_gateway_url_wires_relay() {
|
||||||
|
use crate::chat::transport::matrix::new_project::project_docker_run_args;
|
||||||
|
use crate::http::gateway::{gateway_event_push_handler, gateway_generate_token_handler};
|
||||||
|
use crate::service::gateway::{GatewayConfig, GatewayState, ProjectEntry};
|
||||||
|
use poem::EndpointExt as _;
|
||||||
|
use poem::listener::TcpAcceptor;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
crate::crdt_state::init_for_test();
|
||||||
|
|
||||||
|
// Spin up an in-process gateway server on an ephemeral port so we have
|
||||||
|
// a real URL to embed in the docker run args.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
let gateway_url = format!("http://127.0.0.1:{}", addr.port());
|
||||||
|
|
||||||
|
// project_docker_run_args embeds the gateway URL: this is the production
|
||||||
|
// code path that sets HUSKIES_GATEWAY_URL on the sled container.
|
||||||
|
let docker_args = project_docker_run_args(
|
||||||
|
"huskies-sled-relay",
|
||||||
|
3200,
|
||||||
|
2300,
|
||||||
|
"ssh-ed25519 AAAA...",
|
||||||
|
"Test User",
|
||||||
|
"test@example.com",
|
||||||
|
None,
|
||||||
|
&gateway_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract the injected URL exactly as the sled would read it from its env.
|
||||||
|
let injected_url = docker_args
|
||||||
|
.windows(2)
|
||||||
|
.find(|w| w[0] == "-e" && w[1].starts_with("HUSKIES_GATEWAY_URL="))
|
||||||
|
.map(|w| w[1].trim_start_matches("HUSKIES_GATEWAY_URL=").to_string())
|
||||||
|
.expect("project_docker_run_args must inject HUSKIES_GATEWAY_URL");
|
||||||
|
|
||||||
|
assert_eq!(injected_url, gateway_url, "injected URL must match input");
|
||||||
|
|
||||||
|
// Set up gateway state for the relay project.
|
||||||
|
let mut projects = BTreeMap::new();
|
||||||
|
projects.insert(
|
||||||
|
"sled-relay".to_string(),
|
||||||
|
ProjectEntry::with_url("http://sled-relay:3001"),
|
||||||
|
);
|
||||||
|
let config = GatewayConfig {
|
||||||
|
projects,
|
||||||
|
sled_tokens: BTreeMap::new(),
|
||||||
|
};
|
||||||
|
let state = Arc::new(GatewayState::new(config, PathBuf::new(), 9001).unwrap());
|
||||||
|
let mut gw_rx = state.event_tx.subscribe();
|
||||||
|
|
||||||
|
let route = poem::Route::new()
|
||||||
|
.at(
|
||||||
|
"/gateway/tokens",
|
||||||
|
poem::post(gateway_generate_token_handler),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/gateway/events/push",
|
||||||
|
poem::get(gateway_event_push_handler),
|
||||||
|
)
|
||||||
|
.data(state.clone());
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let acceptor = TcpAcceptor::from_tokio(listener).unwrap();
|
||||||
|
let _ = poem::Server::new_with_acceptor(acceptor).run(route).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn the relay using the URL extracted from the docker run args —
|
||||||
|
// this simulates what the sled does when it reads HUSKIES_GATEWAY_URL
|
||||||
|
// from its container environment.
|
||||||
|
let broadcaster = Arc::new(StatusBroadcaster::new());
|
||||||
|
spawn_relay_task(
|
||||||
|
injected_url,
|
||||||
|
"sled-relay".into(),
|
||||||
|
Arc::clone(&broadcaster),
|
||||||
|
reqwest::Client::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||||
|
|
||||||
|
broadcaster.publish(StatusEvent::StageTransition {
|
||||||
|
story_id: "99_docker_args_relay".into(),
|
||||||
|
story_name: "Docker Args Relay".into(),
|
||||||
|
from_stage: "1_backlog".into(),
|
||||||
|
to_stage: "2_current".into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let received = tokio::time::timeout(std::time::Duration::from_secs(1), gw_rx.recv())
|
||||||
|
.await
|
||||||
|
.expect("timed out: event did not reach gateway within 1 s")
|
||||||
|
.expect("gateway broadcast channel closed unexpectedly");
|
||||||
|
|
||||||
|
assert_eq!(received.project, "sled-relay");
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
received.event,
|
||||||
|
StoredEvent::StageTransition { ref story_id, .. } if story_id == "99_docker_args_relay"
|
||||||
|
),
|
||||||
|
"unexpected gateway event: {:?}",
|
||||||
|
received.event
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user