diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index 8155afd9..e3106d17 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -1903,6 +1903,14 @@ impl RpcHandler { self.set_install_phase("netbird", InstallPhase::WaitingHealthy) .await; + // Containers being "running" is NOT the same as the embedded OIDC + // provider being ready (#10). The dashboard SPA opens right after install + // and, if it loads before /oauth2/.well-known is served, caches a bad + // auth state — the user appears logged-in but can't log out until it + // self-corrects. Wait (best-effort) for OIDC discovery to answer before + // we report Done, so the first dashboard load sees a ready provider. + wait_for_netbird_oidc_ready(Duration::from_secs(60)).await; + self.set_install_phase("netbird", InstallPhase::PostInstall) .await; self.set_install_phase("netbird", InstallPhase::Done).await; @@ -1918,6 +1926,37 @@ impl RpcHandler { } } +/// Best-effort wait for NetBird's embedded OIDC provider to start serving its +/// discovery document. The management server publishes 8086:80 on the host and +/// is the issuer at `/oauth2`, so its `.well-known/openid-configuration` is the +/// signal that the dashboard's login/logout flow will work. Polls until a 2xx +/// or the timeout — NEVER fails the install (the stack is already running; this +/// only narrows the post-install race window in #10). +async fn wait_for_netbird_oidc_ready(timeout: Duration) { + let url = "http://127.0.0.1:8086/oauth2/.well-known/openid-configuration"; + let client = match reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + { + Ok(c) => c, + Err(_) => return, + }; + let deadline = tokio::time::Instant::now() + timeout; + loop { + if let Ok(resp) = client.get(url).send().await { + if resp.status().is_success() { + info!("NetBird OIDC discovery is ready"); + return; + } + } + if tokio::time::Instant::now() >= deadline { + info!("NetBird OIDC discovery not ready within timeout — proceeding anyway"); + return; + } + tokio::time::sleep(Duration::from_secs(2)).await; + } +} + async fn read_or_generate_b64_secret(name: &str) -> String { let path = format!("/var/lib/archipelago/secrets/{}", name); if let Ok(val) = tokio::fs::read_to_string(&path).await {