Skip to content

Commit

Permalink
System Update API (#2100)
Browse files Browse the repository at this point in the history
[RFD 348](https://rfd.shared.oxide.computer/rfd/0348) is still in
progress, but I find it helpful to do this in parallel.

--- 

This is a big PR, but fortunately it is very straightforward: endpoints
plus the service functions, models, and tables you'd expect to see
backing those endpoints. It's a good time to add "updateable" to your
computer's dictionary.

### Included here

- [x] Endpoints
  - [x] System version + status (updating or not + reason)
  - [x] Get tree of updateable components (each w/ version + status)
  - [x] List system updates
  - [x] Get system update 
  - [x] Get tree of component updates for top-level system update
- [x] Start update (noop, returns placeholder `SystemUpdateDeployment`)
  - [x] Stop update (noop)
- [x] Change existing refresh endpoint to match new
`/v1/system/update/...` convention
- [x] Nexus functions for all that except start and stop
  - [x] Basic tests that create the thing and then retrieve it
- [x] Tables
  - [x] System updates
  - [x] Component updates
  - [x] Join table between system updates and component updates
  - [x] Updateable components
  - [x] Update deployments
- Weird stuff
- [x] `version_sort` column on `system_update` and `component_update`
that lets us hack in sorting by version
- [x] Enforce maximum major/minor/patch version number because
`version_sort` relies on zero-padding
- [x] Make system update semver version the PK for the system updates
table and fetch by version in endpoints
- Ideally we'd be able to fetch by ID also, but there would been a lot
of boilerplate to make it work like `NameOrId` without comparable value
due to the latter's reuse in many places. Version is unique and
immutable, so this should be good enough to start.

### Conspicuously missing, to be done in followups

- `nexus` functions for updating updateable component rows (possibly
also system updates and component updates — not sure if they're supposed
to be modified)
- Actually do an update

Do a find for `TODO:` for an embarrassing number of additional bullets.

### Open questions

- What is a good length limit for version strings in the DB? There's no
official limit, but the docs suggest a number less than or equal to 255:
https://semver.org/#does-semver-have-a-size-limit-on-the-version-string
  - Where/how do we enforce the length limit besides the DB?
- How to represent system status, especially steady reason
  • Loading branch information
david-crespo authored Jan 30, 2023
1 parent b58e613 commit 35cf540
Show file tree
Hide file tree
Showing 32 changed files with 3,079 additions and 79 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ rustfmt-wrapper = "0.2"
rustls = "0.20.7"
samael = { git = "https://github.com/njaremko/samael", features = ["xmlsec"], branch = "master" }
schemars = "0.8.10"
semver = { version = "1.0.16", features = ["std", "serde"] }
serde = { version = "1.0", default-features = false, features = [ "derive" ] }
serde_derive = "1.0"
serde_json = "1.0.91"
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ rand.workspace = true
reqwest = { workspace = true, features = ["rustls-tls", "stream"] }
ring.workspace = true
schemars = { workspace = true, features = [ "chrono", "uuid1" ] }
semver.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
Expand Down
38 changes: 38 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use futures::stream::StreamExt;
use parse_display::Display;
use parse_display::FromStr;
use schemars::JsonSchema;
use semver;
use serde::Deserialize;
use serde::Serialize;
use serde_with::{DeserializeFromStr, SerializeDisplay};
Expand Down Expand Up @@ -373,6 +374,38 @@ impl JsonSchema for NameOrId {
}
}

// TODO: remove wrapper for semver::Version once this PR goes through
// https://github.com/GREsau/schemars/pull/195
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Display)]
#[display("{0}")]
pub struct SemverVersion(pub semver::Version);

impl SemverVersion {
pub fn new(major: u64, minor: u64, patch: u64) -> Self {
Self(semver::Version::new(major, minor, patch))
}
}

impl JsonSchema for SemverVersion {
fn schema_name() -> String {
"SemverVersion".to_string()
}

fn json_schema(
_: &mut schemars::gen::SchemaGenerator,
) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(r"^\d+\.\d+\.\d+([\-\+].+)?$".to_owned()),
..Default::default()
})),
..Default::default()
}
.into()
}
}

/// Name for a built-in role
#[derive(
Clone,
Expand Down Expand Up @@ -667,6 +700,11 @@ pub enum ResourceType {
MetricProducer,
RoleBuiltin,
UpdateAvailableArtifact,
SystemUpdate,
ComponentUpdate,
SystemUpdateComponentUpdate,
UpdateDeployment,
UpdateableComponent,
UserBuiltin,
Zpool,
}
Expand Down
146 changes: 146 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,152 @@ CREATE INDEX ON omicron.public.update_available_artifact (
targets_role_version
);

/*
* System updates
*/
CREATE TABLE omicron.public.system_update (
/* Identity metadata (asset) */
id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,

-- Because the version is unique, it could be the PK, but that would make
-- this resource different from every other resource for little benefit.

-- Unique semver version
version STRING(64) NOT NULL, -- TODO: length
-- version string with maj/min/patch 0-padded to be string sortable
version_sort STRING(64) NOT NULL -- TODO: length
);

CREATE UNIQUE INDEX ON omicron.public.system_update (
version
);

CREATE UNIQUE INDEX ON omicron.public.system_update (
version_sort
);

