clusterflux-public/crates/clusterflux-cli/src/client.rs
Clusterflux release a6ca9e26dd Public release release-be1f654ae8de
Source commit: be1f654ae8de7d5eeb2005b4e1a05bf1ebf3124f

Public tree identity: sha256:ab53679496be4be7d1e02bc00723072774d1a3f87e10cc56558a752f28f6abb1
2026-07-17 05:17:01 +02:00

191 lines
6 KiB
Rust

use anyhow::{Context, Result};
use clusterflux_control::{
endpoint_identity, endpoint_is_loopback, ControlSession, LOGIN_API_PATH,
};
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<Self> {
let inner = ControlSession::connect(addr)
.with_context(|| format!("failed to connect to coordinator {addr}"))?;
Ok(Self { inner })
}
pub(crate) fn connect_browser_login(addr: &str) -> Result<Self> {
let inner = ControlSession::connect_to_api_path(addr, LOGIN_API_PATH)
.with_context(|| format!("failed to connect to hosted login endpoint {addr}"))?;
Ok(Self { inner })
}
pub(crate) 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") {
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<Value> {
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<String> {
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<Value> {
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<String>,
stored_session: Option<&StoredCliSession>,
) -> Result<Option<Value>> {
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<Value> {
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(),
}))
}