feat: surface backend runtime workers in tui
This commit is contained in:
@@ -41,6 +41,122 @@ impl BackendRuntimeTarget {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BackendRuntimeListTarget {
|
||||
pub base_url: String,
|
||||
pub workspace_id: Option<String>,
|
||||
pub runtime_id: Option<String>,
|
||||
}
|
||||
|
||||
impl BackendRuntimeListTarget {
|
||||
pub fn new(
|
||||
base_url: impl Into<String>,
|
||||
workspace_id: Option<String>,
|
||||
runtime_id: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
workspace_id,
|
||||
runtime_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct BackendRuntimeListResponse<T> {
|
||||
pub workspace_id: String,
|
||||
pub limit: usize,
|
||||
pub items: Vec<T>,
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub diagnostics: Vec<BackendDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendRuntimeSummary {
|
||||
pub runtime_id: String,
|
||||
pub label: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub host_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub diagnostics: Vec<BackendDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkerWorkspaceSummary {
|
||||
pub visibility: String,
|
||||
pub identity: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkerImplementationSummary {
|
||||
pub kind: String,
|
||||
pub display_hint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkerCapabilitySummary {
|
||||
pub can_stop: bool,
|
||||
pub can_spawn_followup: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkingDirectoryCleanupTarget {
|
||||
pub kind: String,
|
||||
pub working_directory_id: String,
|
||||
pub repository_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkingDirectorySummary {
|
||||
pub working_directory_id: String,
|
||||
pub repository_id: String,
|
||||
#[serde(default)]
|
||||
pub requested_selector: Option<String>,
|
||||
pub materializer_kind: String,
|
||||
#[serde(default)]
|
||||
pub resolved_commit: Option<String>,
|
||||
#[serde(default)]
|
||||
pub resolved_tree: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cleanup_target: Option<BackendWorkingDirectoryCleanupTarget>,
|
||||
pub status: String,
|
||||
#[serde(default)]
|
||||
pub cleanliness: Option<String>,
|
||||
#[serde(default)]
|
||||
pub primary_worker_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub management_kind: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkerSummary {
|
||||
pub runtime_id: String,
|
||||
pub worker_id: String,
|
||||
pub host_id: String,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub role: Option<String>,
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
pub workspace: BackendWorkerWorkspaceSummary,
|
||||
pub state: String,
|
||||
#[serde(default)]
|
||||
pub last_seen_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub pinned: bool,
|
||||
#[serde(default)]
|
||||
pub retention_state: String,
|
||||
pub implementation: BackendWorkerImplementationSummary,
|
||||
pub capabilities: BackendWorkerCapabilitySummary,
|
||||
#[serde(default)]
|
||||
pub working_directory: Option<BackendWorkingDirectorySummary>,
|
||||
#[serde(default)]
|
||||
pub diagnostics: Vec<BackendDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackendRuntimeClient {
|
||||
target: BackendRuntimeTarget,
|
||||
@@ -73,6 +189,72 @@ impl From<reqwest::Error> for BackendRuntimeClientError {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_backend_workers(
|
||||
target: &BackendRuntimeListTarget,
|
||||
) -> Result<BackendRuntimeListResponse<BackendWorkerSummary>, BackendRuntimeClientError> {
|
||||
validate_list_target(target)?;
|
||||
let http = reqwest::Client::new();
|
||||
if let Some(runtime_id) = target.runtime_id.as_deref() {
|
||||
let path = backend_runtime_workers_path(target.workspace_id.as_deref(), runtime_id);
|
||||
let url = join_base_and_path(&target.base_url, &path);
|
||||
return Ok(http
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
||||
.await?);
|
||||
}
|
||||
|
||||
let runtime_path = backend_runtimes_path(target.workspace_id.as_deref());
|
||||
let runtime_url = join_base_and_path(&target.base_url, &runtime_path);
|
||||
let runtimes = http
|
||||
.get(runtime_url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json::<BackendRuntimeListResponse<BackendRuntimeSummary>>()
|
||||
.await?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut diagnostics = runtimes.diagnostics;
|
||||
for runtime in runtimes.items {
|
||||
let path =
|
||||
backend_runtime_workers_path(target.workspace_id.as_deref(), &runtime.runtime_id);
|
||||
let url = join_base_and_path(&target.base_url, &path);
|
||||
match http
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|response| response.error_for_status())
|
||||
{
|
||||
Ok(response) => {
|
||||
let response = response
|
||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
||||
.await?;
|
||||
diagnostics.extend(response.diagnostics);
|
||||
items.extend(response.items);
|
||||
}
|
||||
Err(error) => diagnostics.push(BackendDiagnostic {
|
||||
code: "runtime_worker_list_failed".to_string(),
|
||||
severity: Some("error".to_string()),
|
||||
message: format!(
|
||||
"failed to list workers for runtime {}: {error}",
|
||||
runtime.runtime_id
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BackendRuntimeListResponse {
|
||||
workspace_id: runtimes.workspace_id,
|
||||
limit: runtimes.limit,
|
||||
items,
|
||||
source: "backend_runtime_worker_summary".to_string(),
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
|
||||
impl BackendRuntimeClient {
|
||||
pub async fn connect(target: BackendRuntimeTarget) -> Result<Self, BackendRuntimeClientError> {
|
||||
validate_target(&target)?;
|
||||
@@ -375,6 +557,50 @@ fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeCl
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_list_target(
|
||||
target: &BackendRuntimeListTarget,
|
||||
) -> Result<(), BackendRuntimeClientError> {
|
||||
if target.base_url.trim().is_empty() {
|
||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||
"Backend API base URL is required".to_string(),
|
||||
));
|
||||
}
|
||||
if !(target.base_url.starts_with("http://")) && !(target.base_url.starts_with("https://")) {
|
||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||
"Backend API base URL must start with http:// or https://".to_string(),
|
||||
));
|
||||
}
|
||||
if target.workspace_id.as_deref().is_some_and(str::is_empty) {
|
||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||
"workspace_id must not be empty when provided".to_string(),
|
||||
));
|
||||
}
|
||||
if target.runtime_id.as_deref().is_some_and(str::is_empty) {
|
||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||
"runtime_id must not be empty when provided".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backend_runtimes_path(workspace_id: Option<&str>) -> String {
|
||||
match workspace_id {
|
||||
Some(workspace_id) => format!("/api/w/{}/runtimes", path_segment_encode(workspace_id)),
|
||||
None => "/api/runtimes".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn backend_runtime_workers_path(workspace_id: Option<&str>, runtime_id: &str) -> String {
|
||||
match workspace_id {
|
||||
Some(workspace_id) => format!(
|
||||
"/api/w/{}/runtimes/{}/workers",
|
||||
path_segment_encode(workspace_id),
|
||||
path_segment_encode(runtime_id)
|
||||
),
|
||||
None => format!("/api/runtimes/{}/workers", path_segment_encode(runtime_id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn observation_ws_url(target: &BackendRuntimeTarget) -> String {
|
||||
let path = format!(
|
||||
"/api/runtimes/{}/workers/{}/events/ws",
|
||||
@@ -449,10 +675,12 @@ struct WorkerLifecycleResult {
|
||||
diagnostics: Vec<BackendDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BackendDiagnostic {
|
||||
code: String,
|
||||
message: String,
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendDiagnostic {
|
||||
pub code: String,
|
||||
#[serde(default)]
|
||||
pub severity: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -502,6 +730,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_worker_list_paths_use_scoped_workspace_when_available() {
|
||||
assert_eq!(
|
||||
backend_runtimes_path(Some("workspace/one")),
|
||||
"/api/w/workspace%2Fone/runtimes"
|
||||
);
|
||||
assert_eq!(
|
||||
backend_runtime_workers_path(Some("workspace/one"), "runtime one"),
|
||||
"/api/w/workspace%2Fone/runtimes/runtime%20one/workers"
|
||||
);
|
||||
assert_eq!(
|
||||
backend_runtime_workers_path(None, "runtime one"),
|
||||
"/api/runtimes/runtime%20one/workers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observation_url_uses_backend_runtime_worker_identity() {
|
||||
let target =
|
||||
|
||||
Reference in New Issue
Block a user