feat: add compute

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-21 00:42:17 +01:00
parent 04e452ecc3
commit 7188b44624
17 changed files with 1307 additions and 3 deletions

View File

@@ -142,6 +142,12 @@ pub fn router() -> Router<AppState> {
get(timeline_api),
)
.route("/api/orgs/{org}/timeline", get(org_timeline_api))
.route("/orgs/{org}/compute", get(compute_page))
.route(
"/orgs/{org}/compute/rollouts/{rollout_id}",
get(rollout_detail_page),
)
.route("/api/compute/regions", get(regions_api))
}
fn orgs_context(orgs: &[CachedOrg]) -> Vec<minijinja::Value> {
@@ -4989,3 +4995,202 @@ async fn get_plan_output_api(
}))
.into_response())
}
// ---------------------------------------------------------------------------
// Compute
// ---------------------------------------------------------------------------
async fn compute_page(
State(state): State<AppState>,
session: Session,
Path(org): Path<String>,
) -> Result<Response, Response> {
let orgs = &session.user.orgs;
let _cached_org = require_org_membership(&state, orgs, &org)?;
let (instances, rollouts) = if let Some(ref scheduler) = state.compute_scheduler {
let namespace = &org;
let instances = scheduler
.list_instances(namespace)
.await
.unwrap_or_default();
let rollouts = scheduler
.list_rollouts(namespace)
.await
.unwrap_or_default();
(instances, rollouts)
} else {
(vec![], vec![])
};
let instances_ctx: Vec<minijinja::Value> = instances
.iter()
.map(|i| {
context! {
id => i.id,
resource_name => i.resource_name,
project => i.project,
destination => i.destination,
environment => i.environment,
image => i.image,
region => i.region,
replicas => i.replicas,
cpu => i.cpu,
memory => i.memory,
status => i.status,
}
})
.collect();
let rollouts_ctx: Vec<minijinja::Value> = rollouts
.iter()
.take(20)
.map(|r| {
let resources: Vec<minijinja::Value> = r
.resources
.iter()
.map(|res| {
context! {
name => res.name,
kind => res.kind.to_string(),
status => res.status.to_string(),
message => res.message,
}
})
.collect();
context! {
id => r.id,
apply_id => r.apply_id,
namespace => r.namespace,
status => r.status.to_string(),
resources => resources,
}
})
.collect();
let projects = warn_default(
"compute: list projects",
state
.platform_client
.list_projects(&session.access_token, &org)
.await,
);
let html = state
.templates
.render(
"pages/compute.html.jinja",
context! {
title => format!("Compute - {} - Forage", org),
description => "Managed compute instances",
user => context! { username => session.user.username },
csrf_token => &session.csrf_token,
orgs => orgs_context(orgs),
current_org => &org,
active_tab => "compute",
projects => projects,
instances => instances_ctx,
rollouts => rollouts_ctx,
org_name => &org,
},
)
.map_err(|e| internal_error(&state, "compute render", &e))?;
Ok(Html(html).into_response())
}
async fn rollout_detail_page(
State(state): State<AppState>,
session: Session,
Path((org, rollout_id)): Path<(String, String)>,
) -> Result<Response, Response> {
let orgs = &session.user.orgs;
let _cached_org = require_org_membership(&state, orgs, &org)?;
let scheduler = state.compute_scheduler.as_ref().ok_or_else(|| {
error_page(
&state,
StatusCode::NOT_FOUND,
"Not available",
"Compute is not enabled.",
)
})?;
let rollout = scheduler.get_rollout(&rollout_id).await.map_err(|_| {
error_page(
&state,
StatusCode::NOT_FOUND,
"Not found",
"Rollout not found.",
)
})?;
let resources_ctx: Vec<minijinja::Value> = rollout
.resources
.iter()
.map(|r| {
context! {
name => r.name,
kind => r.kind.to_string(),
status => r.status.to_string(),
message => r.message,
}
})
.collect();
let labels_ctx: Vec<minijinja::Value> = rollout.labels.iter().map(|(k, v)| context! { key => k, value => v }).collect();
let rollout_ctx = context! {
id => rollout.id,
apply_id => rollout.apply_id,
namespace => rollout.namespace,
status => rollout.status.to_string(),
resources => resources_ctx,
labels => labels_ctx,
};
let projects = warn_default(
"rollout detail: list projects",
state
.platform_client
.list_projects(&session.access_token, &org)
.await,
);
let html = state
.templates
.render(
"pages/rollout_detail.html.jinja",
context! {
title => format!("Rollout {} - Forage", rollout.apply_id),
description => "Rollout details",
user => context! { username => session.user.username },
csrf_token => &session.csrf_token,
orgs => orgs_context(orgs),
current_org => &org,
active_tab => "compute",
projects => projects,
rollout => rollout_ctx,
org_name => &org,
},
)
.map_err(|e| internal_error(&state, "rollout detail render", &e))?;
Ok(Html(html).into_response())
}
async fn regions_api() -> impl IntoResponse {
let regions: Vec<serde_json::Value> = forage_core::compute::REGIONS
.iter()
.map(|r| {
serde_json::json!({
"id": r.id,
"name": r.name,
"display_name": r.display_name,
"available": r.available,
})
})
.collect();
Json(regions)
}