Skip to content

Commit cc8d746

Browse files
committed
add status filter, name_starts_with filter, handle null releases on arch
1 parent 987a2f4 commit cc8d746

File tree

7 files changed

+178
-20
lines changed

7 files changed

+178
-20
lines changed

common/src/api/v1/models/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,30 @@ pub struct OriginFilter {
4545
#[derive(Debug, Clone, Serialize, Deserialize)]
4646
pub struct IdentityFilter {
4747
pub name: Option<String>,
48+
pub name_starts_with: Option<String>,
4849
pub version: Option<String>,
4950
}
5051

5152
#[derive(Debug, Clone, Serialize, Deserialize)]
5253
pub struct FreshnessFilter {
5354
pub seen_only: Option<bool>,
5455
}
56+
57+
#[derive(Debug, Clone, Serialize, Deserialize)]
58+
pub struct StatusFilter {
59+
#[serde(default, deserialize_with = "deserialize_comma_separated")]
60+
pub status: Option<Vec<String>>,
61+
}
62+
63+
fn deserialize_comma_separated<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
64+
where
65+
D: serde::Deserializer<'de>,
66+
{
67+
let s: Option<String> = Option::deserialize(deserializer)?;
68+
Ok(s.map(|s| {
69+
s.split(',')
70+
.map(|item| item.trim().to_string())
71+
.filter(|item| !item.is_empty())
72+
.collect()
73+
}))
74+
}

contrib/docs/rebuilderd-v1.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ paths:
272272

273273
- $ref: '#/components/parameters/name'
274274
- $ref: '#/components/parameters/version'
275+
- $ref: '#/components/parameters/status'
275276
responses:
276277
"200":
277278
description: Success
@@ -331,6 +332,7 @@ paths:
331332
- $ref: '#/components/parameters/name'
332333
- $ref: '#/components/parameters/version'
333334
- $ref: '#/components/parameters/architecture'
335+
- $ref: '#/components/parameters/status'
334336
responses:
335337
"200":
336338
description: Success
@@ -1468,7 +1470,19 @@ components:
14681470
schema:
14691471
type: string
14701472
description: |-
1471-
Filters the results by the name of the package.
1473+
Filters the results by the exact name of the package.
1474+
If both 'name' and 'name_starts_with' are specified, 'name' takes precedence.
1475+
name_starts_with:
1476+
in: query
1477+
name: name_starts_with
1478+
required: false
1479+
schema:
1480+
type: string
1481+
description: |-
1482+
Filters the results by packages whose names start with the given prefix.
1483+
Uses SQL LIKE with an implicit '%' wildcard at the end.
1484+
Example: 'lib' matches 'libfoo', 'libbar', etc.
1485+
If both 'name' and 'name_starts_with' are specified, 'name' takes precedence.
14721486
version:
14731487
in: query
14741488
name: version
@@ -1519,6 +1533,19 @@ components:
15191533
type: bool
15201534
description: |-
15211535
Filters the results by packages only seen in the latest sync.
1536+
status:
1537+
in: query
1538+
name: status
1539+
required: false
1540+
schema:
1541+
type: string
1542+
pattern: '^(GOOD|BAD|FAIL|UNKWN)(,(GOOD|BAD|FAIL|UNKWN))*$'
1543+
description: |-
1544+
Filters the results by rebuild/artifact status. Comma-separated list of status values.
1545+
For binary packages, filters by artifact status (GOOD, BAD, UNKWN).
1546+
For source packages, filters by rebuild status (GOOD, BAD, FAIL, UNKWN).
1547+
Values are case-insensitive.
1548+
Example: status=BAD,FAIL
15221549
securitySchemes:
15231550
AuthCookie:
15241551
type: apiKey

daemon/src/api/v1/meta.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::db::Pool;
33
use crate::schema::{build_inputs, source_packages};
44
use crate::{attestation, web};
55
use actix_web::{get, HttpResponse, Responder};
6-
use diesel::{QueryDsl, RunQueryDsl, SqliteExpressionMethods};
6+
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SqliteExpressionMethods};
77
use in_toto::crypto::PrivateKey;
88
use rebuilderd_common::api::v1::FreshnessFilter;
99
use rebuilderd_common::errors::Error;
@@ -93,10 +93,19 @@ pub async fn get_distribution_release_architectures(
9393
) -> web::Result<impl Responder> {
9494
let mut connection = pool.get().map_err(Error::from)?;
9595

96-
let distribution_release_architectures = source_packages::table
96+
let mut query = source_packages::table
9797
.inner_join(build_inputs::table)
9898
.filter(source_packages::distribution.is(&path.0))
99-
.filter(source_packages::release.is(&path.1))
99+
.into_boxed();
100+
101+
// Handle "null" string as NULL
102+
if path.1 == "null" {
103+
query = query.filter(source_packages::release.is_null());
104+
} else {
105+
query = query.filter(source_packages::release.is(&path.1));
106+
}
107+
108+
let distribution_release_architectures = query
100109
.filter(freshness_filter.into_inner().into_filter())
101110
.select(build_inputs::architecture)
102111
.distinct()
@@ -114,9 +123,18 @@ pub async fn get_distribution_release_components(
114123
) -> web::Result<impl Responder> {
115124
let mut connection = pool.get().map_err(Error::from)?;
116125

117-
let distribution_release_components = source_packages::table
126+
let mut query = source_packages::table
118127
.filter(source_packages::distribution.is(&path.0))
119-
.filter(source_packages::release.is(&path.1))
128+
.into_boxed();
129+
130+
// Handle "null" string as NULL
131+
if path.1 == "null" {
132+
query = query.filter(source_packages::release.is_null());
133+
} else {
134+
query = query.filter(source_packages::release.is(&path.1));
135+
}
136+
137+
let distribution_release_components = query
120138
.filter(freshness_filter.into_inner().into_filter())
121139
.select(source_packages::component)
122140
.distinct()
@@ -134,11 +152,26 @@ pub async fn get_distribution_release_component_architectures(
134152
) -> web::Result<impl Responder> {
135153
let mut connection = pool.get().map_err(Error::from)?;
136154

137-
let distribution_release_component_architectures = source_packages::table
155+
let mut query = source_packages::table
138156
.inner_join(build_inputs::table)
139157
.filter(source_packages::distribution.is(&path.0))
140-
.filter(source_packages::release.is(&path.1))
141-
.filter(source_packages::component.is(&path.2))
158+
.into_boxed();
159+
160+
// Handle empty release or "null" string as NULL
161+
if path.1.is_empty() || path.1 == "null" {
162+
query = query.filter(source_packages::release.is_null());
163+
} else {
164+
query = query.filter(source_packages::release.is(&path.1));
165+
}
166+
167+
// Handle empty component or "null" string as NULL
168+
if path.2.is_empty() || path.2 == "null" {
169+
query = query.filter(source_packages::component.is_null());
170+
} else {
171+
query = query.filter(source_packages::component.is(&path.2));
172+
}
173+
174+
let distribution_release_component_architectures = query
142175
.filter(freshness_filter.into_inner().into_filter())
143176
.select(build_inputs::architecture)
144177
.distinct()

daemon/src/api/v1/package.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use diesel::{
2121
};
2222
use rebuilderd_common::api::v1::{
2323
BuildStatus, FreshnessFilter, IdentityFilter, OriginFilter, PackageReport, Page, ResultPage,
24-
SourcePackageReport,
24+
SourcePackageReport, StatusFilter,
2525
};
2626
use rebuilderd_common::errors::Error;
2727

@@ -412,10 +412,11 @@ pub async fn get_source_packages(
412412
origin_filter: web::Query<OriginFilter>,
413413
identity_filter: web::Query<IdentityFilter>,
414414
freshness_filter: web::Query<FreshnessFilter>,
415+
status_filter: web::Query<StatusFilter>,
415416
) -> web::Result<impl Responder> {
416417
let mut connection = pool.get().map_err(Error::from)?;
417418

418-
let records = source_packages_base()
419+
let mut query = source_packages_base()
419420
.filter(
420421
origin_filter
421422
.clone()
@@ -429,11 +430,26 @@ pub async fn get_source_packages(
429430
.into_filter(source_packages::name, source_packages::version),
430431
)
431432
.filter(freshness_filter.clone().into_inner().into_filter())
433+
.into_boxed();
434+
435+
// Apply status filter if provided
436+
if let Some(ref statuses) = status_filter.status {
437+
if !statuses.is_empty() {
438+
let status_values: Vec<String> = statuses.iter().map(|s| s.to_uppercase()).collect();
439+
query = query.filter(
440+
r1.field(rebuilds::status).is_not_null().and(
441+
r1.field(rebuilds::status).assume_not_null().eq_any(status_values)
442+
)
443+
);
444+
}
445+
}
446+
447+
let records = query
432448
.paginate(page.into_inner())
433449
.load::<rebuilderd_common::api::v1::SourcePackage>(connection.as_mut())
434450
.map_err(Error::from)?;
435451

436-
let total = source_packages_base()
452+
let mut count_query = source_packages_base()
437453
.filter(
438454
origin_filter
439455
.clone()
@@ -447,6 +463,21 @@ pub async fn get_source_packages(
447463
.into_filter(source_packages::name, source_packages::version),
448464
)
449465
.filter(freshness_filter.into_inner().into_filter())
466+
.into_boxed();
467+
468+
// Apply status filter to count query
469+
if let Some(ref statuses) = status_filter.status {
470+
if !statuses.is_empty() {
471+
let status_values: Vec<String> = statuses.iter().map(|s| s.to_uppercase()).collect();
472+
count_query = count_query.filter(
473+
r1.field(rebuilds::status).is_not_null().and(
474+
r1.field(rebuilds::status).assume_not_null().eq_any(status_values)
475+
)
476+
);
477+
}
478+
}
479+
480+
let total = count_query
450481
.count()
451482
.get_result::<i64>(connection.as_mut())
452483
.map_err(Error::from)?;
@@ -480,10 +511,11 @@ pub async fn get_binary_packages(
480511
origin_filter: web::Query<OriginFilter>,
481512
identity_filter: web::Query<IdentityFilter>,
482513
freshness_filter: web::Query<FreshnessFilter>,
514+
status_filter: web::Query<StatusFilter>,
483515
) -> web::Result<impl Responder> {
484516
let mut connection = pool.get().map_err(Error::from)?;
485517

486-
let records = binary_packages_base()
518+
let mut query = binary_packages_base()
487519
.filter(
488520
origin_filter
489521
.clone()
@@ -497,11 +529,26 @@ pub async fn get_binary_packages(
497529
.into_filter(binary_packages::name, binary_packages::version),
498530
)
499531
.filter(freshness_filter.clone().into_inner().into_filter())
532+
.into_boxed();
533+
534+
// Apply status filter if provided
535+
if let Some(ref statuses) = status_filter.status {
536+
if !statuses.is_empty() {
537+
let status_values: Vec<String> = statuses.iter().map(|s| s.to_uppercase()).collect();
538+
query = query.filter(
539+
rebuild_artifacts::status.is_not_null().and(
540+
rebuild_artifacts::status.assume_not_null().eq_any(status_values)
541+
)
542+
);
543+
}
544+
}
545+
546+
let records = query
500547
.paginate(page.into_inner())
501548
.load::<rebuilderd_common::api::v1::BinaryPackage>(connection.as_mut())
502549
.map_err(Error::from)?;
503550

504-
let total = binary_packages_base()
551+
let mut count_query = binary_packages_base()
505552
.filter(
506553
origin_filter
507554
.clone()
@@ -515,6 +562,21 @@ pub async fn get_binary_packages(
515562
.into_inner()
516563
.into_filter(binary_packages::name, binary_packages::version),
517564
)
565+
.into_boxed();
566+
567+
// Apply status filter to count query
568+
if let Some(ref statuses) = status_filter.status {
569+
if !statuses.is_empty() {
570+
let status_values: Vec<String> = statuses.iter().map(|s| s.to_uppercase()).collect();
571+
count_query = count_query.filter(
572+
rebuild_artifacts::status.is_not_null().and(
573+
rebuild_artifacts::status.assume_not_null().eq_any(status_values)
574+
)
575+
);
576+
}
577+
}
578+
579+
let total = count_query
518580
.count()
519581
.get_result::<i64>(connection.as_mut())
520582
.map_err(Error::from)?;

daemon/src/api/v1/queue.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ pub async fn request_rebuild(
111111

112112
let identity_filter = IdentityFilter {
113113
name: queue_request.name,
114+
name_starts_with: None,
114115
version: queue_request.version,
115116
};
116117

daemon/src/api/v1/util/filters.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use diesel::query_builder::QueryFragment;
66
use diesel::sql_types::{Bool, Text};
77
use diesel::sqlite::Sqlite;
88
use diesel::{BoolExpressionMethods, BoxableExpression, Expression, SelectableExpression};
9-
use diesel::{ExpressionMethods, SqliteExpressionMethods};
9+
use diesel::{ExpressionMethods, SqliteExpressionMethods, TextExpressionMethods};
1010
use rebuilderd_common::api::v1::{FreshnessFilter, IdentityFilter, OriginFilter};
1111

1212
pub trait IntoIdentityFilter<QS, DB>
@@ -54,6 +54,7 @@ impl<T: 'static> IntoIdentityFilter<T, Sqlite> for IdentityFilter {
5454
+ QueryFragment<Sqlite>
5555
+ ValidGrouping<(), IsAggregate = No>
5656
+ ExpressionMethods
57+
+ SqliteExpressionMethods
5758
+ Send
5859
+ 'static,
5960
VersionColumn: SelectableExpression<T>
@@ -64,9 +65,18 @@ impl<T: 'static> IntoIdentityFilter<T, Sqlite> for IdentityFilter {
6465
+ Send
6566
+ 'static,
6667
{
67-
let name_is: Self::Output = match self.name {
68-
Some(name) => Box::new(name_column.is(name)),
69-
None => Box::new(AsExpression::<Bool>::as_expression(true)),
68+
// If both name and name_starts_with are set, name takes precedence
69+
let name_is: Self::Output = match (self.name, self.name_starts_with) {
70+
(Some(name), _) => {
71+
// Exact match for name
72+
Box::new(name_column.is(name))
73+
},
74+
(None, Some(prefix)) => {
75+
// LIKE pattern for name_starts_with - append % to the prefix
76+
let pattern = format!("{}%", prefix);
77+
Box::new(name_column.like(pattern))
78+
},
79+
(None, None) => Box::new(AsExpression::<Bool>::as_expression(true)),
7080
};
7181

7282
let version_is: Self::Output = match self.version {
@@ -130,12 +140,14 @@ where
130140
};
131141

132142
let release_is: Self::Output = match self.release {
133-
Some(release) => Box::new(source_packages::release.is(release)),
143+
Some(release) if !release.is_empty() => Box::new(source_packages::release.is(release)),
144+
Some(_) => Box::new(source_packages::release.is_null()), // Empty string means NULL
134145
None => Box::new(AsExpression::<Bool>::as_expression(true)),
135146
};
136147

137148
let component_is: Self::Output = match self.component {
138-
Some(component) => Box::new(source_packages::component.is(component)),
149+
Some(component) if !component.is_empty() => Box::new(source_packages::component.is(component)),
150+
Some(_) => Box::new(source_packages::component.is_null()), // Empty string means NULL
139151
None => Box::new(AsExpression::<Bool>::as_expression(true)),
140152
};
141153

tools/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ async fn lookup_package(client: &Client, filter: PkgsFilter) -> Result<BinaryPac
112112

113113
let identity_filter = IdentityFilter {
114114
name: filter.name,
115+
name_starts_with: None,
115116
version: None, // TODO: ls.filter.version
116117
};
117118

@@ -221,6 +222,7 @@ async fn main() -> Result<()> {
221222

222223
let identity_filter = IdentityFilter {
223224
name: ls.filter.name,
225+
name_starts_with: None,
224226
version: None, // TODO: ls.filter.version
225227
};
226228

@@ -415,6 +417,7 @@ async fn main() -> Result<()> {
415417

416418
let identity_filter = IdentityFilter {
417419
name: Some(push.name),
420+
name_starts_with: None,
418421
version: push.version,
419422
};
420423

0 commit comments

Comments
 (0)