Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -3426,6 +3426,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 @@ -3500,40 +3504,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
58 changes: 52 additions & 6 deletions nexus/db-model/src/tuf_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,44 @@ impl FromSql<Jsonb, diesel::pg::Pg> for DbTufSignedRootRole {
.map_err(|e| e.into())
}
}

// The following aren't real models in the sense that they represent DB data,
// but they are the return types of datastore functions

/// Status of a TUF repo import
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TufRepoUploadStatus {
/// The repository already existed in the database
AlreadyExists,

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

impl From<TufRepoUploadStatus> for views::TufRepoUploadStatus {
fn from(status: TufRepoUploadStatus) -> Self {
match status {
TufRepoUploadStatus::AlreadyExists => {
views::TufRepoUploadStatus::AlreadyExists
}
TufRepoUploadStatus::Inserted => {
views::TufRepoUploadStatus::Inserted
}
}
}
}

/// 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.into(),
}
}
}
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