diff --git a/common/src/api/v1/mod.rs b/common/src/api/v1/mod.rs index 861293df..fc0aa2ff 100644 --- a/common/src/api/v1/mod.rs +++ b/common/src/api/v1/mod.rs @@ -110,6 +110,7 @@ pub trait BuildRestApi { async fn get_build_artifacts(&self, id: i32) -> Result>; async fn get_build_artifact(&self, id: i32, artifact_id: i32) -> Result; async fn get_build_artifact_diffoscope(&self, id: i32, artifact_id: i32) -> Result; + async fn get_build_diffoscope(&self, id: i32) -> Result; async fn get_build_artifact_attestation(&self, id: i32, artifact_id: i32) -> Result>; } @@ -167,6 +168,12 @@ pub trait PackageRestApi { ) -> Result>; async fn get_binary_package(&self, id: i32) -> Result; + + async fn get_transition_packages( + &self, + origin_filter: Option<&OriginFilter>, + identity_filter: Option<&IdentityFilter>, + ) -> Result; } #[async_trait] @@ -294,6 +301,20 @@ impl BuildRestApi for Client { Ok(data) } + async fn get_build_diffoscope(&self, diffoscope_log_id: i32) -> Result { + let data = self + .get(Cow::Owned(format!( + "api/v1/builds/diffoscope/{diffoscope_log_id}" + ))) + .send() + .await? + .error_for_status()? + .text() + .await?; + + Ok(data) + } + async fn get_build_artifact_attestation(&self, id: i32, artifact_id: i32) -> Result> { let data = self .get(Cow::Owned(format!( @@ -524,6 +545,24 @@ impl PackageRestApi for Client { Ok(record) } + + async fn get_transition_packages( + &self, + origin_filter: Option<&OriginFilter>, + identity_filter: Option<&IdentityFilter>, + ) -> Result { + let records = self + .get(Cow::Borrowed("api/v1/packages/transition_packages")) + .query(&origin_filter) + .query(&identity_filter) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(records) + } } #[async_trait] diff --git a/common/src/api/v1/models/mod.rs b/common/src/api/v1/models/mod.rs index a03ecf66..2eb8f35a 100644 --- a/common/src/api/v1/models/mod.rs +++ b/common/src/api/v1/models/mod.rs @@ -37,7 +37,11 @@ pub struct ResultPage { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OriginFilter { pub distribution: Option, - pub release: Option, + + #[serde(with = "string_separated_vec")] + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub release: Vec, pub component: Option, pub architecture: Option, } @@ -52,3 +56,52 @@ pub struct IdentityFilter { pub struct FreshnessFilter { pub seen_only: Option, } + +mod string_separated_vec { + use serde::de::Error; + use serde::{Deserializer, Serializer, de}; + use std::fmt::Formatter; + + const SEPARATOR: &str = ","; + + pub fn serialize(value: &Vec, serializer: S) -> Result + where + S: Serializer, + { + let serialized = value.join(SEPARATOR); + serializer.serialize_str(&serialized) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct StringSeparatedVisitor; + impl<'de> de::Visitor<'de> for StringSeparatedVisitor { + type Value = Vec; + + fn expecting( + &self, + formatter: &mut Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + write!(formatter, "a string") + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + self.visit_str(&v) + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + Ok(v.split(SEPARATOR).map(|s| s.to_string()).collect()) + } + } + + deserializer.deserialize_str(StringSeparatedVisitor) + } +} diff --git a/common/src/api/v1/models/package.rs b/common/src/api/v1/models/package.rs index bf2f3e58..9c2e4707 100644 --- a/common/src/api/v1/models/package.rs +++ b/common/src/api/v1/models/package.rs @@ -65,3 +65,15 @@ pub struct BinaryPackage { pub last_seen: NaiveDateTime, pub seen_in_last_sync: bool, } + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "diesel", derive(Queryable))] +#[cfg_attr(feature = "diesel", diesel(check_for_backend(diesel::sqlite::Sqlite)))] +pub struct TransitionBinaryPackage { + pub name: String, + pub version: String, + pub architecture: String, + pub status: Option, + pub build_id: Option, + pub diffoscope_log_id: Option, +} diff --git a/daemon/src/api/v1/build.rs b/daemon/src/api/v1/build.rs index dafddd90..d170385d 100644 --- a/daemon/src/api/v1/build.rs +++ b/daemon/src/api/v1/build.rs @@ -393,6 +393,28 @@ pub async fn get_build_artifact_diffoscope( } } +#[get("/diffoscope/{diffoscope_log_id}")] +pub async fn get_build_diffoscope( + req: HttpRequest, + pool: web::Data, + diffoscope_log_id: web::Path, +) -> web::Result { + let mut connection = pool.get().map_err(Error::from)?; + + let diffoscope = diffoscope_logs::table + .filter(diffoscope_logs::id.is(diffoscope_log_id.into_inner())) + .select(diffoscope_logs::diffoscope_log.nullable()) + .first::>>(connection.as_mut()) + .optional() + .map_err(Error::from)?; + + if let Some(diffoscope) = diffoscope.flatten() { + forward_compressed_data(req, "text/plain; charset=utf-8", diffoscope).await + } else { + Ok(HttpResponse::NotFound().finish()) + } +} + #[get("/{id}/artifacts/{artifact_id}/attestation")] pub async fn get_build_artifact_attestation( req: HttpRequest, diff --git a/daemon/src/api/v1/package.rs b/daemon/src/api/v1/package.rs index 503c2f8b..268ecc7c 100644 --- a/daemon/src/api/v1/package.rs +++ b/daemon/src/api/v1/package.rs @@ -64,6 +64,39 @@ fn source_packages_base() -> _ { )) } +#[diesel::dsl::auto_type] +fn transition_binary_packages_base() -> _ { + binary_packages::table + .inner_join(source_packages::table) + .inner_join(build_inputs::table) + .left_join(r1.on(r1.field(rebuilds::build_input_id).is(build_inputs::id))) + .left_join( + rebuild_artifacts::table.on(rebuild_artifacts::rebuild_id + .is(r1.field(rebuilds::id)) + .and(rebuild_artifacts::name.is(binary_packages::name))), + ) + .left_join( + r2.on(r2.field(rebuilds::build_input_id).is(build_inputs::id).and( + r1.field(rebuilds::built_at) + .lt(r2.field(rebuilds::built_at)) + .or(r1.fields( + rebuilds::built_at + .eq(r2.field(rebuilds::built_at)) + .and(r1.field(rebuilds::id).lt(r2.field(rebuilds::id))), + )), + )), + ) + .filter(r2.field(rebuilds::id).is_null()) + .select(( + binary_packages::name, + binary_packages::version, + binary_packages::architecture, + rebuild_artifacts::status.nullable(), + r1.field(rebuilds::id).nullable(), + rebuild_artifacts::diffoscope_log_id.nullable(), + )) +} + #[diesel::dsl::auto_type] fn binary_packages_base() -> _ { binary_packages::table @@ -471,6 +504,36 @@ pub async fn get_source_package( } } +#[get("/transition_packages")] +pub async fn get_transition_packages( + pool: web::Data, + origin_filter: web::Query, + identity_filter: web::Query, + freshness_filter: web::Query, +) -> web::Result { + let mut connection = pool.get().map_err(Error::from)?; + + let records = transition_binary_packages_base() + .filter( + origin_filter + .clone() + .into_inner() + .into_filter(binary_packages::architecture), + ) + .filter( + identity_filter + .clone() + .into_inner() + .into_filter(binary_packages::name, binary_packages::version), + ) + .filter(freshness_filter.clone().into_inner().into_filter()) + .distinct() + .load::(connection.as_mut()) + .map_err(Error::from)?; + + Ok(HttpResponse::Ok().json(records)) +} + #[get("/binary")] pub async fn get_binary_packages( pool: web::Data, diff --git a/daemon/src/api/v1/queue.rs b/daemon/src/api/v1/queue.rs index fb72e539..a9122557 100644 --- a/daemon/src/api/v1/queue.rs +++ b/daemon/src/api/v1/queue.rs @@ -110,7 +110,7 @@ pub async fn request_rebuild( let origin_filter = OriginFilter { distribution: queue_request.distribution, - release: queue_request.release, + release: queue_request.release.as_slice().to_vec(), component: queue_request.component, architecture: queue_request.architecture, }; diff --git a/daemon/src/api/v1/util/filters.rs b/daemon/src/api/v1/util/filters.rs index 8ed7fb9f..3b133ee0 100644 --- a/daemon/src/api/v1/util/filters.rs +++ b/daemon/src/api/v1/util/filters.rs @@ -3,12 +3,19 @@ use diesel::backend::Backend; use diesel::expression::is_aggregate::No; use diesel::expression::{AsExpression, ValidGrouping}; use diesel::query_builder::QueryFragment; -use diesel::sql_types::{Bool, Text}; +use diesel::sql_types::{Bool, Nullable, Text}; use diesel::sqlite::Sqlite; -use diesel::{BoolExpressionMethods, BoxableExpression, Expression, SelectableExpression}; +use diesel::{ + BoolExpressionMethods, BoxableExpression, Expression, SelectableExpression, define_sql_function, +}; use diesel::{ExpressionMethods, SqliteExpressionMethods}; use rebuilderd_common::api::v1::{FreshnessFilter, IdentityFilter, OriginFilter}; +define_sql_function! { + #[sql_name = "COALESCE"] + fn sqlite_coalesce(value: Nullable, default: Bool) -> Bool; +} + pub trait IntoIdentityFilter where DB: Backend, @@ -129,9 +136,13 @@ where None => Box::new(AsExpression::::as_expression(true)), }; - let release_is: Self::Output = match self.release { - Some(release) => Box::new(source_packages::release.is(release)), - None => Box::new(AsExpression::::as_expression(true)), + let release_is: Self::Output = if !self.release.is_empty() { + Box::new(sqlite_coalesce( + source_packages::release.eq_any(self.release), + false, + )) + } else { + Box::new(AsExpression::::as_expression(true)) }; let component_is: Self::Output = match self.component { diff --git a/daemon/src/lib.rs b/daemon/src/lib.rs index 98f80a37..b9010660 100644 --- a/daemon/src/lib.rs +++ b/daemon/src/lib.rs @@ -71,6 +71,7 @@ pub fn build_server( .service(api::v1::get_build_artifacts) .service(api::v1::get_build_artifact) .service(api::v1::get_build_artifact_diffoscope) + .service(api::v1::get_build_diffoscope) .service(api::v1::get_build_artifact_attestation), ) .service(scope("/dashboard").service(api::v1::get_dashboard)) @@ -93,6 +94,7 @@ pub fn build_server( .service(api::v1::get_source_packages) .service(api::v1::get_source_package) .service(api::v1::get_binary_packages) + .service(api::v1::get_transition_packages) .service(api::v1::get_binary_package), ) .service( diff --git a/tools/src/main.rs b/tools/src/main.rs index 7cf27a1d..b31e28cf 100644 --- a/tools/src/main.rs +++ b/tools/src/main.rs @@ -104,7 +104,7 @@ pub async fn submit_package_report(client: &Client, sync: &PackageReport) -> Res async fn lookup_package(client: &Client, filter: PkgsFilter) -> Result { let origin_filter = OriginFilter { distribution: filter.distro, - release: None, // TODO: ls.filter.release, + release: vec![], // TODO: ls.filter.release, component: filter.suite, architecture: filter.architecture, }; @@ -219,7 +219,7 @@ async fn main() -> Result<()> { SubCommand::Pkgs(Pkgs::Ls(ls)) => { let origin_filter = OriginFilter { distribution: ls.filter.distro, - release: None, // TODO: ls.filter.release, + release: vec![], // TODO: ls.filter.release, component: ls.filter.suite, architecture: ls.filter.architecture, }; @@ -424,7 +424,7 @@ async fn main() -> Result<()> { SubCommand::Queue(Queue::Delete(push)) => { let origin_filter = OriginFilter { distribution: Some(push.distro), - release: None, // TODO: ls.filter.release, + release: vec![], // TODO: ls.filter.release, component: Some(push.suite), architecture: push.architecture, };