Public dry run dryrun-199c53541aa2

This commit is contained in:
Michel Paulissen 2026-07-03 20:31:07 +02:00
parent c8526c39ef
commit 84ef57d84b
5 changed files with 304 additions and 12 deletions

View file

@ -1121,6 +1121,7 @@ fn process_restart_report(args: ProcessRestartArgs) -> Result<Value> {
"tenant": args.scope.tenant,
"project": args.scope.project,
"process": args.process,
"restart": true,
}))?;
let restart_request = process_restart_request_summary(&response, !args.yes);
return Ok(json!({
@ -1824,13 +1825,8 @@ fn main() -> Result<()> {
}
Commands::Run(args) => {
let json_output = args.json;
let plan = run_plan(args, std::env::current_dir()?, session_from_env())?;
if should_execute_local_node(&plan) {
let report = execute_local_node_run(plan)?;
emit_report(&report, json_output)?;
} else {
emit_report(&plan, json_output)?;
}
let report = run_report(args, std::env::current_dir()?, session_from_env())?;
emit_report(&report, json_output)?;
}
Commands::Node {
command: NodeCommands::Attach(args),
@ -3104,6 +3100,117 @@ fn run_plan(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<RunPlan>
})
}
fn run_report(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<Value> {
let plan = run_plan(args, cwd, session)?;
if should_execute_local_node(&plan) {
return Ok(serde_json::to_value(execute_local_node_run(plan)?)?);
}
coordinator_run_report(plan)
}
fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
let config = read_project_config(&plan.project)?;
let tenant = config
.as_ref()
.map(|config| config.tenant.clone())
.unwrap_or_else(|| "tenant".to_owned());
let project = config
.as_ref()
.map(|config| config.project.clone())
.unwrap_or_else(|| "project".to_owned());
let user = config
.as_ref()
.map(|config| config.user.clone())
.unwrap_or_else(|| "user".to_owned());
let coordinator = run_coordinator_endpoint(&plan)?;
let process = "vp-current".to_owned();
let mut session = JsonLineSession::connect(&coordinator)?;
let response = session.request_allow_error(json!({
"type": "start_process",
"tenant": tenant.clone(),
"project": project.clone(),
"process": process.clone(),
"restart": false,
}))?;
let run_start = run_start_summary(&response);
let status = run_start
.get("status")
.and_then(Value::as_str)
.unwrap_or("coordinator_response");
Ok(json!({
"command": "run",
"status": status,
"project_root": plan.project,
"entry": plan.entry,
"tenant": tenant,
"project": project,
"user": user,
"coordinator": coordinator,
"process": process,
"run_start": run_start,
"coordinator_response": response,
"coordinator_session_requests": session.requests(),
"private_website_required": false,
}))
}
fn run_coordinator_endpoint(plan: &RunPlan) -> Result<String> {
match &plan.coordinator {
CoordinatorSelection::Hosted => Ok(plan
.operator_endpoint
.clone()
.unwrap_or_else(default_operator_endpoint)),
CoordinatorSelection::LocalOverride(coordinator) => Ok(coordinator.clone()),
CoordinatorSelection::LocalOnly => {
anyhow::bail!("local-only run should execute through local services")
}
}
}
fn run_start_summary(response: &Value) -> Value {
if response.get("type").and_then(Value::as_str) == Some("process_started") {
return json!({
"status": "started",
"accepted": true,
"process": response.get("process").cloned().unwrap_or(Value::Null),
"coordinator_epoch": response.get("epoch").cloned().unwrap_or(Value::Null),
"restart": false,
"single_active_process_boundary": true,
"next_actions": [
"disasmer process status",
"disasmer logs",
"disasmer process cancel"
],
});
}
let message = response
.get("message")
.and_then(Value::as_str)
.unwrap_or("coordinator rejected run");
let active_conflict = message.contains("already has active virtual process");
json!({
"status": if active_conflict { "blocked_active_process" } else { "coordinator_rejected" },
"accepted": false,
"category": if active_conflict { "active_process_already_running" } else { "coordinator" },
"message": message,
"restart": false,
"single_active_process_boundary": true,
"safe_failure": true,
"next_actions": if active_conflict {
json!([
"disasmer process status",
"disasmer logs",
"disasmer process restart --yes",
"disasmer process cancel --yes",
"wait for the active process to finish"
])
} else {
json!(["disasmer doctor", "check coordinator status"])
},
})
}
fn should_execute_local_node(plan: &RunPlan) -> bool {
match &plan.coordinator {
CoordinatorSelection::LocalOnly => true,
@ -3441,6 +3548,14 @@ impl JsonLineSession {
}
fn request(&mut self, value: Value) -> Result<Value> {
let response = self.request_allow_error(value)?;
if response.get("type").and_then(Value::as_str) == Some("error") {
anyhow::bail!("coordinator error: {response}");
}
Ok(response)
}
fn request_allow_error(&mut self, value: Value) -> Result<Value> {
serde_json::to_writer(&mut self.writer, &value)?;
self.writer.write_all(b"\n")?;
self.writer.flush()?;
@ -3451,9 +3566,6 @@ impl JsonLineSession {
}
self.requests += 1;
let response: Value = serde_json::from_str(&line)?;
if response.get("type").and_then(Value::as_str) == Some("error") {
anyhow::bail!("coordinator error: {response}");
}
Ok(response)
}
@ -3621,6 +3733,89 @@ mod tests {
assert_eq!(plan.operator_endpoint, None);
}
#[test]
fn run_contacts_configured_coordinator_and_reports_active_process_conflicts() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user-live".to_owned(),
coordinator: None,
},
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for response in [
r#"{"type":"process_started","process":"vp-current","epoch":7}"#,
r#"{"type":"error","message":"coordinator request failed: unauthorized coordinator action: project already has active virtual process vp-current; inspect it, restart it, cancel it, or wait before starting another run"}"#,
] {
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(r#""type":"start_process""#));
assert!(line.contains(r#""tenant":"tenant-live""#));
assert!(line.contains(r#""project":"project-live""#));
assert!(line.contains(r#""process":"vp-current""#));
assert!(line.contains(r#""restart":false"#));
stream.write_all(response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
}
});
let started = run_report(
RunArgs {
entry: Some("build".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
json: false,
},
PathBuf::from("/unused"),
CliSession::Anonymous,
)
.unwrap();
let blocked = run_report(
RunArgs {
entry: Some("test".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
json: false,
},
PathBuf::from("/unused"),
CliSession::Anonymous,
)
.unwrap();
server.join().unwrap();
assert_eq!(started["status"], "started");
assert_eq!(started["entry"], "build");
assert_eq!(started["tenant"], "tenant-live");
assert_eq!(started["project"], "project-live");
assert_eq!(started["process"], "vp-current");
assert_eq!(started["run_start"]["restart"], false);
assert_eq!(started["run_start"]["single_active_process_boundary"], true);
assert_eq!(blocked["status"], "blocked_active_process");
assert_eq!(blocked["entry"], "test");
assert_eq!(
blocked["run_start"]["category"],
"active_process_already_running"
);
assert_eq!(blocked["run_start"]["safe_failure"], true);
assert!(blocked["run_start"]["next_actions"]
.as_array()
.unwrap()
.iter()
.any(|action| action == "disasmer process restart --yes"));
}
#[test]
fn local_only_run_executes_ephemeral_local_services() {
let Cli {