Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5f8076c
update status endpoint
david-crespo Aug 21, 2025
f3b99a2
delete target_release_view, rework TargetRelease
david-crespo Sep 23, 2025
37ba90f
avoid _ in match, avoid future surprises
david-crespo Sep 24, 2025
1f82889
Option -> Nullable, helpful doc comment
david-crespo Sep 25, 2025
e2bb69e
merge main
david-crespo Sep 25, 2025
be43a1f
merge main
david-crespo Oct 1, 2025
3ca18a7
address dave's comments: docs, add paused field
david-crespo Oct 2, 2025
321374d
get current target blueprint from watch channel
david-crespo Oct 2, 2025
a2d6054
avoid redundant tuf repo retrieval
david-crespo Oct 2, 2025
d8e1e82
merge main
david-crespo Oct 7, 2025
5ab24ec
tuf repo list endpoint
david-crespo Sep 25, 2025
324aeed
add integration test for tuf repo list
david-crespo Sep 25, 2025
350c44c
switch to paginating by version
david-crespo Sep 25, 2025
0708c53
do artifacts fetch in a single query (still bad)
david-crespo Sep 25, 2025
442e9e1
try doing views::TufRepo
david-crespo Sep 26, 2025
8cc58c1
replace TufRepoInsertResponse with views::TufRepoUpload (tests fail)
david-crespo Sep 26, 2025
0ddef05
add lockstep endpoint for listing artifacts for a repo
david-crespo Sep 26, 2025
78373e2
make repo upload test pass using artifacts endpoint
david-crespo Sep 26, 2025
5cfe2b3
fetch artifacts directly from datastore, no endpoint required
david-crespo Sep 26, 2025
23a785b
never mind about that lockstep endpoint
david-crespo Sep 26, 2025
9e33fc9
make repo get and put endpoints more conventional
david-crespo Sep 26, 2025
de3c39e
self-review fixes
david-crespo Sep 26, 2025
9365744
no reference to "external" types in external API call tree
david-crespo Sep 27, 2025
4a9d869
remove targets_role_version and valid_until from TufRepo view, add ti…
david-crespo Oct 2, 2025
d73c2fe
make diff slightly smaller
david-crespo Oct 7, 2025
7693a68
use our own channel watcher instead of the quiesce handle
david-crespo Oct 7, 2025
e804964
address more comments from dave, including <= bug
david-crespo Oct 7, 2025
273058f
time_last_blueprint -> time_last_step_planned
david-crespo Oct 8, 2025
99f5d98
paused -> suspended
david-crespo Oct 8, 2025
576581d
a couple of helpful suggestions from gpt-5
david-crespo Oct 8, 2025
17b2ce7
merge update status PR
david-crespo Oct 8, 2025
894bef9
address dave's comments
david-crespo Oct 8, 2025
6293043
use view struct directly for datastore upload status
david-crespo Oct 9, 2025
9f004f6
delete unused tuf_list_repos, test pruned filtering
david-crespo Oct 9, 2025
a831883
merge main, tweak a couple more doc comments
david-crespo Oct 9, 2025
4d204b0
get repo should 404 on pruned repos
david-crespo Oct 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions common/src/api/external/http_pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ use dropshot::RequestContext;
use dropshot::ResultsPage;
use dropshot::WhichPage;
use schemars::JsonSchema;
use semver::Version;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeOwned;
Expand Down Expand Up @@ -163,6 +164,54 @@ pub fn marker_for_name_or_id<T: SimpleIdentityOrName, Selector>(
}
}

// Pagination by semantic version in ascending or descending order

/// Scan parameters for resources that support scanning by semantic version
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
pub struct ScanByVersion<Selector = ()> {
#[serde(default = "default_version_sort_mode")]
sort_by: VersionSortMode,
#[serde(flatten)]
pub selector: Selector,
}

/// Supported sort modes when scanning by semantic version
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VersionSortMode {
/// Sort in increasing semantic version order (oldest first)
VersionAscending,
/// Sort in decreasing semantic version order (newest first)
VersionDescending,
}

fn default_version_sort_mode() -> VersionSortMode {
VersionSortMode::VersionDescending
}

