Skip to content

Commit d4a64cd

Browse files
authored
[2/n] store blueprint zone image source in database (#7849)
As discussed in [RFD 554](https://rfd.shared.oxide.computer/rfd/554), we only store zone image artifact hashes in the database, and look up versions via a left join (so it is allowed to fail). This lookup requires the kind + hash to be unique, so we add a database and app-level constraint for that, mirroring the constraint implemented in oxidecomputer/tufaceous#16. Also change `tuf_artifact`'s `version` field to 64 bytes per [this discussion](#7832 (comment)).
1 parent 9163dbf commit d4a64cd

File tree

19 files changed

+572
-58
lines changed

19 files changed

+572
-58
lines changed

clients/nexus-client/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ progenitor::generate_api!(
3131
Blueprint = nexus_types::deployment::Blueprint,
3232
BlueprintPhysicalDiskConfig = nexus_types::deployment::BlueprintPhysicalDiskConfig,
3333
BlueprintPhysicalDiskDisposition = nexus_types::deployment::BlueprintPhysicalDiskDisposition,
34+
BlueprintZoneImageSource = nexus_types::deployment::BlueprintZoneImageSource,
3435
Certificate = omicron_common::api::internal::nexus::Certificate,
3536
ClickhouseMode = nexus_types::deployment::ClickhouseMode,
3637
ClickhousePolicy = nexus_types::deployment::ClickhousePolicy,

dev-tools/reconfigurator-cli/src/main.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use nexus_reconfigurator_simulation::SimStateBuilder;
2626
use nexus_reconfigurator_simulation::Simulator;
2727
use nexus_types::deployment::BlueprintZoneDisposition;
2828
use nexus_types::deployment::BlueprintZoneImageSource;
29+
use nexus_types::deployment::BlueprintZoneImageVersion;
2930
use nexus_types::deployment::OmicronZoneNic;
3031
use nexus_types::deployment::PlanningInput;
3132
use nexus_types::deployment::SledFilter;
@@ -53,6 +54,7 @@ use swrite::{SWrite, swriteln};
5354
use tabled::Tabled;
5455
use tufaceous_artifact::ArtifactHash;
5556
use tufaceous_artifact::ArtifactVersion;
57+
use tufaceous_artifact::ArtifactVersionError;
5658

5759
mod log_capture;
5860

@@ -464,7 +466,11 @@ enum ImageSourceArgs {
464466
/// the zone image comes from the `install` dataset
465467
InstallDataset,
466468
/// the zone image comes from a specific TUF repo artifact
467-
Artifact { version: ArtifactVersion, hash: ArtifactHash },
469+
Artifact {
470+
#[clap(value_parser = parse_blueprint_zone_image_version)]
471+
version: BlueprintZoneImageVersion,
472+
hash: ArtifactHash,
473+
},
468474
}
469475

470476
impl From<ImageSourceArgs> for BlueprintZoneImageSource {
@@ -480,6 +486,19 @@ impl From<ImageSourceArgs> for BlueprintZoneImageSource {
480486
}
481487
}
482488

489+
fn parse_blueprint_zone_image_version(
490+
version: &str,
491+
) -> Result<BlueprintZoneImageVersion, ArtifactVersionError> {
492+
// Treat the literal string "unknown" as an unknown version.
493+
if version == "unknown" {
494+
return Ok(BlueprintZoneImageVersion::Unknown);
495+
}
496+
497+
Ok(BlueprintZoneImageVersion::Available {
498+
version: version.parse::<ArtifactVersion>()?,
499+
})
500+
}
501+
483502
#[derive(Debug, Args)]
484503
struct BlueprintArgs {
485504
/// id of the blueprint

nexus/db-model/src/deployment.rs

+111-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use crate::inventory::ZoneType;
99
use crate::omicron_zone_config::{self, OmicronZoneNic};
1010
use crate::typed_uuid::DbTypedUuid;
1111
use crate::{
12-
ByteCount, Generation, MacAddr, Name, SledState, SqlU8, SqlU16, SqlU32,
13-
impl_enum_type, ipv6,
12+
ArtifactHash, ByteCount, Generation, MacAddr, Name, SledState, SqlU8,
13+
SqlU16, SqlU32, TufArtifact, impl_enum_type, ipv6,
1414
};
1515
use anyhow::{Context, Result, anyhow, bail};
1616
use chrono::{DateTime, Utc};
@@ -24,7 +24,6 @@ use nexus_db_schema::schema::{
2424
bp_sled_metadata, bp_target,
2525
};
2626
use nexus_sled_agent_shared::inventory::OmicronZoneDataset;
27-
use nexus_types::deployment::BlueprintDatasetConfig;
2827
use nexus_types::deployment::BlueprintDatasetDisposition;
2928
use nexus_types::deployment::BlueprintPhysicalDiskConfig;
3029
use nexus_types::deployment::BlueprintPhysicalDiskDisposition;
@@ -34,6 +33,9 @@ use nexus_types::deployment::BlueprintZoneDisposition;
3433
use nexus_types::deployment::BlueprintZoneType;
3534
use nexus_types::deployment::ClickhouseClusterConfig;
3635
use nexus_types::deployment::CockroachDbPreserveDowngrade;
36+
use nexus_types::deployment::{
37+
BlueprintDatasetConfig, BlueprintZoneImageVersion,
38+
};
3739
use nexus_types::deployment::{BlueprintZoneImageSource, blueprint_zone_type};
3840
use nexus_types::deployment::{
3941
OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp,
@@ -449,6 +451,9 @@ pub struct BpOmicronZone {
449451

450452
pub external_ip_id: Option<DbTypedUuid<ExternalIpKind>>,
451453
pub filesystem_pool: DbTypedUuid<ZpoolKind>,
454+
455+
pub image_source: DbBpZoneImageSource,
456+
pub image_artifact_sha256: Option<ArtifactHash>,
452457
}
453458

454459
impl BpOmicronZone {
@@ -468,6 +473,9 @@ impl BpOmicronZone {
468473
expunged_ready_for_cleanup: disposition_expunged_ready_for_cleanup,
469474
} = blueprint_zone.disposition.into();
470475

476+
let DbBpZoneImageSourceColumns { image_source, image_artifact_data } =
477+
blueprint_zone.image_source.clone().into();
478+
471479
// Create a dummy record to start, then fill in the rest
472480
let mut bp_omicron_zone = BpOmicronZone {
473481
// Fill in the known fields that don't require inspecting
@@ -481,6 +489,11 @@ impl BpOmicronZone {
481489
disposition_expunged_as_of_generation,
482490
disposition_expunged_ready_for_cleanup,
483491
zone_type: blueprint_zone.zone_type.kind().into(),
492+
image_source,
493+
// The version is not preserved here -- instead, it is looked up
494+
// from the tuf_artifact table.
495+
image_artifact_sha256: image_artifact_data
496+
.map(|(_version, hash)| hash),
484497

485498
// Set the remainder of the fields to a default
486499
primary_service_ip: "::1"
@@ -685,6 +698,7 @@ impl BpOmicronZone {
685698
pub fn into_blueprint_zone_config(
686699
self,
687700
nic_row: Option<BpOmicronZoneNic>,
701+
image_artifact_row: Option<TufArtifact>,
688702
) -> anyhow::Result<BlueprintZoneConfig> {
689703
// Build up a set of common fields for our `BlueprintZoneType`s
690704
//
@@ -871,14 +885,20 @@ impl BpOmicronZone {
871885
.disposition_expunged_ready_for_cleanup,
872886
};
873887

888+
let image_source_cols = DbBpZoneImageSourceColumns::new(
889+
self.image_source,
890+
self.image_artifact_sha256,
891+
image_artifact_row,
892+
);
893+
874894
Ok(BlueprintZoneConfig {
875895
disposition: disposition_cols.try_into()?,
876896
id: self.id.into(),
877897
filesystem_pool: ZpoolName::new_external(
878898
self.filesystem_pool.into(),
879899
),
880900
zone_type,
881-
image_source: BlueprintZoneImageSource::InstallDataset,
901+
image_source: image_source_cols.try_into()?,
882902
})
883903
}
884904
}
@@ -958,6 +978,93 @@ impl TryFrom<DbBpZoneDispositionColumns> for BlueprintZoneDisposition {
958978
}
959979
}
960980

981+
impl_enum_type!(
982+
BpZoneImageSourceEnum:
983+
984+
#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)]
985+
pub enum DbBpZoneImageSource;
986+
987+
// Enum values
988+
InstallDataset => b"install_dataset"
989+
Artifact => b"artifact"
990+
);
991+
992+
struct DbBpZoneImageSourceColumns {
993+
image_source: DbBpZoneImageSource,
994+
// image_artifact_data is Some if and only if image_source is Artifact.
995+
//
996+
// The BlueprintZoneImageVersion is not actually stored in bp_omicron_zone
997+
// table directly, but is instead looked up from the tuf_artifact table at
998+
// blueprint load time.
999+
image_artifact_data: Option<(BlueprintZoneImageVersion, ArtifactHash)>,
1000+
}
1001+
1002+
impl DbBpZoneImageSourceColumns {
1003+
fn new(
1004+
image_source: DbBpZoneImageSource,
1005+
image_artifact_sha256: Option<ArtifactHash>,
1006+
image_artifact_row: Option<TufArtifact>,
1007+
) -> Self {
1008+
// Note that artifact_row can only be Some if image_artifact_sha256 is
1009+
// Some.
1010+
let image_artifact_data = image_artifact_sha256.map(|hash| {
1011+
let version = match image_artifact_row {
1012+
Some(artifact_row) => BlueprintZoneImageVersion::Available {
1013+
version: artifact_row.version.0,
1014+
},
1015+
None => BlueprintZoneImageVersion::Unknown,
1016+
};
1017+
(version, hash)
1018+
});
1019+
Self { image_source, image_artifact_data }
1020+
}
1021+
}
1022+
1023+
impl From<BlueprintZoneImageSource> for DbBpZoneImageSourceColumns {
1024+
fn from(image_source: BlueprintZoneImageSource) -> Self {
1025+
match image_source {
1026+
BlueprintZoneImageSource::InstallDataset => Self {
1027+
image_source: DbBpZoneImageSource::InstallDataset,
1028+
image_artifact_data: None,
1029+
},
1030+
BlueprintZoneImageSource::Artifact { version, hash } => Self {
1031+
image_source: DbBpZoneImageSource::Artifact,
1032+
image_artifact_data: Some((version, hash.into())),
1033+
},
1034+
}
1035+
}
1036+
}
1037+
1038+
impl TryFrom<DbBpZoneImageSourceColumns> for BlueprintZoneImageSource {
1039+
type Error = anyhow::Error;
1040+
1041+
fn try_from(
1042+
value: DbBpZoneImageSourceColumns,
1043+
) -> Result<Self, Self::Error> {
1044+
match (value.image_source, value.image_artifact_data) {
1045+
(DbBpZoneImageSource::Artifact, Some((version, hash))) => {
1046+
Ok(Self::Artifact { version, hash: hash.into() })
1047+
}
1048+
(DbBpZoneImageSource::Artifact, None) => Err(anyhow!(
1049+
"illegal database state (CHECK constraint broken?!): \
1050+
image_source {:?}, image_artifact_data None",
1051+
value.image_source,
1052+
)),
1053+
(DbBpZoneImageSource::InstallDataset, data @ Some(_)) => {
1054+
Err(anyhow!(
1055+
"illegal database state (CHECK constraint broken?!): \
1056+
image_source {:?}, image_artifact_data {:?}",
1057+
value.image_source,
1058+
data,
1059+
))
1060+
}
1061+
(DbBpZoneImageSource::InstallDataset, None) => {
1062+
Ok(Self::InstallDataset)
1063+
}
1064+
}
1065+
}
1066+
}
1067+
9611068
#[derive(Queryable, Clone, Debug, Selectable, Insertable)]
9621069
#[diesel(table_name = bp_omicron_zone_nic)]
9631070
pub struct BpOmicronZoneNic {

nexus/db-model/src/schema_versions.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(134, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(135, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(135, "blueprint-zone-image-source"),
3132
KnownVersion::new(134, "crucible-agent-reservation-overhead"),
3233
KnownVersion::new(133, "delete-defunct-reservations"),
3334
KnownVersion::new(132, "bp-omicron-zone-filesystem-pool-not-null"),

nexus/db-model/src/tuf_repo.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,13 @@ pub struct TufRepoArtifact {
285285
Copy,
286286
Clone,
287287
Debug,
288+
Hash,
289+
PartialEq,
290+
Eq,
288291
AsExpression,
289292
FromSqlRow,
290293
Serialize,
291294
Deserialize,
292-
PartialEq,
293295
)]
294296
#[diesel(sql_type = Text)]
295297
#[serde(transparent)]

0 commit comments

Comments
 (0)