191 lines
6 KiB
Rust
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(),
|
|
}))
|
|
}
|