Skip to content

Commit f1756cc

Browse files
authored
[nexus] Implement on-boot DB Schema Migration (#3760)
This PR does the following: - Adds a `schema/crdb` directory, containing `dbinit.sql`, `dbwipe.sql`, and a collection of versions. - When new DB versions are added, we can update `dbinit.sql` (as we have been doing) but also add a `up.sql` file to a directory named with the new DB version. - On boot, nexus acquires a lease for the ability to perform schema changes, and it executes them. Nexus refuses to boot until the on-disk schema version matches the version embedded in the Nexus binary. Furthermore, we provide an example "schema migration" from version 1.0.0 to version 1.0.1. Some implementation details: - This PR adds several tests to confirm this behavior works as expected, across all current and future updates - This PR makes `db_metadata` more strongly-typed, so make it easier to access the underlying data - This PR modified `dbinit.sql` to be idempotent, to be in-line with the recommended pattern for all subsequent schema migrations too - We embed the schema changes into the Nexus binary, so they can be parsed and selectively applied. Along these lines, it might make sense to *stop* embedding a schema into the CRDB zone, and leave the initialization entirely under the control of Nexus.
1 parent bf20123 commit f1756cc

File tree

32 files changed

+4248
-295
lines changed

32 files changed

+4248
-295
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api/internal/nexus.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ pub struct UpdateArtifactId {
118118
// must be left as a `todo!()` for now; `types::UpdateArtifactKind` will not
119119
// be updated with the new variant until step 5 below.
120120
//
121-
// 3. Add it to <repo root>/common/src/sql/dbinit.sql under (CREATE TYPE
121+
// 3. Add it to the sql database schema under (CREATE TYPE
122122
// omicron.public.update_artifact_kind).
123123
//
124124
// TODO: After omicron ships this would likely involve a DB migration.

common/src/nexus_config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ pub struct UpdatesConfig {
204204
pub default_base_url: String,
205205
}
206206

207+
/// Options to tweak database schema changes.
208+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
209+
pub struct SchemaConfig {
210+
pub schema_dir: PathBuf,
211+
}
212+
207213
/// Optional configuration for the timeseries database.
208214
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
209215
pub struct TimeseriesDbConfig {
@@ -355,6 +361,9 @@ pub struct PackageConfig {
355361
/// this is unconfigured.
356362
#[serde(default)]
357363
pub updates: Option<UpdatesConfig>,
364+
/// Describes how to handle and perform schema changes.
365+
#[serde(default)]
366+
pub schema: Option<SchemaConfig>,
358367
/// Tunable configuration for testing and experimentation
359368
#[serde(default)]
360369
pub tunables: Tunables,
@@ -642,6 +651,7 @@ mod test {
642651
trusted_root: PathBuf::from("/path/to/root.json"),
643652
default_base_url: "http://example.invalid/".into(),
644653
}),
654+
schema: None,
645655
tunables: Tunables { max_vpc_ipv4_subnet_prefix: 27 },
646656
dendrite: HashMap::from([(
647657
SwitchLocation::Switch0,

dev-tools/tests/test_omicron_dev.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,7 @@ async fn test_db_run() {
303303

304304
anyhow::ensure!(has_omicron_schema(&client).await);
305305

306-
// Now run db-populate. It should fail because the database is already
307-
// populated.
306+
// Now run db-populate.
308307
eprintln!("running db-populate");
309308
let populate_result = Exec::cmd(&cmd_path)
310309
.arg("db-populate")
@@ -317,13 +316,6 @@ async fn test_db_run() {
317316
eprintln!("exit status: {:?}", populate_result.exit_status);
318317
eprintln!("stdout: {:?}", populate_result.stdout_str());
319318
eprintln!("stdout: {:?}", populate_result.stderr_str());
320-
anyhow::ensure!(matches!(
321-
populate_result.exit_status,
322-
ExitStatus::Exited(1)
323-
));
324-
anyhow::ensure!(populate_result
325-
.stderr_str()
326-
.contains("database \"omicron\" already exists"),);
327319
anyhow::ensure!(has_omicron_schema(&client).await);
328320

329321
// Try again, but with the --wipe flag.

docs/adding-an-endpoint.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ this document should act as a jumping-off point.
3838
* If your application logic involes any multi-step operations which would be interrupted by Nexus stopping mid-execution (due to reboot, crash, failure, etc), it is recommended to use a https://github.com/oxidecomputer/omicron/tree/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/src/app/sagas[saga] to define the operations durably.
3939

4040
=== **Database**
41-
* `CREATE TABLE` for the resource in xref:../common/src/sql/dbinit.sql[dbinit.sql] (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/common/src/sql/dbinit.sql#L1103-L1129[Example])
41+
* `CREATE TABLE` for the resource in xref:../schema/crdb/dbinit.sql[dbinit.sql] (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/common/src/sql/dbinit.sql#L1103-L1129[Example])
4242
* Add an equivalent schema for the resource in xref:../nexus/db-model/src/schema.rs[schema.rs], which allows https://docs.diesel.rs/master/diesel/index.html[Diesel] to translate raw SQL to rust queries (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/db-model/src/schema.rs#L144-L155[Example])
4343
* Add a Rust representation of the database object to xref:../nexus/db-model/src[the DB model] (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/db-model/src/ip_pool.rs#L24-L40[Example])
4444
** Remember to make the object visible; this is usually defined in xref:../nexus/db-model/src/lib.rs[lib.rs] (https://github.com/oxidecomputer/omicron/blob/1dfe47c1b3122bc4f32a9c517cb31b1600581ea2/nexus/db-model/src/lib.rs#L102[Example]).

nexus/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ serde_urlencoded.workspace = true
7070
serde_with.workspace = true
7171
sled-agent-client.workspace = true
7272
slog.workspace = true
73+
slog-async.workspace = true
7374
slog-dtrace.workspace = true
75+
slog-term.workspace = true
7476
steno.workspace = true
7577
tempfile.workspace = true
7678
thiserror.workspace = true
@@ -112,6 +114,7 @@ openapiv3.workspace = true
112114
oxide-client.workspace = true
113115
pem.workspace = true
114116
petgraph.workspace = true
117+
pretty_assertions.workspace = true
115118
rcgen.workspace = true
116119
regex.workspace = true
117120
rustls = { workspace = true }

nexus/db-model/src/db_metadata.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,33 @@
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

55
use crate::schema::db_metadata;
6+
use crate::SemverVersion;
7+
use chrono::{DateTime, Utc};
8+
use serde::{Deserialize, Serialize};
69

710
/// Internal database metadata
8-
#[derive(Queryable, Insertable, Debug, Clone, Selectable)]
11+
#[derive(
12+
Queryable, Insertable, Debug, Clone, Selectable, Serialize, Deserialize,
13+
)]
914
#[diesel(table_name = db_metadata)]
1015
pub struct DbMetadata {
11-
name: String,
12-
value: String,
16+
singleton: bool,
17+
time_created: DateTime<Utc>,
18+
time_modified: DateTime<Utc>,
19+
version: SemverVersion,
20+
target_version: Option<SemverVersion>,
1321
}
1422

1523
impl DbMetadata {
16-
pub fn new(name: String, value: String) -> Self {
17-
Self { name, value }
24+
pub fn time_created(&self) -> &DateTime<Utc> {
25+
&self.time_created
1826
}
1927

20-
pub fn name(&self) -> &str {
21-
&self.name
28+
pub fn time_modified(&self) -> &DateTime<Utc> {
29+
&self.time_modified
2230
}
2331

24-
pub fn value(&self) -> &str {
25-
&self.value
32+
pub fn version(&self) -> &SemverVersion {
33+
&self.version
2634
}
2735
}

nexus/db-model/src/instance.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ pub struct InstanceRuntimeState {
174174
// TODO-cleanup: Different type?
175175
#[diesel(column_name = hostname)]
176176
pub hostname: String,
177+
#[diesel(column_name = boot_on_fault)]
178+
pub boot_on_fault: bool,
177179
}
178180

179181
impl From<InstanceRuntimeState>
@@ -225,6 +227,7 @@ impl From<internal::nexus::InstanceRuntimeState> for InstanceRuntimeState {
225227
hostname: state.hostname,
226228
gen: state.gen.into(),
227229
time_updated: state.time_updated,
230+
boot_on_fault: false,
228231
}
229232
}
230233
}

nexus/db-model/src/schema.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ table! {
356356
ncpus -> Int8,
357357
memory -> Int8,
358358
hostname -> Text,
359+
boot_on_fault -> Bool,
359360
}
360361
}
361362

@@ -1114,15 +1115,21 @@ table! {
11141115
}
11151116

11161117
table! {
1117-
db_metadata (name) {
1118-
name -> Text,
1119-
value -> Text,
1118+
db_metadata (singleton) {
1119+
singleton -> Bool,
1120+
time_created -> Timestamptz,
1121+
time_modified -> Timestamptz,
1122+
version -> Text,
1123+
target_version -> Nullable<Text>,
11201124
}
11211125
}
11221126

11231127
/// The version of the database schema this particular version of Nexus was
11241128
/// built against.
1125-
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(1, 0, 0);
1129+
///
1130+
/// This should be updated whenever the schema is changed. For more details,
1131+
/// refer to: schema/crdb/README.adoc
1132+
pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(2, 0, 0);
11261133

11271134
allow_tables_to_appear_in_same_query!(
11281135
system_update,

0 commit comments

Comments
 (0)