use anyhow::{Context, Result}; use clusterflux_control::{endpoint_identity, endpoint_is_loopback, ControlSession}; use clusterflux_core::coordinator_wire_request; use serde_json::{json, Value}; use crate::config::StoredCliSession; use crate::errors::cli_error_summary; use crate::CliScopeArgs; pub(crate) struct JsonLineSession { inner: ControlSession, } impl JsonLineSession { pub(crate) fn connect(addr: &str) -> Result { let inner = ControlSession::connect(addr) .with_context(|| format!("failed to connect to coordinator {addr}"))?; Ok(Self { inner }) } pub(crate) fn request(&mut self, value: Value) -> Result { let response = self.request_allow_error(value)?; if response.get("type").and_then(Value::as_str) == Some("error") { let message = response .get("message") .and_then(Value::as_str) .map(str::to_owned) .unwrap_or_else(|| response.to_string()); let machine_error = cli_error_summary(&message); let category = machine_error .get("category") .and_then(Value::as_str) .unwrap_or("unknown"); let exit_code = machine_error .get("stable_exit_code") .and_then(Value::as_i64) .unwrap_or(1); anyhow::bail!("coordinator error ({category}, exit {exit_code}): {response}"); } Ok(response) } pub(crate) fn request_allow_error(&mut self, value: Value) -> Result { let request_id = format!("cli-{}", self.inner.requests() + 1); let wire_request = coordinator_wire_request(request_id, value); self.inner .request(&wire_request) .map_err(anyhow::Error::from) } pub(crate) fn requests(&self) -> u64 { self.inner.requests() } } pub(crate) fn control_endpoint_identity(endpoint: &str) -> Result { endpoint_identity(endpoint).map_err(anyhow::Error::from) } pub(crate) fn authenticated_or_local_trusted_request( coordinator: &str, stored_session: Option<&StoredCliSession>, authenticated_request: Value, local_trusted_request: Value, ) -> Result { if let Some(session_secret) = stored_session_for_coordinator(coordinator, stored_session) .and_then(|session| session.session_secret.as_ref()) { Ok(json!({ "type": "authenticated", "session_secret": session_secret, "request": authenticated_request, })) } else if is_loopback_coordinator(coordinator) { Ok(local_trusted_request) } else { anyhow::bail!( "no authenticated CLI session matches coordinator {coordinator}; run `clusterflux login --browser` from the current project" ) } } pub(crate) fn is_loopback_coordinator(coordinator: &str) -> bool { endpoint_is_loopback(coordinator) } pub(crate) fn stored_session_for_coordinator<'a>( coordinator: &str, stored_session: Option<&'a StoredCliSession>, ) -> Option<&'a StoredCliSession> { stored_session.filter(|session| { session.session_secret.is_some() && control_endpoint_identity(&session.coordinator).ok() == control_endpoint_identity(coordinator).ok() }) } pub(crate) fn list_task_events_if_available_with_session( coordinator: Option<&str>, scope: &CliScopeArgs, process: Option, stored_session: Option<&StoredCliSession>, ) -> Result> { let Some(coordinator) = coordinator else { return Ok(None); }; let mut session = JsonLineSession::connect(coordinator)?; let response = session.request(authenticated_or_local_trusted_request( coordinator, stored_session, json!({ "type": "list_task_events", "process": process, }), json!({ "type": "list_task_events", "tenant": scope.tenant, "project": scope.project, "actor_user": scope.user, "process": process, }), )?)?; Ok(Some(json!({ "coordinator": coordinator, "response": response, "coordinator_session_requests": session.requests(), }))) } pub(crate) fn list_attached_nodes_if_available_with_session( coordinator: Option<&str>, scope: &CliScopeArgs, stored_session: Option<&StoredCliSession>, ) -> Result { let Some(coordinator) = coordinator else { return Ok(json!({ "checked": false, "source": "no_coordinator", "count": 0, "online": 0, "response": null, })); }; let mut session = JsonLineSession::connect(coordinator)?; let response = session.request(authenticated_or_local_trusted_request( coordinator, stored_session, json!({ "type": "list_node_descriptors", }), json!({ "type": "list_node_descriptors", "tenant": scope.tenant, "project": scope.project, "actor_user": scope.user, }), )?)?; let descriptors = response .get("descriptors") .and_then(Value::as_array) .cloned() .unwrap_or_default(); let online = descriptors .iter() .filter(|descriptor| { descriptor .get("online") .and_then(Value::as_bool) .unwrap_or(false) }) .count(); Ok(json!({ "checked": true, "source": "coordinator", "coordinator": coordinator, "count": descriptors.len(), "online": online, "response": response, "coordinator_session_requests": session.requests(), })) }