wizard: materialize picked repo across clawstor fleet at step-2-next (#5)
This commit was merged in pull request #5.
This commit is contained in:
@@ -22,6 +22,7 @@ pub mod repos;
|
||||
pub mod research;
|
||||
pub mod research_pipeline;
|
||||
pub mod research_setup;
|
||||
pub mod wizard_repo;
|
||||
pub mod routines;
|
||||
pub mod sessions;
|
||||
pub mod skills;
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Wizard-driven clawstor repo materialization.
|
||||
//!
|
||||
//! Bridges the research wizard (frontend) to clawstor's fleet-wide
|
||||
//! `POST /api/v2/repos/{ensure,release}` primitives so a picked repo
|
||||
//! is checked out on every clawstor peer at step-2-next, and released
|
||||
//! if the user backs out.
|
||||
//!
|
||||
//! The clawstor bearer token is server-side only; the frontend never
|
||||
//! sees it. Endpoints require the standard `Authed` extractor and
|
||||
//! resolve the picked `repo_id` against the caller's workspace so a
|
||||
//! user cannot ensure a repo they can't see.
|
||||
//!
|
||||
//! Configured via env:
|
||||
//! CLAWSTOR_URL — aggregator base, e.g. https://quantum.taila4f562.ts.net/clawstor
|
||||
//! CLAWSTOR_TOKEN — bearer token whose namespace scopes the writes
|
||||
//!
|
||||
//! Both missing = disabled (500). Not-configured is a deploy-time
|
||||
//! decision; runtime callers get a plain error.
|
||||
|
||||
use axum::{extract::State, Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RepoBody {
|
||||
pub repo_id: Uuid,
|
||||
/// Git ref (branch, tag, or SHA the remote will accept via
|
||||
/// `git clone --branch`). When omitted, falls back to the repo's
|
||||
/// recorded `default_branch`.
|
||||
#[serde(default)]
|
||||
pub git_ref: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PeerResult {
|
||||
pub peer: String,
|
||||
pub ok: bool,
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub head_sha: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cached: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub removed: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FanoutReply {
|
||||
pub url: String,
|
||||
pub git_ref: String,
|
||||
pub workspace: String,
|
||||
pub peers: Vec<PeerResult>,
|
||||
pub all_ok: bool,
|
||||
}
|
||||
|
||||
/// `POST /api/research/wizard/repo/ensure` — materialize the picked
|
||||
/// repo across the clawstor fleet. Returns the aggregator's per-peer
|
||||
/// reply so the wizard can render which nodes succeeded.
|
||||
pub async fn ensure_repo(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Json(body): Json<RepoBody>,
|
||||
) -> Result<Json<FanoutReply>, ApiError> {
|
||||
proxy(&state, &user, body, "ensure").await
|
||||
}
|
||||
|
||||
/// `POST /api/research/wizard/repo/release` — inverse of ensure.
|
||||
/// Called by the wizard on cancel (modal close before submit).
|
||||
pub async fn release_repo(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Json(body): Json<RepoBody>,
|
||||
) -> Result<Json<FanoutReply>, ApiError> {
|
||||
proxy(&state, &user, body, "release").await
|
||||
}
|
||||
|
||||
async fn proxy(
|
||||
state: &AppState,
|
||||
user: &cm_auth::AuthedUser,
|
||||
body: RepoBody,
|
||||
action: &str,
|
||||
) -> Result<Json<FanoutReply>, ApiError> {
|
||||
// Workspace-scoped lookup — a caller can't touch repos outside
|
||||
// their own workspace even if they know the id.
|
||||
let repo = cm_db::repo::repos::get(&state.pool, body.repo_id, user.workspace_id).await?;
|
||||
let url = repo.clone_url.ok_or(ApiError::BadRequest)?;
|
||||
let git_ref = body
|
||||
.git_ref
|
||||
.as_deref()
|
||||
.map(str::to_string)
|
||||
.or(repo.default_branch)
|
||||
.ok_or(ApiError::BadRequest)?;
|
||||
if url.trim().is_empty() || git_ref.trim().is_empty() {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
|
||||
let base = std::env::var("CLAWSTOR_URL").map_err(|_| ApiError::Internal)?;
|
||||
let token = std::env::var("CLAWSTOR_TOKEN").map_err(|_| ApiError::Internal)?;
|
||||
let endpoint = format!("{}/api/v2/repos/{}", base.trim_end_matches('/'), action);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(360))
|
||||
.build()
|
||||
.map_err(|_| ApiError::Internal)?;
|
||||
let resp = client
|
||||
.post(&endpoint)
|
||||
.bearer_auth(token)
|
||||
.json(&serde_json::json!({
|
||||
"url": url,
|
||||
"git_ref": git_ref,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal)?;
|
||||
if !resp.status().is_success() {
|
||||
return Err(ApiError::Internal);
|
||||
}
|
||||
let reply: FanoutReply = resp.json().await.map_err(|_| ApiError::Internal)?;
|
||||
Ok(Json(reply))
|
||||
}
|
||||
Reference in New Issue
Block a user