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
54
Cargo.lock
generated
54
Cargo.lock
generated
|
|
@ -913,6 +913,7 @@ dependencies = [
|
|||
"sha2",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"tar",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
|
|
@ -921,6 +922,18 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"xz2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1685,6 +1698,17 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2992,6 +3016,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
|
|
@ -3992,6 +4027,25 @@ version = "0.6.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust2"
|
||||
version = "0.10.4"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ regex = "1.12.3"
|
|||
reqwest = { version = "0.13.2", default-features = false, features = [ "json", "rustls" ] }
|
||||
serde = { version = "1.0.228", features = [ "derive" ] }
|
||||
serde_json = "1.0.149"
|
||||
tar = "0.4"
|
||||
sha2 = "0.10.9"
|
||||
sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "migrate" ] }
|
||||
subtle = "2.6.1"
|
||||
|
|
@ -63,6 +64,7 @@ tower-http = { version = "0.6.8", features = [ "cors", "trace", "limit", "fs", "
|
|||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = [ "env-filter", "json" ] }
|
||||
urlencoding = "2.1.3"
|
||||
xz2 = "0.1"
|
||||
uuid = { version = "1.18.1", features = [ "v4", "serde" ] }
|
||||
|
||||
[profile.release]
|
||||
|
|
|
|||
|
|
@ -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