CREATE TYPE omicron.public.updateable_component_type AS ENUM (
'bootloader_for_rot',
'bootloader_for_sp',
'bootloader_for_host_proc',
'hubris_for_psc_rot',
'hubris_for_psc_sp',
'hubris_for_sidecar_rot',
'hubris_for_sidecar_sp',
'hubris_for_gimlet_rot',
'hubris_for_gimlet_sp',
'helios_host_phase_1',
'helios_host_phase_2',
'host_omicron'
);

/*
* Component updates. Associated with at least one system_update through
* system_update_component_update.
*/
CREATE TABLE omicron.public.component_update (
/* Identity metadata (asset) */
id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,

-- On component updates there's no device ID because the update can apply to
-- multiple instances of a given device kind

-- The *system* update version associated with this version (this is confusing, will rename)
version STRING(64) NOT NULL, -- TODO: length
-- TODO: add component update version to component_update

component_type omicron.public.updateable_component_type NOT NULL
);

-- version is unique per component type
CREATE UNIQUE INDEX ON omicron.public.component_update (
component_type, version
);

/*
* Associate system updates with component updates. Not done with a
* system_update_id field on component_update because the same component update
* may be part of more than one system update.
*/
CREATE TABLE omicron.public.system_update_component_update (
system_update_id UUID NOT NULL,
component_update_id UUID NOT NULL,

PRIMARY KEY (system_update_id, component_update_id)
);

-- For now, the plan is to treat stopped, failed, completed as sub-cases of
-- "steady" described by a "reason". But reason is not implemented yet.
-- Obviously this could be a boolean, but boolean status fields never stay
-- boolean for long.
CREATE TYPE omicron.public.update_status AS ENUM (
'updating',
'steady'
);

/*
* Updateable components and their update status
*/
CREATE TABLE omicron.public.updateable_component (
/* Identity metadata (asset) */
id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,

-- Free-form string that comes from the device
device_id STRING(40) NOT NULL,

component_type omicron.public.updateable_component_type NOT NULL,

-- The semver version of this component's own software
version STRING(64) NOT NULL, -- TODO: length

-- The version of the system update this component's software came from.
-- This may need to be nullable if we are registering components before we
-- know about system versions at all
system_version STRING(64) NOT NULL, -- TODO: length
-- version string with maj/min/patch 0-padded to be string sortable
system_version_sort STRING(64) NOT NULL, -- TODO: length

status omicron.public.update_status NOT NULL
-- TODO: status reason for updateable_component
);

-- can't have two components of the same type with the same device ID
CREATE UNIQUE INDEX ON omicron.public.updateable_component (
component_type, device_id
);

CREATE INDEX ON omicron.public.updateable_component (
system_version_sort
);

/*
* System updates
*/
CREATE TABLE omicron.public.update_deployment (
/* Identity metadata (asset) */
id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,

-- semver version of corresponding system update
-- TODO: this makes sense while version is the PK of system_update, but
-- if/when I change that back to ID, this needs to be the ID too
version STRING(64) NOT NULL,

status omicron.public.update_status NOT NULL
-- TODO: status reason for update_deployment
);

CREATE INDEX on omicron.public.update_deployment (
time_created
);

/*******************************************************************/

/*
Expand Down
1 change: 1 addition & 0 deletions nexus/db-model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ rand.workspace = true
ref-cast.workspace = true
thiserror.workspace = true
schemars = { workspace = true, features = ["chrono", "uuid1"] }
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
steno.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ mod organization;
mod oximeter_info;
mod producer_endpoint;
mod project;
mod semver_version;
mod system_update;
// These actually represent subqueries, not real table.
// However, they must be defined in the same crate as our tables
// for join-based marker trait generation.
Expand Down Expand Up @@ -119,6 +121,7 @@ pub use region::*;
pub use region_snapshot::*;
pub use role_assignment::*;
pub use role_builtin::*;
pub use semver_version::*;
pub use service::*;
pub use service_kind::*;
pub use silo::*;
Expand All @@ -128,6 +131,7 @@ pub use silo_user_password_hash::*;
pub use sled::*;
pub use snapshot::*;
pub use ssh_key::*;
pub use system_update::*;
pub use update_artifact::*;
pub use user_builtin::*;
pub use virtual_provisioning_collection::*;
Expand Down
64 changes: 64 additions & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,70 @@ table! {
}
}

table! {
system_update (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,

version -> Text,
version_sort -> Text,
}
}

table! {
update_deployment (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,

version -> Text,
status -> crate::UpdateStatusEnum,
// TODO: status reason for updateable_component
}
}

table! {
component_update (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,

version -> Text,
component_type -> crate::UpdateableComponentTypeEnum,
}
}

table! {
updateable_component (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,

device_id -> Text,
version -> Text,
system_version -> Text,
system_version_sort -> Text,
component_type -> crate::UpdateableComponentTypeEnum,
status -> crate::UpdateStatusEnum,
// TODO: status reason for updateable_component
}
}

table! {
system_update_component_update (system_update_id, component_update_id) {
system_update_id -> Uuid,
component_update_id -> Uuid,
}
}

allow_tables_to_appear_in_same_query!(
system_update,
component_update,
system_update_component_update,
);
joinable!(system_update_component_update -> component_update (component_update_id));

allow_tables_to_appear_in_same_query!(ip_pool_range, ip_pool);
joinable!(ip_pool_range -> ip_pool (ip_pool_id));

Expand Down
Loading

0 comments on commit 35cf540

Please sign in to comment.