fix(mission_runtime): mint pairing code via admin endpoint, not log scrape
Fresh gateways sometimes boot claim-ing already paired (no pairing_code in the log banner), which broke the log-scrape approach. Instead, docker exec into the container and hit the localhost /admin/paircode/new endpoint that always mints a fresh one-time code and returns JSON we can parse.
This commit is contained in:
@@ -24,12 +24,12 @@
|
|||||||
//! carry the current binding (both null when torn down or never
|
//! carry the current binding (both null when torn down or never
|
||||||
//! provisioned).
|
//! provisioned).
|
||||||
|
|
||||||
|
use bollard::exec::{CreateExecOptions, StartExecResults};
|
||||||
use bollard::models::{
|
use bollard::models::{
|
||||||
ContainerCreateBody, EndpointSettings, HostConfig, Mount, MountTypeEnum, NetworkConnectRequest,
|
ContainerCreateBody, EndpointSettings, HostConfig, Mount, MountTypeEnum, NetworkConnectRequest,
|
||||||
};
|
};
|
||||||
use bollard::query_parameters::{
|
use bollard::query_parameters::{
|
||||||
CreateContainerOptions, InspectContainerOptions, LogsOptions, RemoveContainerOptions,
|
CreateContainerOptions, InspectContainerOptions, RemoveContainerOptions, StartContainerOptions,
|
||||||
StartContainerOptions,
|
|
||||||
};
|
};
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@@ -248,15 +248,15 @@ impl MissionRuntimeProvisioner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll container logs for up to ~10s waiting for the ZeroClaw
|
/// Poll the daemon's localhost admin endpoint until it responds
|
||||||
/// pairing banner. Returns None on timeout — callers keep any
|
/// with a fresh pairing code. Boot takes ~1-2s; deadline is 15s.
|
||||||
/// previously-persisted code, or the topology run fails and the
|
/// Returns None on timeout so callers see a clear paired=false
|
||||||
/// operator sees a clear error surface.
|
/// signal in the mission binding log.
|
||||||
async fn wait_for_pairing_code(&self, name: &str) -> Option<String> {
|
async fn wait_for_pairing_code(&self, name: &str) -> Option<String> {
|
||||||
let deadline = std::time::Duration::from_secs(10);
|
let deadline = std::time::Duration::from_secs(15);
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
while start.elapsed() < deadline {
|
while start.elapsed() < deadline {
|
||||||
if let Some(code) = self.scrape_pairing_code(name).await {
|
if let Some(code) = self.mint_pairing_code(name).await {
|
||||||
return Some(code);
|
return Some(code);
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
@@ -264,47 +264,61 @@ impl MissionRuntimeProvisioner {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-shot scrape of `docker logs <name>` for `X-Pairing-Code: NNNNNN`.
|
/// `docker exec` into the container and hit the local admin
|
||||||
async fn scrape_pairing_code(&self, name: &str) -> Option<String> {
|
/// endpoint that mints a fresh pairing code. This works whether
|
||||||
let mut stream = self.docker.logs(
|
/// the daemon booted "already paired" (no code in the log) or
|
||||||
|
/// "pairing required" (code in the log) — both surfaces mint on
|
||||||
|
/// demand.
|
||||||
|
async fn mint_pairing_code(&self, name: &str) -> Option<String> {
|
||||||
|
let exec = self
|
||||||
|
.docker
|
||||||
|
.create_exec(
|
||||||
name,
|
name,
|
||||||
Some(LogsOptions {
|
CreateExecOptions {
|
||||||
stdout: true,
|
cmd: Some(
|
||||||
stderr: true,
|
[
|
||||||
tail: "200".to_string(),
|
"curl",
|
||||||
|
"-fs",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
"http://127.0.0.1:42617/admin/paircode/new",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
attach_stdout: Some(true),
|
||||||
|
attach_stderr: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
},
|
||||||
);
|
)
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
let started = self.docker.start_exec(&exec.id, None).await.ok()?;
|
||||||
|
let StartExecResults::Attached { mut output, .. } = started else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = output.next().await {
|
||||||
if let Ok(c) = chunk {
|
if let Ok(c) = chunk {
|
||||||
buf.push_str(&String::from_utf8_lossy(&c.into_bytes()));
|
buf.push_str(&c.to_string());
|
||||||
if buf.len() > 64_000 {
|
if buf.len() > 8_000 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
extract_pairing_code(&buf)
|
extract_pairing_code_from_json(&buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a ZeroClaw daemon boot banner and pull out the pairing code
|
/// Parse the JSON `{ "pairing_code": "NNNNNN", ... }` body from the
|
||||||
/// from a line like `Send: POST /pair with header X-Pairing-Code: 273200`.
|
/// admin/paircode/new endpoint.
|
||||||
fn extract_pairing_code(logs: &str) -> Option<String> {
|
fn extract_pairing_code_from_json(body: &str) -> Option<String> {
|
||||||
for line in logs.lines() {
|
let v: serde_json::Value = serde_json::from_str(body.trim()).ok()?;
|
||||||
if let Some(idx) = line.find("X-Pairing-Code:") {
|
v.get("pairing_code")
|
||||||
let rest = &line[idx + "X-Pairing-Code:".len()..];
|
.and_then(|x| x.as_str())
|
||||||
let code: String = rest
|
.filter(|s| !s.is_empty())
|
||||||
.chars()
|
.map(String::from)
|
||||||
.skip_while(|c| c.is_whitespace())
|
|
||||||
.take_while(|c| c.is_ascii_digit())
|
|
||||||
.collect();
|
|
||||||
if !code.is_empty() {
|
|
||||||
return Some(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MissionRuntimeProvisioner {
|
impl MissionRuntimeProvisioner {
|
||||||
|
|||||||
Reference in New Issue
Block a user