Public dry run dryrun-33643196cbdf
This commit is contained in:
parent
666360b98d
commit
17e52b5a2f
4 changed files with 172 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "f6bbcf3a2f6fd20b724a51a04861ea480d4c5607",
|
"source_commit": "33643196cbdfe5eeacd69bc39b8cdf86cb924d2d",
|
||||||
"release_name": "dryrun-f6bbcf3a2f6f",
|
"release_name": "dryrun-33643196cbdf",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::net::{TcpListener, TcpStream};
|
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
@ -19,6 +19,7 @@ const DEFAULT_OIDC_ISSUER_URL: &str = "https://auth.michelpaulissen.com";
|
||||||
const BROWSER_CALLBACK_ADDR: &str = "127.0.0.1:45173";
|
const BROWSER_CALLBACK_ADDR: &str = "127.0.0.1:45173";
|
||||||
const BROWSER_CALLBACK_PATH: &str = "/callback";
|
const BROWSER_CALLBACK_PATH: &str = "/callback";
|
||||||
const DEFAULT_BROWSER_LOGIN_CALLBACK_TIMEOUT_SECONDS: u64 = 300;
|
const DEFAULT_BROWSER_LOGIN_CALLBACK_TIMEOUT_SECONDS: u64 = 300;
|
||||||
|
const DOCTOR_COORDINATOR_TIMEOUT: Duration = Duration::from_millis(500);
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
#[command(name = "disasmer", version, arg_required_else_help = true)]
|
#[command(name = "disasmer", version, arg_required_else_help = true)]
|
||||||
|
|
@ -639,10 +640,17 @@ struct ProjectConfig {
|
||||||
|
|
||||||
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
let config = read_project_config(&cwd)?;
|
let config = read_project_config(&cwd)?;
|
||||||
|
let coordinator = args.scope.coordinator.or_else(|| {
|
||||||
|
config
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|config| config.coordinator.clone())
|
||||||
|
});
|
||||||
|
let coordinator_reachability = coordinator_reachability(coordinator.as_deref());
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"command": "doctor",
|
"command": "doctor",
|
||||||
"cwd": cwd,
|
"cwd": cwd,
|
||||||
"coordinator": args.scope.coordinator.or_else(|| config.as_ref().and_then(|config| config.coordinator.clone())),
|
"coordinator": coordinator,
|
||||||
|
"coordinator_reachability": coordinator_reachability,
|
||||||
"auth": auth_state_value(),
|
"auth": auth_state_value(),
|
||||||
"project": config,
|
"project": config,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -663,6 +671,73 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
|
||||||
|
let Some(coordinator) = coordinator else {
|
||||||
|
return json!({
|
||||||
|
"checked": false,
|
||||||
|
"status": "not_configured",
|
||||||
|
"next_action": "run disasmer login --browser or pass --coordinator"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
match ping_coordinator(coordinator, DOCTOR_COORDINATOR_TIMEOUT) {
|
||||||
|
Ok(response) => json!({
|
||||||
|
"checked": true,
|
||||||
|
"status": "reachable",
|
||||||
|
"coordinator": coordinator,
|
||||||
|
"response": response
|
||||||
|
}),
|
||||||
|
Err(err) => json!({
|
||||||
|
"checked": true,
|
||||||
|
"status": "unreachable",
|
||||||
|
"coordinator": coordinator,
|
||||||
|
"error": err.to_string(),
|
||||||
|
"next_action": "check the coordinator URL, network, and service status"
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
|
||||||
|
let transport_addr = json_line_transport_addr(coordinator);
|
||||||
|
let socket_addrs = transport_addr
|
||||||
|
.to_socket_addrs()
|
||||||
|
.with_context(|| format!("failed to resolve {coordinator} via {transport_addr}"))?;
|
||||||
|
let mut saw_addr = false;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
|
for socket_addr in socket_addrs {
|
||||||
|
saw_addr = true;
|
||||||
|
match TcpStream::connect_timeout(&socket_addr, timeout) {
|
||||||
|
Ok(stream) => {
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(timeout))
|
||||||
|
.with_context(|| format!("failed to set read timeout for {socket_addr}"))?;
|
||||||
|
stream
|
||||||
|
.set_write_timeout(Some(timeout))
|
||||||
|
.with_context(|| format!("failed to set write timeout for {socket_addr}"))?;
|
||||||
|
let mut session = JsonLineSession::from_stream(stream)?;
|
||||||
|
return session.request(json!({ "type": "ping" })).with_context(|| {
|
||||||
|
format!("coordinator ping failed for {coordinator} via {socket_addr}")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => last_error = Some(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !saw_addr {
|
||||||
|
anyhow::bail!(
|
||||||
|
"failed to resolve any socket address for {coordinator} via {transport_addr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"failed to connect to {coordinator} via {transport_addr}: {}",
|
||||||
|
last_error
|
||||||
|
.map(|err| err.to_string())
|
||||||
|
.unwrap_or_else(|| "no socket addresses attempted".to_owned())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
let config = read_project_config(&cwd)?;
|
let config = read_project_config(&cwd)?;
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
|
|
@ -1363,6 +1438,20 @@ fn human_report(value: &Value) -> String {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(reachability) = value.get("coordinator_reachability") {
|
||||||
|
if let Some(status) = reachability.get("status").and_then(Value::as_str) {
|
||||||
|
lines.push(format!("coordinator reachability: {status}"));
|
||||||
|
}
|
||||||
|
if let Some(error) = reachability.get("error").and_then(Value::as_str) {
|
||||||
|
lines.push(format!("coordinator error: {error}"));
|
||||||
|
}
|
||||||
|
if let Some(response_type) = reachability
|
||||||
|
.pointer("/response/type")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
{
|
||||||
|
lines.push(format!("coordinator ping: {response_type}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(response) = value
|
if let Some(response) = value
|
||||||
.get("response")
|
.get("response")
|
||||||
.or_else(|| value.get("coordinator_response"))
|
.or_else(|| value.get("coordinator_response"))
|
||||||
|
|
@ -2724,6 +2813,10 @@ impl JsonLineSession {
|
||||||
let transport_addr = json_line_transport_addr(addr);
|
let transport_addr = json_line_transport_addr(addr);
|
||||||
let writer = TcpStream::connect(&transport_addr)
|
let writer = TcpStream::connect(&transport_addr)
|
||||||
.with_context(|| format!("failed to connect to {addr} via {transport_addr}"))?;
|
.with_context(|| format!("failed to connect to {addr} via {transport_addr}"))?;
|
||||||
|
Self::from_stream(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_stream(writer: TcpStream) -> Result<Self> {
|
||||||
let reader = BufReader::new(writer.try_clone()?);
|
let reader = BufReader::new(writer.try_clone()?);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
writer,
|
writer,
|
||||||
|
|
@ -3198,6 +3291,73 @@ mod tests {
|
||||||
assert_eq!(json_line_transport_addr("127.0.0.1:7999"), "127.0.0.1:7999");
|
assert_eq!(json_line_transport_addr("127.0.0.1:7999"), "127.0.0.1:7999");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doctor_reports_unchecked_coordinator_reachability_without_config() {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let report = doctor_report(
|
||||||
|
DoctorArgs {
|
||||||
|
scope: CliScopeArgs {
|
||||||
|
coordinator: None,
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
user: "user".to_owned(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temp.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report["command"], "doctor");
|
||||||
|
assert!(report["coordinator"].is_null());
|
||||||
|
assert_eq!(report["coordinator_reachability"]["checked"], false);
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_reachability"]["status"],
|
||||||
|
"not_configured"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doctor_pings_configured_coordinator() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap().to_string();
|
||||||
|
let server = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||||
|
let mut line = String::new();
|
||||||
|
reader.read_line(&mut line).unwrap();
|
||||||
|
assert!(line.contains("\"type\":\"ping\""));
|
||||||
|
stream
|
||||||
|
.write_all(b"{\"type\":\"pong\",\"epoch\":42}\n")
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let report = doctor_report(
|
||||||
|
DoctorArgs {
|
||||||
|
scope: CliScopeArgs {
|
||||||
|
coordinator: Some(addr.clone()),
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
user: "user".to_owned(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temp.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
server.join().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report["coordinator"], addr);
|
||||||
|
assert_eq!(report["coordinator_reachability"]["checked"], true);
|
||||||
|
assert_eq!(report["coordinator_reachability"]["status"], "reachable");
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_reachability"]["response"]["type"],
|
||||||
|
"pong"
|
||||||
|
);
|
||||||
|
assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_first_mvp_command_surface_parses() {
|
fn cli_first_mvp_command_surface_parses() {
|
||||||
for args in [
|
for args in [
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ for (const [name, pattern] of [
|
||||||
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
["CLI version coverage", /fn top_level_version_is_available\(\)/],
|
||||||
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
["CLI JSON parse coverage", /fn cli_first_json_mode_parses_for_primary_commands\(\)/],
|
||||||
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
["CLI human output coverage", /fn human_report_is_text_not_json\(\)/],
|
||||||
|
["doctor unchecked reachability coverage", /fn doctor_reports_unchecked_coordinator_reachability_without_config\(\)/],
|
||||||
|
["doctor ping reachability coverage", /fn doctor_pings_configured_coordinator\(\)/],
|
||||||
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
||||||
["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/],
|
["build no full repo upload coverage", /fn build_command_reuses_bundle_inspection_without_full_repo_upload\(\)/],
|
||||||
["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/],
|
["safe coordinator-required plans", /fn node_enroll_and_process_commands_have_safe_plan_without_coordinator\(\)/],
|
||||||
|
|
@ -127,6 +129,7 @@ for (const [name, pattern] of [
|
||||||
["human default assertion", /default output should be human-readable text, not JSON/],
|
["human default assertion", /default output should be human-readable text, not JSON/],
|
||||||
["login JSON mode", /\["login", "--coordinator", "https:\/\/coord\.example\.test", "--json"\]/],
|
["login JSON mode", /\["login", "--coordinator", "https:\/\/coord\.example\.test", "--json"\]/],
|
||||||
["doctor human mode", /\["doctor"\]/],
|
["doctor human mode", /\["doctor"\]/],
|
||||||
|
["doctor reachability JSON mode", /doctorJson\.coordinator_reachability\.status/],
|
||||||
["bundle inspect JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/],
|
["bundle inspect JSON mode", /\["bundle", "inspect", "--project", project, "--json"\]/],
|
||||||
["auth expiry posture", /token_expiry_posture[\s\S]*expires_at/],
|
["auth expiry posture", /token_expiry_posture[\s\S]*expires_at/],
|
||||||
]) {
|
]) {
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,16 @@ assert(loginJson.human_flow.Device);
|
||||||
const doctorHuman = disasmer(["doctor"]);
|
const doctorHuman = disasmer(["doctor"]);
|
||||||
assertHuman("doctor", doctorHuman, [
|
assertHuman("doctor", doctorHuman, [
|
||||||
/Disasmer doctor/,
|
/Disasmer doctor/,
|
||||||
|
/coordinator reachability: not_configured/,
|
||||||
/dependencies:/,
|
/dependencies:/,
|
||||||
/auth:/,
|
/auth:/,
|
||||||
/node capabilities:/,
|
/node capabilities:/,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const doctorJson = json(["doctor", "--json"]);
|
||||||
|
assert.strictEqual(doctorJson.coordinator_reachability.checked, false);
|
||||||
|
assert.strictEqual(doctorJson.coordinator_reachability.status, "not_configured");
|
||||||
|
|
||||||
const authJson = json(["auth", "status", "--json"], {
|
const authJson = json(["auth", "status", "--json"], {
|
||||||
DISASMER_TOKEN: "token",
|
DISASMER_TOKEN: "token",
|
||||||
DISASMER_TOKEN_EXPIRES_AT: "2026-07-04T00:00:00Z",
|
DISASMER_TOKEN_EXPIRES_AT: "2026-07-04T00:00:00Z",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue