fc-server: add keep flag toggle and nixexprs.tar.xz channel endpoint
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I3411f76548f8061e835631a7928f899d6a6a6964
This commit is contained in:
parent
5b472a2f57
commit
5410fdc044
5 changed files with 203 additions and 2 deletions
|
|
@ -26,6 +26,7 @@ serde_json.workspace = true
|
|||
sha2.workspace = true
|
||||
sqlx.workspace = true
|
||||
subtle.workspace = true
|
||||
tar.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
|
|
@ -34,6 +35,7 @@ tower-http.workspace = true
|
|||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace = true
|
||||
xz2.workspace = true
|
||||
|
||||
# Our crates
|
||||
fc-common.workspace = true
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use axum::{
|
|||
extract::{Path, Query, State},
|
||||
http::{Extensions, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
routing::{get, post, put},
|
||||
};
|
||||
use fc_common::{
|
||||
Build,
|
||||
|
|
@ -302,6 +302,24 @@ async fn download_build_product(
|
|||
}
|
||||
}
|
||||
|
||||
async fn set_keep_flag(
|
||||
_auth: crate::auth_middleware::RequireAdmin,
|
||||
State(state): State<AppState>,
|
||||
Path((id, value)): Path<(Uuid, bool)>,
|
||||
) -> Result<Json<Build>, ApiError> {
|
||||
let build = fc_common::repo::builds::set_keep(&state.pool, id, value)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
tracing::info!(
|
||||
build_id = %id,
|
||||
keep = value,
|
||||
"Build keep flag updated"
|
||||
);
|
||||
|
||||
Ok(Json(build))
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/builds", get(list_builds))
|
||||
|
|
@ -311,6 +329,7 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/builds/{id}/cancel", post(cancel_build))
|
||||
.route("/builds/{id}/restart", post(restart_build))
|
||||
.route("/builds/{id}/bump", post(bump_build))
|
||||
.route("/builds/{id}/keep/{value}", put(set_keep_flag))
|
||||
.route("/builds/{id}/steps", get(list_build_steps))
|
||||
.route("/builds/{id}/products", get(list_build_products))
|
||||
.route(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
Router,
|
||||
body::Body,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
use fc_common::{
|
||||
Validate,
|
||||
models::{Channel, CreateChannel},
|
||||
models::{BuildStatus, Channel, CreateChannel},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -102,10 +107,129 @@ async fn promote_channel(
|
|||
Ok(Json(channel))
|
||||
}
|
||||
|
||||
/// Generate and serve `nixexprs.tar.xz` for Nix channel compatibility.
|
||||
/// Contains a `default.nix` with fake derivations pointing at store paths,
|
||||
/// enabling `nix-channel --add <url>`.
|
||||
async fn nixexprs_tarball(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let channel = fc_common::repo::channels::get(&state.pool, id)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let evaluation_id = channel.current_evaluation_id.ok_or_else(|| {
|
||||
ApiError(fc_common::CiError::NotFound(
|
||||
"Channel has no current evaluation".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
let builds =
|
||||
fc_common::repo::builds::list_for_evaluation(&state.pool, evaluation_id)
|
||||
.await
|
||||
.map_err(ApiError)?;
|
||||
|
||||
let succeeded: Vec<_> = builds
|
||||
.iter()
|
||||
.filter(|b| b.status == BuildStatus::Succeeded)
|
||||
.collect();
|
||||
|
||||
if succeeded.is_empty() {
|
||||
return Err(ApiError(fc_common::CiError::NotFound(
|
||||
"No succeeded builds in current evaluation".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
// Generate default.nix
|
||||
let approx_size = 256 + succeeded.len() * 200;
|
||||
let mut nix_src = String::with_capacity(approx_size);
|
||||
let _ = writeln!(nix_src, "{{ system ? builtins.currentSystem }}:");
|
||||
let _ = writeln!(nix_src, "let");
|
||||
let _ = writeln!(nix_src, " mkFakeDerivation = attrs:");
|
||||
let _ = writeln!(
|
||||
nix_src,
|
||||
" let d = derivation (attrs // {{ builder = \"builtin:fetchurl\"; \
|
||||
preferLocalBuild = true; }});"
|
||||
);
|
||||
let _ = writeln!(
|
||||
nix_src,
|
||||
" in d // {{ type = \"derivation\"; inherit (d) outPath drvPath name \
|
||||
system; outputSpecified = true; }};"
|
||||
);
|
||||
let _ = writeln!(nix_src, "in {{");
|
||||
|
||||
for build in &succeeded {
|
||||
let output_path = match &build.build_output_path {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
let system = build.system.as_deref().unwrap_or("x86_64-linux");
|
||||
// Sanitize job_name for use as a Nix attribute (replace dots/slashes)
|
||||
let attr_name = build.job_name.replace('.', "-").replace('/', "-");
|
||||
let _ = writeln!(
|
||||
nix_src,
|
||||
" \"{attr_name}\" = mkFakeDerivation {{ name = \"{}\"; system = \
|
||||
\"{system}\"; outPath = \"{output_path}\"; }};",
|
||||
build.job_name.replace('"', "\\\""),
|
||||
);
|
||||
}
|
||||
|
||||
let _ = writeln!(nix_src, "}}");
|
||||
|
||||
// Build tar.xz archive in memory
|
||||
let xz_data =
|
||||
tokio::task::spawn_blocking(move || -> Result<Vec<u8>, String> {
|
||||
let mut xz_buf = Vec::new();
|
||||
{
|
||||
let xz_writer = xz2::write::XzEncoder::new(&mut xz_buf, 6);
|
||||
let mut tar_builder = tar::Builder::new(xz_writer);
|
||||
|
||||
let nix_bytes = nix_src.as_bytes();
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(nix_bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
|
||||
tar_builder
|
||||
.append_data(&mut header, "default.nix", nix_bytes)
|
||||
.map_err(|e| format!("Failed to append to tar: {e}"))?;
|
||||
|
||||
let xz_writer = tar_builder
|
||||
.into_inner()
|
||||
.map_err(|e| format!("Failed to finish tar: {e}"))?;
|
||||
xz_writer
|
||||
.finish()
|
||||
.map_err(|e| format!("Failed to finish xz: {e}"))?;
|
||||
}
|
||||
Ok(xz_buf)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ApiError(fc_common::CiError::Build(format!("Task join error: {e}")))
|
||||
})?
|
||||
.map_err(|e| ApiError(fc_common::CiError::Build(e)))?;
|
||||
|
||||
Ok(
|
||||
(
|
||||
StatusCode::OK,
|
||||
[
|
||||
("content-type", "application/x-xz"),
|
||||
(
|
||||
"content-disposition",
|
||||
"attachment; filename=\"nixexprs.tar.xz\"",
|
||||
),
|
||||
],
|
||||
Body::from(xz_data),
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/channels", get(list_channels).post(create_channel))
|
||||
.route("/channels/{id}", get(get_channel).delete(delete_channel))
|
||||
.route("/channels/{id}/nixexprs.tar.xz", get(nixexprs_tarball))
|
||||
.route(
|
||||
"/channels/{channel_id}/promote/{eval_id}",
|
||||
post(promote_channel),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue