feat: add worker runtime registry overview

This commit is contained in:
2026-06-24 19:56:08 +09:00
parent 371fd7c6e5
commit 9bd1550715
6 changed files with 921 additions and 554 deletions
File diff suppressed because it is too large Load Diff
+9
View File
@@ -40,6 +40,15 @@ pub enum Error {
MissingFrontmatter(String),
#[error("unknown local host `{0}`")]
UnknownHost(String),
#[error("unknown local worker `{0}`")]
UnknownWorker(String),
#[error("invalid runtime {kind} `{value}`")]
InvalidRuntimeIdentifier { kind: String, value: String },
#[error("runtime `{runtime_id}` does not support `{capability}`")]
RuntimeCapabilityUnsupported {
runtime_id: String,
capability: String,
},
#[error("unknown local repository `{0}`")]
UnknownRepository(String),
#[error("workspace identity error: {0}")]
+6 -2
View File
@@ -3,7 +3,7 @@ use std::process::{Command, Output};
use serde::{Deserialize, Serialize};
use crate::hosts::RuntimeDiagnostic;
use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic};
const LEGACY_LOCAL_REPOSITORY_ID: &str = "local";
const LOCAL_REPOSITORY_PREFIX: &str = "local-";
@@ -340,7 +340,11 @@ fn truncate_field(value: &str, limit: usize) -> String {
fn diagnostic(code: &str, severity: &str, message: String) -> RuntimeDiagnostic {
RuntimeDiagnostic {
code: code.to_string(),
severity: severity.to_string(),
severity: match severity {
"error" => DiagnosticSeverity::Error,
"warning" => DiagnosticSeverity::Warning,
_ => DiagnosticSeverity::Info,
},
message,
}
}
+53 -20
View File
@@ -11,7 +11,8 @@ use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use crate::hosts::{
HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary, WorkspaceWorkerRuntime,
DiagnosticSeverity, HostSummary, LocalPodRuntime, RuntimeDiagnostic, RuntimeSummary,
WorkerRuntimeRegistry, WorkerSummary,
};
use crate::identity::WorkspaceIdentity;
use crate::records::{
@@ -63,7 +64,7 @@ pub struct WorkspaceApi {
config: ServerConfig,
store: Arc<dyn ControlPlaneStore>,
records: LocalProjectRecordReader,
runtime: Arc<dyn WorkspaceWorkerRuntime>,
runtime: Arc<WorkerRuntimeRegistry>,
}
impl WorkspaceApi {
@@ -77,11 +78,11 @@ impl WorkspaceApi {
updated_at: config.workspace_created_at.clone(),
})
.await?;
let runtime = Arc::new(LocalRuntimeBridge::new(
let runtime = Arc::new(WorkerRuntimeRegistry::for_local_pods(LocalPodRuntime::new(
config.workspace_id.clone(),
config.workspace_root.clone(),
config.local_runtime_data_dir.clone(),
));
)));
Ok(Self {
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
config,
@@ -125,6 +126,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
get(repository_tickets),
)
.route("/api/hosts", get(list_hosts))
.route("/api/runtimes", get(list_runtimes))
.route("/api/workers", get(list_workers))
.route("/api/hosts/{host_id}/workers", get(list_host_workers))
.fallback(get(static_or_spa_fallback))
@@ -381,7 +383,7 @@ async fn repository_tickets(
source: "workspace_local_ticket_fallback".to_string(),
diagnostics: vec![RuntimeDiagnostic {
code: "repository_ticket_target_metadata_absent".to_string(),
severity: "info".to_string(),
severity: DiagnosticSeverity::Info,
message: "Ticket target Repository metadata is not available yet; Kanban groups all workspace-local Tickets by state as a read-only fallback.".to_string(),
}],
}))
@@ -396,11 +398,25 @@ async fn list_hosts(
workspace_id: api.config.workspace_id,
limit,
items: runtime_hosts.items,
source: "worker_runtime".to_string(),
source: "worker_runtime_registry".to_string(),
diagnostics: runtime_hosts.diagnostics,
}))
}
async fn list_runtimes(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<RuntimeSummary>>> {
let limit = api.config.max_records.min(200);
let runtimes = api.runtime.list_runtimes(limit);
Ok(Json(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items: runtimes.items,
source: "worker_runtime_registry".to_string(),
diagnostics: runtimes.diagnostics,
}))
}
async fn list_workers(
State(api): State<WorkspaceApi>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
@@ -411,16 +427,18 @@ async fn list_host_workers(
State(api): State<WorkspaceApi>,
AxumPath(host_id): AxumPath<String>,
) -> ApiResult<Json<RuntimeListResponse<WorkerSummary>>> {
let runtime_hosts = api.runtime.list_hosts(1);
let expected_host_id = runtime_hosts
.items
.first()
.map(|host| host.host_id.as_str())
.ok_or_else(|| Error::UnknownHost(host_id.clone()))?;
if host_id != expected_host_id {
return Err(Error::UnknownHost(host_id).into());
}
workers_response(api).map(Json)
let limit = api.config.max_records.min(200);
let runtime_workers = api
.runtime
.list_workers_for_host(&host_id, limit)
.map_err(|err| err.into_error())?;
Ok(Json(RuntimeListResponse {
workspace_id: api.config.workspace_id,
limit,
items: runtime_workers.items,
source: "worker_runtime_registry".to_string(),
diagnostics: runtime_workers.diagnostics,
}))
}
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
@@ -430,7 +448,7 @@ fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSu
workspace_id: api.config.workspace_id,
limit,
items: runtime_workers.items,
source: "worker_runtime".to_string(),
source: "worker_runtime_registry".to_string(),
diagnostics: runtime_workers.diagnostics,
})
}
@@ -589,11 +607,14 @@ impl From<Error> for ApiError {
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status = match &self.0 {
Error::InvalidRuntimeIdentifier { .. } => StatusCode::BAD_REQUEST,
Error::InvalidRecordId(_)
| Error::MissingFrontmatter(_)
| Error::UnknownHost(_)
| Error::UnknownWorker(_)
| Error::UnknownRepository(_) => StatusCode::NOT_FOUND,
Error::Ticket(_) => StatusCode::NOT_FOUND,
Error::RuntimeCapabilityUnsupported { .. } => StatusCode::NOT_IMPLEMENTED,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(
@@ -703,18 +724,30 @@ mod tests {
assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND);
let hosts = get_json(app.clone(), "/api/hosts").await;
assert_eq!(hosts["source"], "worker_runtime_registry");
assert_eq!(hosts["items"][0]["runtime_id"], "local-pod-runtime");
assert_eq!(hosts["items"][0]["host_id"], TEST_REPOSITORY_ID);
assert_eq!(hosts["items"][0]["kind"], "local_host");
assert_eq!(hosts["items"][0]["kind"], "local-pod-host");
assert_eq!(
hosts["items"][0]["capabilities"]["local_pod_inspection"],
"unavailable"
"available"
);
assert_eq!(
hosts["items"][0]["capabilities"]["workspace_scope"],
"current_workspace"
);
assert!(!hosts.to_string().contains("metadata.json"));
let runtimes = get_json(app.clone(), "/api/runtimes").await;
assert_eq!(runtimes["source"], "worker_runtime_registry");
assert_eq!(runtimes["items"][0]["runtime_id"], "local-pod-runtime");
assert_eq!(runtimes["items"][0]["host_ids"][0], TEST_REPOSITORY_ID);
let workers = get_json(app.clone(), "/api/workers").await;
assert!(workers["items"].as_array().unwrap().is_empty());
assert_eq!(
workers["diagnostics"][0]["code"],
"local_pod_metadata_root_missing"
"local_pod_registry_unreadable"
);
let host_workers = get_json(