impl<T> ScanParams for ScanByVersion<T>
where
T: Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize,
{
type MarkerValue = Version;

fn direction(&self) -> PaginationOrder {
match self.sort_by {
VersionSortMode::VersionAscending => PaginationOrder::Ascending,
VersionSortMode::VersionDescending => PaginationOrder::Descending,
}
}

fn from_query(
p: &PaginationParams<Self, PageSelector<Self, Self::MarkerValue>>,
) -> Result<&Self, HttpError> {
Ok(match p.page {
WhichPage::First(ref scan_params) => scan_params,
WhichPage::Next(PageSelector { ref scan, .. }) => scan,
})
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda thought I already had this, but I guess not. We can sort by version because we store the version in the DB as a lexicographically-sortable string. Thanks, 2023 me.

/// Pad the version numbers with zeros so the result is lexicographically
/// sortable, e.g., `0.1.2` becomes `00000000.00000001.00000002`.
///
/// This requires that we impose a maximum size on each of the numbers so as not
/// to exceed the available number of digits.
///
/// An important caveat is that while lexicographic sort with padding does work
/// for the numeric part of the version string, it does not technically satisfy
/// the semver spec's rules for sorting pre-release and build metadata. Build
/// metadata is supposed to be ignored. Pre-release has more complicated rules,
/// most notably that a version *with* a pre-release string on it has lower
/// precedence than one *without*. See: <https://semver.org/#spec-item-11>. We
/// have decided sorting these wrong is tolerable for now. We can revisit later
/// if necessary.
///
/// Compare to the `Display` implementation on `Semver::Version`
/// <https://github.com/dtolnay/semver/blob/7fd09f7/src/display.rs>
fn to_sortable_string(v: &semver::Version) -> Result<String, external::Error> {

/// See `dropshot::ResultsPage::new`
fn page_selector_for<F, T, S, M>(
item: &T,
Expand Down Expand Up @@ -313,6 +362,13 @@ pub type PaginatedByNameOrId<Selector = ()> = PaginationParams<
pub type PageSelectorByNameOrId<Selector = ()> =
PageSelector<ScanByNameOrId<Selector>, NameOrId>;

/// Query parameters for pagination by semantic version
pub type PaginatedByVersion<Selector = ()> =
PaginationParams<ScanByVersion<Selector>, PageSelectorByVersion<Selector>>;
/// Page selector for pagination by semantic version
pub type PageSelectorByVersion<Selector = ()> =
PageSelector<ScanByVersion<Selector>, Version>;

pub fn id_pagination<'a, Selector>(
pag_params: &'a DataPageParams<Uuid>,
scan_params: &'a ScanById<Selector>,
Expand Down
38 changes: 4 additions & 34 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3427,6 +3427,10 @@ pub struct ServiceIcmpConfig {
pub enabled: bool,
}

// TODO: move these TUF repo structs out of this file. They're not external
// anymore after refactors that use views::TufRepo in the external API. They are
// still used extensively in internal services.

/// A description of an uploaded TUF repository.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
pub struct TufRepoDescription {
Expand Down Expand Up @@ -3501,40 +3505,6 @@ pub struct TufArtifactMeta {
pub sign: Option<Vec<u8>>,
}

/// Data about a successful TUF repo import into Nexus.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct TufRepoInsertResponse {
/// The repository as present in the database.
pub recorded: TufRepoDescription,

/// Whether this repository already existed or is new.
pub status: TufRepoInsertStatus,
}

/// Status of a TUF repo import.
///
/// Part of `TufRepoInsertResponse`.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum TufRepoInsertStatus {
/// The repository already existed in the database.
AlreadyExists,

/// The repository did not exist, and was inserted into the database.
Inserted,
}

/// Data about a successful TUF repo get from Nexus.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct TufRepoGetResponse {
/// The description of the repository.
pub description: TufRepoDescription,
}

#[derive(
Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq, ObjectIdentity,
)]
Expand Down
40 changes: 33 additions & 7 deletions nexus/db-model/src/tuf_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use nexus_db_schema::schema::{
tuf_artifact, tuf_repo, tuf_repo_artifact, tuf_trust_root,
};
use nexus_types::external_api::shared::TufSignedRootRole;
use nexus_types::external_api::views;
use nexus_types::external_api::views::{self, TufRepoUploadStatus};
use omicron_common::{api::external, update::ArtifactId};
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::TufArtifactKind;
Expand All @@ -30,8 +30,6 @@ use uuid::Uuid;

/// A description of a TUF update: a repo, along with the artifacts it
/// contains.
///
/// This is the internal variant of [`external::TufRepoDescription`].
#[derive(Debug, Clone)]
pub struct TufRepoDescription {
/// The repository.
Expand Down Expand Up @@ -64,7 +62,6 @@ impl TufRepoDescription {
}
}

/// Converts self into [`external::TufRepoDescription`].
pub fn into_external(self) -> external::TufRepoDescription {
external::TufRepoDescription {
repo: self.repo.into_external(),
Expand All @@ -78,8 +75,6 @@ impl TufRepoDescription {
}

/// A record representing an uploaded TUF repository.
///
/// This is the internal variant of [`external::TufRepoMeta`].
#[derive(
Queryable, Identifiable, Insertable, Clone, Debug, Selectable, AsChangeset,
)]
Expand Down Expand Up @@ -134,7 +129,6 @@ impl TufRepo {
)
}

/// Converts self into [`external::TufRepoMeta`].
pub fn into_external(self) -> external::TufRepoMeta {
external::TufRepoMeta {
hash: self.sha256.into(),
Expand All @@ -156,6 +150,17 @@ impl TufRepo {
}
}

impl From<TufRepo> for views::TufRepo {
fn from(repo: TufRepo) -> views::TufRepo {
views::TufRepo {
hash: repo.sha256.into(),
system_version: repo.system_version.into(),
file_name: repo.file_name,
time_created: repo.time_created,
}
}
}

#[derive(Queryable, Insertable, Clone, Debug, Selectable, AsChangeset)]
#[diesel(table_name = tuf_artifact)]
pub struct TufArtifact {
Expand Down Expand Up @@ -413,3 +418,24 @@ impl FromSql<Jsonb, diesel::pg::Pg> for DbTufSignedRootRole {
.map_err(|e| e.into())
}
}

// The following isn't a real model in the sense that it represents DB data,
// but it is the return type of a datastore function. The main reason we can't
// just use the view for this like we do with TufRepoUploadStatus is that
// TufRepoDescription has a bit more info in it that we rely on in code outside
// of the external API, like tests and internal APIs

/// The return value of the tuf repo insert function
pub struct TufRepoUpload {
pub recorded: TufRepoDescription,
pub status: TufRepoUploadStatus,
}

impl From<TufRepoUpload> for views::TufRepoUpload {
fn from(upload: TufRepoUpload) -> Self {
views::TufRepoUpload {
repo: upload.recorded.repo.into(),
status: upload.status,
}
}
}
2 changes: 1 addition & 1 deletion nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ mod switch_port;
mod target_release;
#[cfg(test)]
pub(crate) mod test_utils;
mod update;
pub mod update;
mod user_data_export;
mod utilization;
mod v2p_mapping;
Expand Down
Loading
Loading