Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nexus: split out control plane zones as separate artifacts #7452

Merged
merged 9 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
18 changes: 18 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"api_identity",
"bootstore",
"brand-metadata",
"certificates",
"clickhouse-admin",
"clickhouse-admin/api",
Expand Down Expand Up @@ -135,6 +136,7 @@ members = [
default-members = [
"api_identity",
"bootstore",
"brand-metadata",
"certificates",
"clickhouse-admin",
"clickhouse-admin/api",
Expand Down Expand Up @@ -493,6 +495,7 @@ nexus-types = { path = "nexus/types" }
nom = "7.1.3"
num-integer = "0.1.46"
num = { version = "0.4.3", default-features = false, features = [ "libm" ] }
omicron-brand-metadata = { path = "brand-metadata" }
omicron-clickhouse-admin = { path = "clickhouse-admin" }
omicron-certificates = { path = "certificates" }
omicron-cockroach-admin = { path = "cockroach-admin" }
Expand Down
15 changes: 15 additions & 0 deletions brand-metadata/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "omicron-brand-metadata"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[dependencies]
omicron-workspace-hack.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
tar.workspace = true

[lints]
workspace = true
151 changes: 151 additions & 0 deletions brand-metadata/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Handling of `oxide.json` metadata files in tarballs.
//!
//! `oxide.json` is originally defined by the omicron1(7) zone brand, which
//! lives at <https://github.com/oxidecomputer/helios-omicron-brand>. tufaceous
//! extended this format with additional archive types for identifying other
//! types of tarballs; this crate covers those extensions so they can be used
//! across the Omicron codebase.

use std::io::{Error, ErrorKind, Read, Result, Write};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Metadata {
v: String,

// helios-build-utils defines a top-level `i` field for extra information,
// but omicron-package doesn't use this for the package name and version.
// We can also benefit from having rich types for these extra fields, so
// any additional top-level fields (including `i`) that exist for a given
// archive type should be deserialized as part of `ArchiveType`.
#[serde(flatten)]
t: ArchiveType,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case", tag = "t")]
pub enum ArchiveType {
// Originally defined in helios-build-utils (part of helios-omicron-brand):
Baseline,
Layer(LayerInfo),
Os,

// tufaceous extensions:
Rot,
ControlPlane,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LayerInfo {
pub pkg: String,
pub version: semver::Version,
}

impl Metadata {
pub fn new(archive_type: ArchiveType) -> Metadata {
Metadata { v: "1".into(), t: archive_type }
}

pub fn append_to_tar<T: Write>(
&self,
a: &mut tar::Builder<T>,
mtime: u64,
) -> Result<()> {
let mut b = serde_json::to_vec(self)?;
b.push(b'\n');

let mut h = tar::Header::new_ustar();
h.set_entry_type(tar::EntryType::Regular);
h.set_username("root")?;
h.set_uid(0);
h.set_groupname("root")?;
h.set_gid(0);
h.set_path("oxide.json")?;
h.set_mode(0o444);
h.set_size(b.len().try_into().unwrap());
h.set_mtime(mtime);
h.set_cksum();

a.append(&h, b.as_slice())?;
Ok(())
}

/// Read `Metadata` from a tar archive.
///
/// `oxide.json` is generally the first file in the archive, so this should
/// be a just-opened archive with no entries already read.
pub fn read_from_tar<T: Read>(a: &mut tar::Archive<T>) -> Result<Metadata> {
for entry in a.entries()? {
let mut entry = entry?;
if entry.path()? == std::path::Path::new("oxide.json") {
return Ok(serde_json::from_reader(&mut entry)?);
}
}
Err(Error::new(ErrorKind::InvalidData, "oxide.json is not present"))
}

pub fn archive_type(&self) -> &ArchiveType {
&self.t
}

pub fn is_layer(&self) -> bool {
matches!(&self.t, ArchiveType::Layer(_))
}

pub fn layer_info(&self) -> Result<&LayerInfo> {
match &self.t {
ArchiveType::Layer(info) => Ok(info),
_ => Err(Error::new(
ErrorKind::InvalidData,
"archive is not the \"layer\" type",
)),
}
}

pub fn is_baseline(&self) -> bool {
matches!(&self.t, ArchiveType::Baseline)
}

pub fn is_os(&self) -> bool {
matches!(&self.t, ArchiveType::Os)
}

pub fn is_rot(&self) -> bool {
matches!(&self.t, ArchiveType::Rot)
}

pub fn is_control_plane(&self) -> bool {
matches!(&self.t, ArchiveType::ControlPlane)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_deserialize() {
let metadata: Metadata = serde_json::from_str(
r#"{"v":"1","t":"layer","pkg":"nexus","version":"12.0.0-0.ci+git3a2ed5e97b3"}"#,
)
.unwrap();
assert!(metadata.is_layer());
let info = metadata.layer_info().unwrap();
assert_eq!(info.pkg, "nexus");
assert_eq!(info.version, "12.0.0-0.ci+git3a2ed5e97b3".parse().unwrap());

let metadata: Metadata = serde_json::from_str(
r#"{"v":"1","t":"os","i":{"checksum":"42eda100ee0e3bf44b9d0bb6a836046fa3133c378cd9d3a4ba338c3ba9e56eb7","name":"ci 3a2ed5e/9d37813 2024-12-20 08:54"}}"#,
).unwrap();
assert!(metadata.is_os());

let metadata: Metadata =
serde_json::from_str(r#"{"v":"1","t":"control_plane"}"#).unwrap();
assert!(metadata.is_control_plane());
}
}
3 changes: 3 additions & 0 deletions common/src/api/internal/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,10 @@ pub enum KnownArtifactKind {
GimletRotBootloader,
Host,
Trampoline,
/// Composite artifact of all control plane zones
ControlPlane,
/// Individual control plane zone
Zone,

// PSC Artifacts
PscSp,
Expand Down
2 changes: 1 addition & 1 deletion nexus/src/app/background/tasks/tuf_artifact_replication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ enum ArtifactHandle {
}

impl ArtifactHandle {
async fn file(&self) -> anyhow::Result<tokio::fs::File> {
async fn file(&self) -> std::io::Result<tokio::fs::File> {
match self {
ArtifactHandle::Extracted(handle) => handle.file().await,
#[cfg(test)]
Expand Down
15 changes: 10 additions & 5 deletions nexus/src/app/update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use nexus_db_queries::context::OpContext;
use omicron_common::api::external::{
Error, SemverVersion, TufRepoInsertResponse, TufRepoInsertStatus,
};
use update_common::artifacts::ArtifactsWithPlan;
use update_common::artifacts::{ArtifactsWithPlan, ControlPlaneZonesMode};

mod common_sp_update;
mod host_phase1_updater;
Expand Down Expand Up @@ -51,10 +51,15 @@ impl super::Nexus {
Error::internal_error("updates system not initialized")
})?;

let artifacts_with_plan =
ArtifactsWithPlan::from_stream(body, Some(file_name), &self.log)
.await
.map_err(|error| error.to_http_error())?;
let artifacts_with_plan = ArtifactsWithPlan::from_stream(
body,
Some(file_name),
ControlPlaneZonesMode::Split,
&self.log,
)
.await
.map_err(|error| error.to_http_error())?;

// Now store the artifacts in the database.
let tuf_repo_description = TufRepoDescription::from_external(
artifacts_with_plan.description().clone(),
Expand Down
21 changes: 21 additions & 0 deletions nexus/tests/integration_tests/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ async fn test_repo_upload() -> Result<()> {
.map(|artifact| artifact.hash)
.collect::<HashSet<_>>()
.len();
// The repository description should have `Zone` artifacts instead of the
// composite `ControlPlane` artifact.
assert_eq!(
initial_description
.artifacts
.iter()
.filter_map(|artifact| {
if artifact.id.kind == KnownArtifactKind::Zone.into() {
Some(&artifact.id.name)
} else {
None
}
})
.collect::<Vec<_>>(),
["zone1", "zone2"]
);
assert!(!initial_description
.artifacts
.iter()
.any(|artifact| artifact.id.kind
== KnownArtifactKind::ControlPlane.into()));

// The artifact replication background task should have been activated, and
// we should see a local repo and successful PUTs.
Expand Down
1 change: 1 addition & 0 deletions sled-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ omicron-workspace-hack.workspace = true
slog-error-chain.workspace = true
walkdir.workspace = true
zip.workspace = true
omicron-brand-metadata.workspace = true

[target.'cfg(target_os = "illumos")'.dependencies]
opte-ioctl.workspace = true
Expand Down
Loading
Loading