diff --git a/Cargo.lock b/Cargo.lock index d8f8001246f..0d9194f8c85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2167,18 +2167,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" -[[package]] -name = "datatest-stable" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a560b3fd20463b56397bd457aa71243ccfdcffe696050b66e3b1e0ec0457e7f1" -dependencies = [ - "camino", - "fancy-regex", - "libtest-mimic", - "walkdir", -] - [[package]] name = "db-dev" version = "0.1.0" @@ -3061,12 +3049,6 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" -[[package]] -name = "escape8259" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" - [[package]] name = "event-listener" version = "2.5.3" @@ -3111,17 +3093,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" -[[package]] -name = "fancy-regex" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" -dependencies = [ - "bit-set 0.5.3", - "regex-automata", - "regex-syntax 0.8.5", -] - [[package]] name = "fastrand" version = "2.1.1" @@ -4726,6 +4697,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", + "tufaceous-artifact", "tufaceous-lib", "update-engine", "uuid", @@ -5275,18 +5247,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "libtest-mimic" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0bda45ed5b3a2904262c1bb91e526127aa70e7ef3758aba2ef93cf896b9b58" -dependencies = [ - "clap", - "escape8259", - "termcolor", - "threadpool", -] - [[package]] name = "libxml" version = "0.3.3" @@ -5879,6 +5839,7 @@ dependencies = [ "strum", "thiserror 1.0.69", "tokio", + "tufaceous-artifact", "uuid", ] @@ -6612,16 +6573,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "num_enum" version = "0.5.11" @@ -6710,17 +6661,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "omicron-brand-metadata" -version = "0.1.0" -dependencies = [ - "omicron-workspace-hack", - "semver 1.0.25", - "serde", - "serde_json", - "tar", -] - [[package]] name = "omicron-certificates" version = "0.1.0" @@ -6862,6 +6802,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "toml 0.8.20", + "tufaceous-artifact", "uuid", ] @@ -7162,6 +7103,7 @@ dependencies = [ "tokio-util", "tough", "tufaceous", + "tufaceous-artifact", "tufaceous-lib", "update-common", "update-engine", @@ -7316,7 +7258,6 @@ dependencies = [ "fs-err", "futures", "hex", - "omicron-common", "omicron-pins", "omicron-workspace-hack", "omicron-zone-package", @@ -7331,6 +7272,7 @@ dependencies = [ "tar", "tokio", "toml 0.8.20", + "tufaceous-artifact", "tufaceous-lib", ] @@ -7396,7 +7338,6 @@ dependencies = [ "nexus-reconfigurator-blippy", "nexus-sled-agent-shared", "nexus-types", - "omicron-brand-metadata", "omicron-common", "omicron-ddm-admin-client", "omicron-test-utils", @@ -7449,6 +7390,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.20", + "tufaceous-brand-metadata", "usdt", "uuid", "walkdir", @@ -7520,11 +7462,10 @@ dependencies = [ "base16ct", "base64 0.22.1", "base64ct", - "bit-set 0.5.3", - "bit-vec 0.6.3", "bitflags 1.3.2", "bitflags 2.6.0", "bstr", + "buf-list", "byteorder", "bytes", "cc", @@ -7648,6 +7589,7 @@ dependencies = [ "zerocopy 0.8.10", "zeroize", "zip 0.6.6", + "zip 2.1.3", ] [[package]] @@ -12043,15 +11985,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - [[package]] name = "time" version = "0.3.36" @@ -12594,33 +12527,54 @@ dependencies = [ [[package]] name = "tufaceous" version = "0.1.0" +source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010" dependencies = [ "anyhow", - "assert_cmd", "camino", "chrono", "clap", "console", - "datatest-stable", - "fs-err", "humantime", - "omicron-common", - "omicron-test-utils", - "omicron-workspace-hack", - "predicates", "semver 1.0.25", "slog", "slog-async", "slog-envlogger", "slog-term", - "tempfile", "tokio", + "tufaceous-artifact", "tufaceous-lib", ] +[[package]] +name = "tufaceous-artifact" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010" +dependencies = [ + "parse-display", + "proptest", + "schemars", + "semver 1.0.25", + "serde", + "serde_human_bytes", + "strum", + "test-strategy", +] + +[[package]] +name = "tufaceous-brand-metadata" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010" +dependencies = [ + "semver 1.0.25", + "serde", + "serde_json", + "tar", +] + [[package]] name = "tufaceous-lib" version = "0.1.0" +source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#d2387032714f66e31b7e255d89f9bf6eb9b3a010" dependencies = [ "anyhow", "async-trait", @@ -12637,11 +12591,7 @@ dependencies = [ "futures", "hex", "hubtools", - "itertools 0.14.0", - "omicron-brand-metadata", - "omicron-common", - "omicron-test-utils", - "omicron-workspace-hack", + "itertools 0.13.0", "parse-size", "rand 0.8.5", "semver 1.0.25", @@ -12654,6 +12604,8 @@ dependencies = [ "tokio", "toml 0.8.20", "tough", + "tufaceous-artifact", + "tufaceous-brand-metadata", "url", "zip 2.1.3", ] @@ -12991,7 +12943,6 @@ dependencies = [ "futures", "hex", "hubtools", - "omicron-brand-metadata", "omicron-common", "omicron-test-utils", "omicron-workspace-hack", @@ -13005,6 +12956,8 @@ dependencies = [ "tokio-util", "tough", "tufaceous", + "tufaceous-artifact", + "tufaceous-brand-metadata", "tufaceous-lib", ] @@ -13472,6 +13425,7 @@ dependencies = [ "toml 0.8.20", "toml_edit 0.22.24", "transceiver-controller", + "tufaceous-artifact", "tui-tree-widget", "unicode-width 0.1.14", "update-engine", @@ -13598,6 +13552,7 @@ dependencies = [ "tough", "transceiver-controller", "tufaceous", + "tufaceous-artifact", "tufaceous-lib", "update-common", "update-engine", diff --git a/Cargo.toml b/Cargo.toml index 09cafe05f73..1a673614790 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "api_identity", "bootstore", - "brand-metadata", "certificates", "clickhouse-admin", "clickhouse-admin/api", @@ -119,8 +118,6 @@ members = [ "sled-storage", "sp-sim", "test-utils", - "tufaceous-lib", - "tufaceous", "typed-rng", "update-common", "update-engine", @@ -137,7 +134,6 @@ members = [ default-members = [ "api_identity", "bootstore", - "brand-metadata", "certificates", "clickhouse-admin", "clickhouse-admin/api", @@ -257,8 +253,6 @@ default-members = [ "sled-storage", "sp-sim", "test-utils", - "tufaceous-lib", - "tufaceous", "typed-rng", "update-common", "update-engine", @@ -321,7 +315,6 @@ async-bb8-diesel = "0.2" async-trait = "0.1.86" atomicwrites = "0.4.4" authz-macros = { path = "nexus/authz-macros" } -aws-lc-rs = "1.12.4" backoff = { version = "0.4.0", features = [ "tokio" ] } base64 = "0.22.1" bcs = "0.1.6" @@ -370,7 +363,6 @@ crucible-common = { git = "https://github.com/oxidecomputer/crucible", rev = "81 csv = "1.3.1" curve25519-dalek = "4" daft = { version = "0.1.1", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] } -datatest-stable = "0.2.9" display-error-chain = "0.2.2" omicron-ddm-admin-client = { path = "clients/ddm-admin-client" } db-macros = { path = "nexus/db-macros" } @@ -498,7 +490,6 @@ nix = { version = "0.29", features = ["net"] } 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" } @@ -606,7 +597,6 @@ semver = { version = "1.0.25", features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = [ "derive", "rc" ] } serde_human_bytes = { git = "https://github.com/oxidecomputer/serde_human_bytes", branch = "main" } serde_json = "1.0.139" -serde_path_to_error = "0.1.16" serde_tokenstream = "0.2" serde_urlencoded = "0.7.1" serde_with = "3.12.0" @@ -676,8 +666,10 @@ toml_edit = "0.22.24" tough = { version = "0.19.0", features = [ "http" ] } transceiver-controller = { git = "https://github.com/oxidecomputer/transceiver-control", features = [ "api-traits" ] } trybuild = "1.0.103" -tufaceous = { path = "tufaceous" } -tufaceous-lib = { path = "tufaceous-lib" } +tufaceous = { git = "https://github.com/oxidecomputer/tufaceous", branch = "main" } +tufaceous-artifact = { git = "https://github.com/oxidecomputer/tufaceous", branch = "main", features = ["proptest", "schemars"] } +tufaceous-brand-metadata = { git = "https://github.com/oxidecomputer/tufaceous", branch = "main" } +tufaceous-lib = { git = "https://github.com/oxidecomputer/tufaceous", branch = "main" } tui-tree-widget = "0.23.0" typed-rng = { path = "typed-rng" } typify = "0.3.0" @@ -888,6 +880,12 @@ opt-level = 3 # propolis-client = { path = "../propolis/lib/propolis-client" } # propolis-mock-server = { path = "../propolis/bin/mock-server" } +# [patch."https://github.com/oxidecomputer/tufaceous"] +# tufaceous = { path = "../tufaceous/bin" } +# tufaceous-artifact = { path = "../tufaceous/artifact" } +# tufaceous-brand-metadata = { path = "../tufaceous/brand-metadata" } +# tufaceous-lib = { path = "../tufaceous/lib" } + # [patch."https://github.com/oxidecomputer/typify"] # typify = { path = "../typify/typify" } diff --git a/brand-metadata/Cargo.toml b/brand-metadata/Cargo.toml deleted file mode 100644 index 98462c695e2..00000000000 --- a/brand-metadata/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[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 diff --git a/brand-metadata/src/lib.rs b/brand-metadata/src/lib.rs deleted file mode 100644 index f25b120ac63..00000000000 --- a/brand-metadata/src/lib.rs +++ /dev/null @@ -1,151 +0,0 @@ -// 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 . 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( - &self, - a: &mut tar::Builder, - 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(a: &mut tar::Archive) -> Result { - 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()); - } -} diff --git a/common/Cargo.toml b/common/Cargo.toml index 99902e1cd75..0ed300f0bc1 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -50,6 +50,7 @@ parse-display.workspace = true progenitor-client.workspace = true omicron-workspace-hack.workspace = true regress.workspace = true +tufaceous-artifact.workspace = true [dev-dependencies] camino-tempfile.workspace = true diff --git a/common/src/api/internal/nexus.rs b/common/src/api/internal/nexus.rs index 14c9b2b8885..7d85c2aeab7 100644 --- a/common/src/api/internal/nexus.rs +++ b/common/src/api/internal/nexus.rs @@ -12,14 +12,11 @@ use omicron_uuid_kinds::DownstairsRegionKind; use omicron_uuid_kinds::TypedUuid; use omicron_uuid_kinds::UpstairsRepairKind; use omicron_uuid_kinds::UpstairsSessionKind; -use parse_display::{Display, FromStr}; use schemars::JsonSchema; -use semver::Version; use serde::{Deserialize, Serialize}; use std::fmt; use std::net::SocketAddr; use std::time::Duration; -use strum::{EnumIter, IntoEnumIterator}; use uuid::Uuid; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -241,112 +238,6 @@ pub struct ProducerRegistrationResponse { pub lease_duration: Duration, } -/// An identifier for a single update artifact. -#[derive( - Clone, - Debug, - Eq, - PartialEq, - Hash, - Ord, - PartialOrd, - Deserialize, - Serialize, - JsonSchema, -)] -pub struct UpdateArtifactId { - /// The artifact's name. - pub name: String, - - /// The artifact's version. - pub version: Version, - - /// The kind of update artifact this is. - pub kind: KnownArtifactKind, -} - -// Adding a new KnownArtifactKind -// =============================== -// -// To add a new kind of update artifact: -// -// 1. Add it here. -// 2. Regenerate OpenAPI documents with `cargo xtask openapi generate` -- this -// should work without any compile errors. -// 3. Run `cargo check --all-targets` to resolve compile errors. -// -// NOTE: KnownArtifactKind has to be in snake_case due to openapi-lint -// requirements. - -/// Kinds of update artifacts, as used by Nexus to determine what updates are available and by -/// sled-agent to determine how to apply an update when asked. -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Ord, - PartialOrd, - Display, - FromStr, - Deserialize, - Serialize, - JsonSchema, - EnumIter, -)] -#[display(style = "snake_case")] -#[serde(rename_all = "snake_case")] -pub enum KnownArtifactKind { - // Sled Artifacts - GimletSp, - GimletRot, - GimletRotBootloader, - Host, - Trampoline, - /// Composite artifact of all control plane zones - ControlPlane, - /// Individual control plane zone - Zone, - - // PSC Artifacts - PscSp, - PscRot, - PscRotBootloader, - - // Switch Artifacts - SwitchSp, - SwitchRot, - SwitchRotBootloader, -} - -impl KnownArtifactKind { - /// Returns an iterator over all the variants in this struct. - /// - /// This is provided as a helper so dependent packages don't have to pull in - /// strum explicitly. - pub fn iter() -> KnownArtifactKindIter { - ::iter() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn known_artifact_kind_roundtrip() { - for kind in KnownArtifactKind::iter() { - let as_string = kind.to_string(); - let kind2 = as_string.parse::().unwrap_or_else( - |error| panic!("error parsing kind {as_string}: {error}"), - ); - assert_eq!(kind, kind2); - } - } -} - /// A `HostIdentifier` represents either an IP host or network (v4 or v6), /// or an entire VPC (identified by its VNI). It is used in firewall rule /// host filters. diff --git a/common/src/update.rs b/common/src/update.rs index c712ae45019..0283239dbda 100644 --- a/common/src/update.rs +++ b/common/src/update.rs @@ -2,9 +2,8 @@ // 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/. -use std::{borrow::Cow, convert::Infallible, fmt, str::FromStr}; +use std::{fmt, str::FromStr}; -use crate::api::internal::nexus::KnownArtifactKind; use hex::FromHexError; use schemars::{ gen::SchemaGenerator, @@ -13,61 +12,7 @@ use schemars::{ }; use semver::Version; use serde::{Deserialize, Serialize}; - -/// Description of the `artifacts.json` target found in rack update -/// repositories. -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ArtifactsDocument { - pub system_version: Version, - pub artifacts: Vec, -} - -impl ArtifactsDocument { - /// Creates an artifacts document with the provided system version and an - /// empty list of artifacts. - pub fn empty(system_version: Version) -> Self { - Self { system_version, artifacts: Vec::new() } - } -} - -/// Describes an artifact available in the repository. -/// -/// See also [`crate::api::internal::nexus::UpdateArtifactId`], which is used -/// internally in Nexus and Sled Agent. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct Artifact { - /// Used to differentiate between different series of artifacts of the same - /// kind. This is used by the control plane to select the correct artifact. - /// - /// For SP and ROT images ([`KnownArtifactKind::GimletSp`], - /// [`KnownArtifactKind::GimletRot`], [`KnownArtifactKind::PscSp`], - /// [`KnownArtifactKind::PscRot`], [`KnownArtifactKind::SwitchSp`], - /// [`KnownArtifactKind::SwitchRot`]), `name` is the value of the board - /// (`BORD`) tag in the image caboose. - /// - /// In the future when [`KnownArtifactKind::ControlPlane`] is split up into - /// separate zones, `name` will be the zone name. - pub name: String, - pub version: Version, - pub kind: ArtifactKind, - pub target: String, -} - -impl Artifact { - /// Returns the artifact ID for this artifact. - pub fn id(&self) -> ArtifactId { - ArtifactId { - name: self.name.clone(), - version: self.version.clone(), - kind: self.kind.clone(), - } - } - - /// Returns the artifact ID for this artifact without clones. - pub fn into_id(self) -> ArtifactId { - ArtifactId { name: self.name, version: self.version, kind: self.kind } - } -} +use tufaceous_artifact::{Artifact, ArtifactKind}; /// An identifier for an artifact. /// @@ -103,6 +48,16 @@ impl fmt::Display for ArtifactId { } } +impl From for ArtifactId { + fn from(artifact: Artifact) -> Self { + ArtifactId { + name: artifact.name, + version: artifact.version, + kind: artifact.kind, + } + } +} + /// A hash-based identifier for an artifact. /// /// Some places, e.g. the installinator, request artifacts by hash rather than @@ -127,151 +82,6 @@ pub struct ArtifactHashId { pub hash: ArtifactHash, } -/// The kind of artifact we are dealing with. -/// -/// To ensure older versions of Nexus can work with update repositories that -/// describe artifact kinds it is not yet aware of, this is a newtype wrapper -/// around a string. The set of known artifact kinds is described in -/// [`KnownArtifactKind`], and this type has conversions to and from it. -#[derive( - Debug, - Clone, - PartialEq, - Eq, - Hash, - Ord, - PartialOrd, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(transparent)] -pub struct ArtifactKind(Cow<'static, str>); - -impl ArtifactKind { - /// Creates a new `ArtifactKind` from a string. - pub fn new(kind: String) -> Self { - Self(kind.into()) - } - - /// Creates a new `ArtifactKind` from a static string. - pub const fn from_static(kind: &'static str) -> Self { - Self(Cow::Borrowed(kind)) - } - - /// Creates a new `ArtifactKind` from a known kind. - pub fn from_known(kind: KnownArtifactKind) -> Self { - Self::new(kind.to_string()) - } - - /// Returns the kind as a string. - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Converts self to a `KnownArtifactKind`, if it is known. - pub fn to_known(&self) -> Option { - self.0.parse().ok() - } -} - -/// These artifact kinds are not stored anywhere, but are derived from stored -/// kinds and used as internal identifiers. -impl ArtifactKind { - /// Gimlet root of trust bootloader slot image identifier. - /// - /// Derived from [`KnownArtifactKind::GimletRotBootloader`]. - pub const GIMLET_ROT_STAGE0: Self = - Self::from_static("gimlet_rot_bootloader"); - - /// Gimlet root of trust A slot image identifier. - /// - /// Derived from [`KnownArtifactKind::GimletRot`]. - pub const GIMLET_ROT_IMAGE_A: Self = - Self::from_static("gimlet_rot_image_a"); - - /// Gimlet root of trust B slot image identifier. - /// - /// Derived from [`KnownArtifactKind::GimletRot`]. - pub const GIMLET_ROT_IMAGE_B: Self = - Self::from_static("gimlet_rot_image_b"); - - /// PSC root of trust stage0 image identifier. - /// - /// Derived from [`KnownArtifactKind::PscRotBootloader`]. - pub const PSC_ROT_STAGE0: Self = Self::from_static("psc_rot_bootloader"); - - /// PSC root of trust A slot image identifier. - /// - /// Derived from [`KnownArtifactKind::PscRot`]. - pub const PSC_ROT_IMAGE_A: Self = Self::from_static("psc_rot_image_a"); - - /// PSC root of trust B slot image identifier. - /// - /// Derived from [`KnownArtifactKind::PscRot`]. - pub const PSC_ROT_IMAGE_B: Self = Self::from_static("psc_rot_image_b"); - - /// Switch root of trust A slot image identifier. - /// - /// Derived from [`KnownArtifactKind::SwitchRotBootloader`]. - pub const SWITCH_ROT_STAGE0: Self = - Self::from_static("switch_rot_bootloader"); - - /// Switch root of trust A slot image identifier. - /// - /// Derived from [`KnownArtifactKind::SwitchRot`]. - pub const SWITCH_ROT_IMAGE_A: Self = - Self::from_static("switch_rot_image_a"); - - /// Switch root of trust B slot image identifier. - /// - /// Derived from [`KnownArtifactKind::SwitchRot`]. - pub const SWITCH_ROT_IMAGE_B: Self = - Self::from_static("switch_rot_image_b"); - - /// Host phase 1 identifier. - /// - /// Derived from [`KnownArtifactKind::Host`]. - pub const HOST_PHASE_1: Self = Self::from_static("host_phase_1"); - - /// Host phase 2 identifier. - /// - /// Derived from [`KnownArtifactKind::Host`]. - pub const HOST_PHASE_2: Self = Self::from_static("host_phase_2"); - - /// Trampoline phase 1 identifier. - /// - /// Derived from [`KnownArtifactKind::Trampoline`]. - pub const TRAMPOLINE_PHASE_1: Self = - Self::from_static("trampoline_phase_1"); - - /// Trampoline phase 2 identifier. - /// - /// Derived from [`KnownArtifactKind::Trampoline`]. - pub const TRAMPOLINE_PHASE_2: Self = - Self::from_static("trampoline_phase_2"); -} - -impl From for ArtifactKind { - fn from(kind: KnownArtifactKind) -> Self { - Self::from_known(kind) - } -} - -impl fmt::Display for ArtifactKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl FromStr for ArtifactKind { - type Err = Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self::new(s.to_owned())) - } -} - /// The hash of an artifact. #[derive( Copy, @@ -328,39 +138,3 @@ pub fn hex_schema(gen: &mut SchemaGenerator) -> Schema { schema.format = Some(format!("hex string ({N} bytes)")); schema.into() } - -#[cfg(test)] -mod tests { - use crate::api::internal::nexus::KnownArtifactKind; - use crate::update::ArtifactKind; - - #[test] - fn serde_artifact_kind() { - assert_eq!( - serde_json::from_str::("\"gimlet_sp\"") - .unwrap() - .to_known(), - Some(KnownArtifactKind::GimletSp) - ); - assert_eq!( - serde_json::from_str::("\"fhqwhgads\"") - .unwrap() - .to_known(), - None, - ); - assert!(serde_json::from_str::("null").is_err()); - - assert_eq!( - serde_json::to_string(&ArtifactKind::from_known( - KnownArtifactKind::GimletSp - )) - .unwrap(), - "\"gimlet_sp\"" - ); - assert_eq!( - serde_json::to_string(&ArtifactKind::new("fhqwhgads".to_string())) - .unwrap(), - "\"fhqwhgads\"" - ); - } -} diff --git a/dev-tools/releng/Cargo.toml b/dev-tools/releng/Cargo.toml index 69f7dae2d4e..78af644f4a8 100644 --- a/dev-tools/releng/Cargo.toml +++ b/dev-tools/releng/Cargo.toml @@ -14,7 +14,6 @@ clap.workspace = true fs-err = { workspace = true, features = ["tokio"] } futures.workspace = true hex.workspace = true -omicron-common.workspace = true omicron-pins.workspace = true omicron-workspace-hack.workspace = true omicron-zone-package.workspace = true @@ -29,6 +28,7 @@ slog-term.workspace = true tar.workspace = true tokio = { workspace = true, features = ["full"] } toml.workspace = true +tufaceous-artifact.workspace = true tufaceous-lib.workspace = true [lints] diff --git a/dev-tools/releng/src/hubris.rs b/dev-tools/releng/src/hubris.rs index 8ed3308d262..5985032bbac 100644 --- a/dev-tools/releng/src/hubris.rs +++ b/dev-tools/releng/src/hubris.rs @@ -10,11 +10,11 @@ use anyhow::Result; use camino::Utf8PathBuf; use fs_err::tokio as fs; use futures::future::TryFutureExt; -use omicron_common::api::internal::nexus::KnownArtifactKind; use semver::Version; use serde::Deserialize; use slog::warn; use slog::Logger; +use tufaceous_artifact::KnownArtifactKind; use tufaceous_lib::assemble::DeserializedArtifactData; use tufaceous_lib::assemble::DeserializedArtifactSource; use tufaceous_lib::assemble::DeserializedFileArtifactSource; diff --git a/dev-tools/releng/src/tuf.rs b/dev-tools/releng/src/tuf.rs index 9b7e66278de..b43fba22057 100644 --- a/dev-tools/releng/src/tuf.rs +++ b/dev-tools/releng/src/tuf.rs @@ -12,13 +12,13 @@ use chrono::Timelike; use chrono::Utc; use fs_err::tokio as fs; use fs_err::tokio::File; -use omicron_common::api::internal::nexus::KnownArtifactKind; use omicron_zone_package::config::Config; use semver::Version; use sha2::Digest; use sha2::Sha256; use slog::Logger; use tokio::io::AsyncReadExt; +use tufaceous_artifact::KnownArtifactKind; use tufaceous_lib::assemble::ArtifactManifest; use tufaceous_lib::assemble::DeserializedArtifactData; use tufaceous_lib::assemble::DeserializedArtifactSource; diff --git a/installinator/Cargo.toml b/installinator/Cargo.toml index c1ea2bd8598..e9cca136e24 100644 --- a/installinator/Cargo.toml +++ b/installinator/Cargo.toml @@ -45,6 +45,7 @@ tufaceous-lib.workspace = true update-engine.workspace = true uuid.workspace = true omicron-workspace-hack.workspace = true +tufaceous-artifact.workspace = true [dev-dependencies] omicron-test-utils.workspace = true diff --git a/installinator/src/dispatch.rs b/installinator/src/dispatch.rs index 1fcf351a9b7..845fbbc012a 100644 --- a/installinator/src/dispatch.rs +++ b/installinator/src/dispatch.rs @@ -13,13 +13,11 @@ use installinator_common::{ InstallinatorStepId, StepContext, StepHandle, StepProgress, StepSuccess, StepWarning, UpdateEngine, }; +use omicron_common::update::{ArtifactHash, ArtifactHashId}; use omicron_common::FileKv; -use omicron_common::{ - api::internal::nexus::KnownArtifactKind, - update::{ArtifactHash, ArtifactHashId, ArtifactKind}, -}; use sha2::{Digest, Sha256}; use slog::{error, warn, Drain}; +use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; use tufaceous_lib::ControlPlaneZoneImages; use update_engine::StepResult; diff --git a/installinator/src/mock_peers.rs b/installinator/src/mock_peers.rs index ccb35a2f066..d562ef91d04 100644 --- a/installinator/src/mock_peers.rs +++ b/installinator/src/mock_peers.rs @@ -560,10 +560,10 @@ mod tests { InstallinatorProgressMetadata, InstallinatorStepId, StepContext, StepEvent, StepEventKind, StepOutcome, StepSuccess, UpdateEngine, }; - use omicron_common::api::internal::nexus::KnownArtifactKind; use omicron_test_utils::dev::test_setup_log; use test_strategy::proptest; use tokio_stream::wrappers::ReceiverStream; + use tufaceous_artifact::KnownArtifactKind; // The #[proptest] macro doesn't currently with with #[tokio::test] sadly. #[proptest] diff --git a/installinator/src/test_helpers.rs b/installinator/src/test_helpers.rs index a12a377e15e..42a49442ec9 100644 --- a/installinator/src/test_helpers.rs +++ b/installinator/src/test_helpers.rs @@ -3,10 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use futures::Future; -use omicron_common::{ - api::internal::nexus::KnownArtifactKind, - update::{ArtifactHash, ArtifactHashId}, -}; +use omicron_common::update::{ArtifactHash, ArtifactHashId}; +use tufaceous_artifact::KnownArtifactKind; pub(crate) fn dummy_artifact_hash_id( kind: KnownArtifactKind, diff --git a/installinator/src/write.rs b/installinator/src/write.rs index 7274ef4ad55..af1d93a3592 100644 --- a/installinator/src/write.rs +++ b/installinator/src/write.rs @@ -924,9 +924,6 @@ mod tests { Event, InstallinatorCompletionMetadata, InstallinatorComponent, InstallinatorStepId, StepEventKind, StepOutcome, }; - use omicron_common::{ - api::internal::nexus::KnownArtifactKind, update::ArtifactKind, - }; use omicron_test_utils::dev::test_setup_log; use partial_io::{ proptest_types::{ @@ -939,6 +936,7 @@ mod tests { use tokio::io::AsyncReadExt; use tokio::sync::Mutex; use tokio_stream::wrappers::ReceiverStream; + use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; #[proptest(ProptestConfig { cases: 32, ..ProptestConfig::default() })] fn proptest_write_artifact( diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 4b73a8dd783..95b9e58b110 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -162,6 +162,7 @@ tufaceous.workspace = true tufaceous-lib.workspace = true httptest.workspace = true strum.workspace = true +tufaceous-artifact.workspace = true [[bench]] name = "setup_benchmark" diff --git a/nexus/db-model/Cargo.toml b/nexus/db-model/Cargo.toml index 26de4d9e890..41a4fb419c0 100644 --- a/nexus/db-model/Cargo.toml +++ b/nexus/db-model/Cargo.toml @@ -50,6 +50,7 @@ nexus-types.workspace = true omicron-passwords.workspace = true sled-agent-client.workspace = true omicron-workspace-hack.workspace = true +tufaceous-artifact.workspace = true [dev-dependencies] camino-tempfile.workspace = true diff --git a/nexus/db-model/src/tuf_repo.rs b/nexus/db-model/src/tuf_repo.rs index f603e681887..fd61378129d 100644 --- a/nexus/db-model/src/tuf_repo.rs +++ b/nexus/db-model/src/tuf_repo.rs @@ -13,13 +13,14 @@ use chrono::{DateTime, Utc}; use diesel::{deserialize::FromSql, serialize::ToSql, sql_types::Text}; use omicron_common::{ api::external, - update::{ArtifactHash as ExternalArtifactHash, ArtifactId, ArtifactKind}, + update::{ArtifactHash as ExternalArtifactHash, ArtifactId}, }; use omicron_uuid_kinds::TufArtifactKind; use omicron_uuid_kinds::TufRepoKind; use omicron_uuid_kinds::TypedUuid; use serde::{Deserialize, Serialize}; use std::fmt; +use tufaceous_artifact::ArtifactKind; use uuid::Uuid; /// A description of a TUF update: a repo, along with the artifacts it diff --git a/nexus/tests/integration_tests/updates.rs b/nexus/tests/integration_tests/updates.rs index c67c25b579d..606f3622fa1 100644 --- a/nexus/tests/integration_tests/updates.rs +++ b/nexus/tests/integration_tests/updates.rs @@ -21,7 +21,6 @@ use nexus_test_utils::{load_test_config, test_setup, test_setup_with_config}; use omicron_common::api::external::{ TufRepoGetResponse, TufRepoInsertResponse, TufRepoInsertStatus, }; -use omicron_common::api::internal::nexus::KnownArtifactKind; use omicron_sled_agent::sim; use pretty_assertions::assert_eq; use semver::Version; @@ -29,6 +28,7 @@ use serde::Deserialize; use std::collections::HashSet; use std::fmt::Debug; use std::io::Write; +use tufaceous_artifact::KnownArtifactKind; use tufaceous_lib::assemble::{DeserializedManifest, ManifestTweak}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 421f8f6e098..b509ef68909 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -108,7 +108,7 @@ omicron-workspace-hack.workspace = true slog-error-chain.workspace = true walkdir.workspace = true zip.workspace = true -omicron-brand-metadata.workspace = true +tufaceous-brand-metadata.workspace = true [target.'cfg(target_os = "illumos")'.dependencies] opte-ioctl.workspace = true diff --git a/sled-agent/src/fakes/nexus.rs b/sled-agent/src/fakes/nexus.rs index 1efbf695b36..d3e68d856f3 100644 --- a/sled-agent/src/fakes/nexus.rs +++ b/sled-agent/src/fakes/nexus.rs @@ -7,16 +7,15 @@ //! This must be an exact subset of the Nexus internal interface //! to operate correctly. -use dropshot::Body; use dropshot::{ - endpoint, ApiDescription, FreeformBody, HttpError, HttpResponseOk, + endpoint, ApiDescription, HttpError, HttpResponseOk, HttpResponseUpdatedNoContent, Path, RequestContext, TypedBody, }; use internal_dns_types::config::DnsConfigBuilder; use internal_dns_types::names::ServiceName; use nexus_client::types::SledAgentInfo; use omicron_common::api::external::Error; -use omicron_common::api::internal::nexus::{SledVmmState, UpdateArtifactId}; +use omicron_common::api::internal::nexus::SledVmmState; use omicron_uuid_kinds::{OmicronZoneUuid, PropolisUuid, SledUuid}; use schemars::JsonSchema; use serde::Deserialize; @@ -28,13 +27,6 @@ use sled_agent_api::VmmPathParam; /// - Not all methods should be called by all tests. By default, /// each method, representing an endpoint, should return an error. pub trait FakeNexusServer: Send + Sync { - fn cpapi_artifact_download( - &self, - _artifact_id: UpdateArtifactId, - ) -> Result, Error> { - Err(Error::internal_error("Not implemented")) - } - fn sled_agent_get( &self, _sled_id: SledUuid, @@ -65,22 +57,6 @@ pub trait FakeNexusServer: Send + Sync { /// [`start_test_server`]. pub type ServerContext = Box; -#[endpoint { - method = GET, - path = "/artifacts/{kind}/{name}/{version}", -}] -async fn cpapi_artifact_download( - request_context: RequestContext, - path_params: Path, -) -> Result, HttpError> { - let context = request_context.context(); - - Ok(HttpResponseOk( - Body::from(context.cpapi_artifact_download(path_params.into_inner())?) - .into(), - )) -} - /// Path parameters for Sled Agent requests (internal API) #[derive(Deserialize, JsonSchema)] struct SledAgentPathParam { @@ -139,7 +115,6 @@ async fn cpapi_instances_put( fn api() -> ApiDescription { let mut api = ApiDescription::new(); - api.register(cpapi_artifact_download).unwrap(); api.register(sled_agent_get).unwrap(); api.register(sled_agent_put).unwrap(); api.register(cpapi_instances_put).unwrap(); diff --git a/sled-agent/src/updates.rs b/sled-agent/src/updates.rs index bb222a4a9e5..fc27ab943a6 100644 --- a/sled-agent/src/updates.rs +++ b/sled-agent/src/updates.rs @@ -6,8 +6,8 @@ use bootstrap_agent_api::Component; use camino::{Utf8Path, Utf8PathBuf}; -use omicron_brand_metadata::Metadata; use serde::{Deserialize, Serialize}; +use tufaceous_brand_metadata::Metadata; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/tufaceous-lib/Cargo.toml b/tufaceous-lib/Cargo.toml deleted file mode 100644 index c3814b1215f..00000000000 --- a/tufaceous-lib/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "tufaceous-lib" -version = "0.1.0" -edition = "2021" -license = "MPL-2.0" -publish = false - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true, features = ["backtrace"] } -async-trait.workspace = true -aws-lc-rs.workspace = true -base64.workspace = true -buf-list.workspace = true -bytes.workspace = true -camino.workspace = true -camino-tempfile.workspace = true -chrono.workspace = true -debug-ignore.workspace = true -flate2.workspace = true -fs-err.workspace = true -futures.workspace = true -hex.workspace = true -hubtools.workspace = true -itertools.workspace = true -omicron-brand-metadata.workspace = true -omicron-common.workspace = true -omicron-workspace-hack.workspace = true -parse-size.workspace = true -rand.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_path_to_error.workspace = true -sha2.workspace = true -slog.workspace = true -tar.workspace = true -tokio.workspace = true -toml.workspace = true -tough.workspace = true -url.workspace = true -zip.workspace = true - -[dev-dependencies] -omicron-test-utils.workspace = true -tokio = { workspace = true, features = ["test-util"] } diff --git a/tufaceous-lib/src/archive.rs b/tufaceous-lib/src/archive.rs deleted file mode 100644 index 60be5799b2d..00000000000 --- a/tufaceous-lib/src/archive.rs +++ /dev/null @@ -1,297 +0,0 @@ -// 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/. - -//! Support for reading and writing zip archives. - -use anyhow::{anyhow, bail, Context, Result}; -use buf_list::BufList; -use bytes::Bytes; -use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; -use debug_ignore::DebugIgnore; -use fs_err::File; -use std::{ - fmt, - io::{BufReader, BufWriter, Cursor, Read, Seek}, -}; -use zip::{ - write::{FileOptions, SimpleFileOptions}, - CompressionMethod, ZipArchive, ZipWriter, -}; - -/// A builder for TUF repo archives. -#[derive(Debug)] -pub(crate) struct ArchiveBuilder { - writer: DebugIgnore>>, - // Stored for better error messages. - output_path: Utf8PathBuf, -} - -/// Defines the base directory for TUF repo archives created by this tool. -/// -/// The usual convention is that the base dir is the name of the archive -/// (e.g. foo-1.0 for foo-1.0.zip). but just using a consistent name here -/// simplifies the code that extracts the archive. -pub const ZIP_BASE_DIR: &str = "repo"; - -impl ArchiveBuilder { - /// Creates a new `ArchiveBuilder`, writing to the given path. - pub fn new(output_path: Utf8PathBuf) -> Result { - // The filename must end with "zip". - if output_path.extension() != Some("zip") { - bail!("output path `{output_path}` must end with .zip"); - } - - let file = File::create(&output_path)?; - let writer = ZipWriter::new(BufWriter::new(file)); - Ok(Self { writer: writer.into(), output_path }) - } - - /// Writes the given path to the archive at the name `name`. - /// - /// The name has [`ZIP_BASE_DIR`] prepended to it. - pub fn write_file( - &mut self, - path: &Utf8Path, - name: &Utf8Path, - ) -> Result<()> { - let name = Utf8Path::new(ZIP_BASE_DIR).join(name); - - self.writer.start_file(name.as_str(), Self::file_options())?; - let mut reader = fs_err::File::open(path)?; - std::io::copy(&mut reader, &mut *self.writer).with_context(|| { - format!( - "error writing `{path}` to archive at `{}`", - self.output_path - ) - })?; - Ok(()) - } - - pub fn finish(self) -> Result<()> { - let Self { writer, output_path } = self; - - let zip_file = writer.0.finish().with_context(|| { - format!("error finalizing archive at `{}`", output_path) - })?; - zip_file.into_inner().with_context(|| { - format!("error writing archive at `{}`", output_path) - })?; - - Ok(()) - } - - fn file_options() -> SimpleFileOptions { - // The main purpose of the zip archive is to transmit archives that are - // already compressed, so there's no point trying to re-compress them. - FileOptions::default().compression_method(CompressionMethod::Stored) - } -} - -/// An extractor for archives created by tufaceous. -/// -/// Ideally we'd just be able to read the TUF repo out of a zip archive in -/// memory, but sadly that isn't possible today due to a missing lifetime -/// parameter on `Transport::fetch`. See [this -/// issue](https://github.com/awslabs/tough/pull/563). -#[derive(Debug)] -pub struct ArchiveExtractor { - archive: ZipArchive, -} - -impl ArchiveExtractor> { - /// Builds an extractor from the given path. - /// - /// The archive must be a zip file generated by tufaceous. - pub fn from_path(zip_path: &Utf8Path) -> Result { - let reader = BufReader::new(File::open(zip_path)?); - Self::new(reader).with_context(|| { - format!("error opening zip archive at `{zip_path}`") - }) - } -} - -impl<'a> ArchiveExtractor> { - /// Loads an archived repository from memory as borrowed bytes. - pub fn from_borrowed_bytes(archive: &'a [u8]) -> Result { - let reader = Cursor::new(archive); - Self::new(reader).context("error opening zip archive from memory") - } -} - -impl ArchiveExtractor> { - /// Loads an archived repository from memory as owned bytes. - pub fn from_owned_bytes(archive: impl Into) -> Result { - let reader = Cursor::new(archive.into()); - Self::new(reader).context("error opening zip archive from memory") - } -} - -impl<'a> ArchiveExtractor> { - /// Loads an archived repository from memory as a borrowed BufList. - pub fn from_borrowed_buf_list(archive: &'a BufList) -> Result { - let reader = buf_list::Cursor::new(archive); - Self::new(reader).context("error opening zip archive from memory") - } -} - -impl ArchiveExtractor> { - /// Loads an archived repository from memory as an owned BufList. - pub fn from_owned_buf_list(archive: BufList) -> Result { - let reader = buf_list::Cursor::new(archive); - Self::new(reader).context("error opening zip archive from memory") - } -} - -impl ArchiveExtractor -where - R: Read + Seek, -{ - /// Creates a new `ArchiveExtractor` from the given reader. - pub fn new(reader: R) -> Result> { - // Validate the archive to ensure all paths are correctly formed. - let archive = Self::validate(ZipArchive::new(reader)?)?; - - Ok(Self { archive }) - } - - fn validate(mut archive: ZipArchive) -> Result> { - for i in 0..archive.len() { - let zip_file = archive.by_index(i).with_context(|| { - format!("error reading file number `{i} from archive") - })?; - if !zip_file.is_file() { - bail!("archive must consist only of files, not directories"); - } - let path = Utf8Path::new(zip_file.name()); - validate_path(path).map_err(|error| { - anyhow!("invalid path in archive `{path}`: {error}") - })?; - } - - Ok(archive) - } - - /// Extracts this archive into the specified directory. - /// - /// Once this is completed, use - /// [`OmicronRepo::load_untrusted`](crate::OmicronRepo::load_untrusted) to - /// load the archive from `output_dir`. - /// - /// [`ZIP_BASE_DIR`] will be stripped from output paths. - pub fn extract(&mut self, output_dir: &Utf8Path) -> Result<()> { - for i in 0..self.archive.len() { - let mut zip_file = self.archive.by_index(i).with_context(|| { - format!("error reading file number `{i} from archive") - })?; - // SAFETY: file names have already been checked in `Self::validate`. - let file_name = Utf8Path::new(zip_file.name()).to_owned(); - let dest_path = output_dir.join( - file_name - .strip_prefix(ZIP_BASE_DIR) - .expect("checked in Self::validate"), - ); - // The file is in a directory. - fs_err::create_dir_all( - dest_path.parent().expect("at least 1 component"), - )?; - - let mut writer = File::create(&dest_path)?; - std::io::copy(&mut zip_file, &mut writer).with_context(|| { - format!( - "error writing `{file_name}` in archive to `{dest_path}`" - ) - })?; - } - - Ok(()) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum InvalidPath<'a> { - AbsolutePath, - ExactlyBaseDir, - IncorrectBaseDir, - InvalidComponent(Utf8Component<'a>), -} - -impl fmt::Display for InvalidPath<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - InvalidPath::AbsolutePath => { - write!(f, "path is absolute -- expected relative paths") - } - InvalidPath::ExactlyBaseDir => { - write!(f, "path is exactly `{ZIP_BASE_DIR}` -- expected `{ZIP_BASE_DIR}/`") - } - InvalidPath::IncorrectBaseDir => { - write!(f, "invalid base directory -- must be `{ZIP_BASE_DIR}`") - } - InvalidPath::InvalidComponent(component) => { - write!(f, "invalid component `{component}`") - } - } - } -} - -fn validate_path(path: &Utf8Path) -> Result<(), InvalidPath<'_>> { - if path.is_absolute() { - return Err(InvalidPath::AbsolutePath); - } - if path == ZIP_BASE_DIR { - return Err(InvalidPath::ExactlyBaseDir); - } - if !path.starts_with(ZIP_BASE_DIR) { - return Err(InvalidPath::IncorrectBaseDir); - } - - for component in path.components() { - if !matches!(component, Utf8Component::Normal(_)) { - return Err(InvalidPath::InvalidComponent(component)); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_validate_path() { - let valid = ["repo/foo", "repo/foo/bar", "repo/foo/./bar", "repo//foo"]; - let invalid = [ - ("repo", InvalidPath::ExactlyBaseDir), - ("repo/", InvalidPath::ExactlyBaseDir), - ("repo/.", InvalidPath::ExactlyBaseDir), - ("not-repo", InvalidPath::IncorrectBaseDir), - ("not-repo/foo", InvalidPath::IncorrectBaseDir), - ( - "repo/..", - InvalidPath::InvalidComponent(Utf8Component::ParentDir), - ), - ("/repo/foo", InvalidPath::AbsolutePath), - ]; - - for path in valid { - validate_path(Utf8Path::new(path)).unwrap_or_else(|err| { - panic!("expected path `{path}` to be valid: {err}") - }); - } - - for (path, expected) in invalid { - eprintln!("testing invalid path: `{path}`"); - let actual = match validate_path(Utf8Path::new(path)) { - Ok(()) => panic!("expected path `{path}` to be invalid"), - Err(error) => error, - }; - - assert_eq!( - actual, expected, - "for path `{path}`, InvalidPath error should match" - ); - } - } -} diff --git a/tufaceous-lib/src/artifact.rs b/tufaceous-lib/src/artifact.rs deleted file mode 100644 index 903e2dbab9d..00000000000 --- a/tufaceous-lib/src/artifact.rs +++ /dev/null @@ -1,431 +0,0 @@ -// 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/. - -use std::{ - io::{self, BufReader, Write}, - path::Path, -}; - -use anyhow::{bail, Context, Result}; -use buf_list::BufList; -use bytes::Bytes; -use camino::Utf8PathBuf; -use fs_err::File; -use omicron_brand_metadata::Metadata; -use omicron_common::update::ArtifactKind; -use semver::Version; - -mod composite; - -pub use composite::CompositeControlPlaneArchiveBuilder; -pub use composite::CompositeEntry; -pub use composite::CompositeHostArchiveBuilder; -pub use composite::CompositeRotArchiveBuilder; -pub use composite::MtimeSource; - -/// The location a artifact will be obtained from. -#[derive(Clone, Debug)] -pub enum ArtifactSource { - File(Utf8PathBuf), - Memory(BufList), - // We might need to support downloading data over HTTP as well -} - -/// Describes a new artifact to be added. -pub struct AddArtifact { - kind: ArtifactKind, - name: String, - version: Version, - source: ArtifactSource, -} - -impl AddArtifact { - /// Creates an [`AddArtifact`] from the provided source. - pub fn new( - kind: ArtifactKind, - name: String, - version: Version, - source: ArtifactSource, - ) -> Self { - Self { kind, name, version, source } - } - - /// Creates an [`AddArtifact`] from the path, name and version. - /// - /// If the name is `None`, it is derived from the filename of the path - /// without matching extensions. - pub fn from_path( - kind: ArtifactKind, - name: Option, - version: Version, - path: Utf8PathBuf, - ) -> Result { - let name = match name { - Some(name) => name, - None => path - .file_name() - .context("artifact path is a directory")? - .split('.') - .next() - .expect("str::split has at least 1 element") - .to_owned(), - }; - - Ok(Self { kind, name, version, source: ArtifactSource::File(path) }) - } - - /// Returns the kind of artifact this is. - pub fn kind(&self) -> &ArtifactKind { - &self.kind - } - - /// Returns the name of the new artifact. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the version of the new artifact. - pub fn version(&self) -> &Version { - &self.version - } - - /// Returns the source for this artifact. - pub fn source(&self) -> &ArtifactSource { - &self.source - } - - /// Writes this artifact to the specified writer. - pub(crate) fn write_to(&self, writer: &mut W) -> Result<()> { - match &self.source { - ArtifactSource::File(path) => { - let mut reader = File::open(path)?; - std::io::copy(&mut reader, writer)?; - } - ArtifactSource::Memory(buf_list) => { - for chunk in buf_list { - writer.write_all(chunk)?; - } - } - } - - Ok(()) - } -} - -pub(crate) fn make_filler_text(length: usize) -> Vec { - std::iter::repeat(FILLER_TEXT).flatten().copied().take(length).collect() -} - -/// Represents host phase images. -/// -/// The host and trampoline artifacts are actually tarballs, with phase 1 and -/// phase 2 images inside them. This code extracts those images out of the -/// tarballs. -#[derive(Clone, Debug)] -pub struct HostPhaseImages { - pub phase_1: Bytes, - pub phase_2: Bytes, -} - -impl HostPhaseImages { - pub fn extract(reader: R) -> Result { - let mut phase_1 = Vec::new(); - let mut phase_2 = Vec::new(); - Self::extract_into( - reader, - io::Cursor::<&mut Vec>::new(&mut phase_1), - io::Cursor::<&mut Vec>::new(&mut phase_2), - )?; - Ok(Self { phase_1: phase_1.into(), phase_2: phase_2.into() }) - } - - pub fn extract_into( - reader: R, - phase_1: W, - phase_2: W, - ) -> Result<()> { - let uncompressed = flate2::bufread::GzDecoder::new(reader); - let mut archive = tar::Archive::new(uncompressed); - - let mut oxide_json_found = false; - let mut phase_1_writer = Some(phase_1); - let mut phase_2_writer = Some(phase_2); - for entry in archive - .entries() - .context("error building list of entries from archive")? - { - let entry = entry.context("error reading entry from archive")?; - let path = entry - .header() - .path() - .context("error reading path from archive")?; - if path == Path::new(OXIDE_JSON_FILE_NAME) { - let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; - let metadata: Metadata = - serde_json::from_slice(&json_bytes).with_context(|| { - format!( - "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" - ) - })?; - if !metadata.is_os() { - bail!( - "unexpected archive type: expected os, found {:?}", - metadata.archive_type(), - ) - } - oxide_json_found = true; - } else if path == Path::new(HOST_PHASE_1_FILE_NAME) { - if let Some(phase_1) = phase_1_writer.take() { - read_entry_into(entry, HOST_PHASE_1_FILE_NAME, phase_1)?; - } - } else if path == Path::new(HOST_PHASE_2_FILE_NAME) { - if let Some(phase_2) = phase_2_writer.take() { - read_entry_into(entry, HOST_PHASE_2_FILE_NAME, phase_2)?; - } - } - - if oxide_json_found - && phase_1_writer.is_none() - && phase_2_writer.is_none() - { - break; - } - } - - let mut not_found = Vec::new(); - if !oxide_json_found { - not_found.push(OXIDE_JSON_FILE_NAME); - } - - // If we didn't `.take()` the writer out of the options, we never saw - // the expected phase1/phase2 filenames. - if phase_1_writer.is_some() { - not_found.push(HOST_PHASE_1_FILE_NAME); - } - if phase_2_writer.is_some() { - not_found.push(HOST_PHASE_2_FILE_NAME); - } - - if !not_found.is_empty() { - bail!("required files not found: {}", not_found.join(", ")) - } - - Ok(()) - } -} - -fn read_entry( - entry: tar::Entry, - file_name: &str, -) -> Result { - let mut buf = Vec::new(); - read_entry_into(entry, file_name, io::Cursor::new(&mut buf))?; - Ok(buf.into()) -} - -fn read_entry_into( - mut entry: tar::Entry, - file_name: &str, - mut out: W, -) -> Result<()> { - let entry_type = entry.header().entry_type(); - if entry_type != tar::EntryType::Regular { - bail!("for {file_name}, expected regular file, found {entry_type:?}"); - } - io::copy(&mut entry, &mut out) - .with_context(|| format!("error reading {file_name} from archive"))?; - Ok(()) -} - -/// Represents RoT A/B hubris archives. -/// -/// RoT artifacts are actually tarballs, with both A and B hubris archives -/// inside them. This code extracts those archives out of the tarballs. -#[derive(Clone, Debug)] -pub struct RotArchives { - pub archive_a: Bytes, - pub archive_b: Bytes, -} - -impl RotArchives { - pub fn extract(reader: R) -> Result { - let mut archive_a = Vec::new(); - let mut archive_b = Vec::new(); - Self::extract_into( - reader, - io::Cursor::<&mut Vec>::new(&mut archive_a), - io::Cursor::<&mut Vec>::new(&mut archive_b), - )?; - Ok(Self { archive_a: archive_a.into(), archive_b: archive_b.into() }) - } - - pub fn extract_into( - reader: R, - archive_a: W, - archive_b: W, - ) -> Result<()> { - let uncompressed = flate2::bufread::GzDecoder::new(reader); - let mut archive = tar::Archive::new(uncompressed); - - let mut oxide_json_found = false; - let mut archive_a_writer = Some(archive_a); - let mut archive_b_writer = Some(archive_b); - for entry in archive - .entries() - .context("error building list of entries from archive")? - { - let entry = entry.context("error reading entry from archive")?; - let path = entry - .header() - .path() - .context("error reading path from archive")?; - if path == Path::new(OXIDE_JSON_FILE_NAME) { - let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; - let metadata: Metadata = - serde_json::from_slice(&json_bytes).with_context(|| { - format!( - "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" - ) - })?; - if !metadata.is_rot() { - bail!( - "unexpected archive type: expected rot, found {:?}", - metadata.archive_type(), - ) - } - oxide_json_found = true; - } else if path == Path::new(ROT_ARCHIVE_A_FILE_NAME) { - if let Some(archive_a) = archive_a_writer.take() { - read_entry_into(entry, ROT_ARCHIVE_A_FILE_NAME, archive_a)?; - } - } else if path == Path::new(ROT_ARCHIVE_B_FILE_NAME) { - if let Some(archive_b) = archive_b_writer.take() { - read_entry_into(entry, ROT_ARCHIVE_B_FILE_NAME, archive_b)?; - } - } - - if oxide_json_found - && archive_a_writer.is_none() - && archive_b_writer.is_none() - { - break; - } - } - - let mut not_found = Vec::new(); - if !oxide_json_found { - not_found.push(OXIDE_JSON_FILE_NAME); - } - - // If we didn't `.take()` the writer out of the options, we never saw - // the expected A/B filenames. - if archive_a_writer.is_some() { - not_found.push(ROT_ARCHIVE_A_FILE_NAME); - } - if archive_b_writer.is_some() { - not_found.push(ROT_ARCHIVE_B_FILE_NAME); - } - - if !not_found.is_empty() { - bail!("required files not found: {}", not_found.join(", ")) - } - - Ok(()) - } -} - -/// Represents control plane zone images. -/// -/// The control plane artifact is actually a tarball that contains a set of zone -/// images. This code extracts those images out of the tarball. -#[derive(Clone, Debug)] -pub struct ControlPlaneZoneImages { - pub zones: Vec<(String, Bytes)>, -} - -impl ControlPlaneZoneImages { - pub fn extract(reader: R) -> Result { - let mut zones = Vec::new(); - Self::extract_into(reader, |name, reader| { - let mut buf = Vec::new(); - io::copy(reader, &mut buf)?; - zones.push((name, buf.into())); - Ok(()) - })?; - Ok(Self { zones }) - } - - pub fn extract_into(reader: R, mut handler: F) -> Result<()> - where - R: io::Read, - F: FnMut(String, &mut dyn io::Read) -> Result<()>, - { - let uncompressed = - flate2::bufread::GzDecoder::new(BufReader::new(reader)); - let mut archive = tar::Archive::new(uncompressed); - - let mut oxide_json_found = false; - let mut zone_found = false; - for entry in archive - .entries() - .context("error building list of entries from archive")? - { - let mut entry = - entry.context("error reading entry from archive")?; - let path = entry - .header() - .path() - .context("error reading path from archive")?; - if path == Path::new(OXIDE_JSON_FILE_NAME) { - let json_bytes = read_entry(entry, OXIDE_JSON_FILE_NAME)?; - let metadata: Metadata = - serde_json::from_slice(&json_bytes).with_context(|| { - format!( - "error deserializing JSON from {OXIDE_JSON_FILE_NAME}" - ) - })?; - if !metadata.is_control_plane() { - bail!( - "unexpected archive type: expected control_plane, found {:?}", - metadata.archive_type(), - ) - } - oxide_json_found = true; - } else if path.starts_with(CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY) { - if let Some(name) = path - .file_name() - .and_then(|s| s.to_str()) - .map(|s| s.to_string()) - { - handler(name, &mut entry)?; - } - zone_found = true; - } - } - - let mut not_found = Vec::new(); - if !oxide_json_found { - not_found.push(OXIDE_JSON_FILE_NAME); - } - if !not_found.is_empty() { - bail!("required files not found: {}", not_found.join(", ")) - } - if !zone_found { - bail!( - "no zone images found in `{}/`", - CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY - ); - } - - Ok(()) - } -} - -static FILLER_TEXT: &[u8; 16] = b"tufaceousfaketxt"; -static OXIDE_JSON_FILE_NAME: &str = "oxide.json"; -static HOST_PHASE_1_FILE_NAME: &str = "image/rom"; -static HOST_PHASE_2_FILE_NAME: &str = "image/zfs.img"; -static ROT_ARCHIVE_A_FILE_NAME: &str = "archive-a.zip"; -static ROT_ARCHIVE_B_FILE_NAME: &str = "archive-b.zip"; -static CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY: &str = "zones"; diff --git a/tufaceous-lib/src/artifact/composite.rs b/tufaceous-lib/src/artifact/composite.rs deleted file mode 100644 index 50574d704f3..00000000000 --- a/tufaceous-lib/src/artifact/composite.rs +++ /dev/null @@ -1,216 +0,0 @@ -// 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/. - -use super::CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY; -use super::HOST_PHASE_1_FILE_NAME; -use super::HOST_PHASE_2_FILE_NAME; -use super::ROT_ARCHIVE_A_FILE_NAME; -use super::ROT_ARCHIVE_B_FILE_NAME; -use anyhow::anyhow; -use anyhow::bail; -use anyhow::Context; -use anyhow::Result; -use camino::Utf8Path; -use flate2::write::GzEncoder; -use flate2::Compression; -use omicron_brand_metadata::{ArchiveType, Metadata}; -use sha2::Digest; -use sha2::Sha256; -use std::collections::HashMap; -use std::io::BufWriter; -use std::io::Write; - -/// Represents a single entry in a composite artifact. -/// -/// A composite artifact is a tarball containing multiple artifacts. This -/// struct is intended for the insertion of one such entry into the artifact. -/// -/// At the moment it only accepts byte slices, but it could be extended to -/// support arbitrary readers in the future. -pub struct CompositeEntry<'a> { - pub data: &'a [u8], - pub mtime_source: MtimeSource, -} - -pub struct CompositeControlPlaneArchiveBuilder { - inner: CompositeTarballBuilder, - hashes: HashMap<[u8; 32], String>, -} - -impl CompositeControlPlaneArchiveBuilder { - pub fn new(writer: W, mtime_source: MtimeSource) -> Result { - let metadata = Metadata::new(ArchiveType::ControlPlane); - let inner = - CompositeTarballBuilder::new(writer, metadata, mtime_source)?; - Ok(Self { inner, hashes: HashMap::new() }) - } - - pub fn append_zone( - &mut self, - name: &str, - entry: CompositeEntry<'_>, - ) -> Result<()> { - let name_path = Utf8Path::new(name); - if name_path.file_name() != Some(name) { - bail!("control plane zone filenames should not contain paths"); - } - if let Some(duplicate) = - self.hashes.insert(Sha256::digest(&entry.data).into(), name.into()) - { - bail!( - "duplicate zones are not allowed \ - ({name} and {duplicate} have the same checksum)" - ); - } - let path = - Utf8Path::new(CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY).join(name_path); - self.inner.append_file(path.as_str(), entry) - } - - pub fn finish(self) -> Result { - self.inner.finish() - } -} - -pub struct CompositeRotArchiveBuilder { - inner: CompositeTarballBuilder, -} - -impl CompositeRotArchiveBuilder { - pub fn new(writer: W, mtime_source: MtimeSource) -> Result { - let metadata = Metadata::new(ArchiveType::Rot); - let inner = - CompositeTarballBuilder::new(writer, metadata, mtime_source)?; - Ok(Self { inner }) - } - - pub fn append_archive_a( - &mut self, - entry: CompositeEntry<'_>, - ) -> Result<()> { - self.inner.append_file(ROT_ARCHIVE_A_FILE_NAME, entry) - } - - pub fn append_archive_b( - &mut self, - entry: CompositeEntry<'_>, - ) -> Result<()> { - self.inner.append_file(ROT_ARCHIVE_B_FILE_NAME, entry) - } - - pub fn finish(self) -> Result { - self.inner.finish() - } -} - -pub struct CompositeHostArchiveBuilder { - inner: CompositeTarballBuilder, -} - -impl CompositeHostArchiveBuilder { - pub fn new(writer: W, mtime_source: MtimeSource) -> Result { - let metadata = Metadata::new(ArchiveType::Os); - let inner = - CompositeTarballBuilder::new(writer, metadata, mtime_source)?; - Ok(Self { inner }) - } - - pub fn append_phase_1(&mut self, entry: CompositeEntry<'_>) -> Result<()> { - self.inner.append_file(HOST_PHASE_1_FILE_NAME, entry) - } - - pub fn append_phase_2(&mut self, entry: CompositeEntry<'_>) -> Result<()> { - self.inner.append_file(HOST_PHASE_2_FILE_NAME, entry) - } - - pub fn finish(self) -> Result { - self.inner.finish() - } -} - -struct CompositeTarballBuilder { - builder: tar::Builder>>, -} - -impl CompositeTarballBuilder { - fn new( - writer: W, - metadata: Metadata, - mtime_source: MtimeSource, - ) -> Result { - let mut builder = tar::Builder::new(GzEncoder::new( - BufWriter::new(writer), - Compression::fast(), - )); - metadata.append_to_tar(&mut builder, mtime_source.into_mtime())?; - Ok(Self { builder }) - } - - fn append_file( - &mut self, - path: &str, - entry: CompositeEntry<'_>, - ) -> Result<()> { - let header = - make_tar_header(path, entry.data.len(), entry.mtime_source); - self.builder - .append(&header, entry.data) - .with_context(|| format!("error append {path:?}")) - } - - fn finish(self) -> Result { - let gz_encoder = - self.builder.into_inner().context("error finalizing archive")?; - let buf_writer = - gz_encoder.finish().context("error finishing gz encoder")?; - buf_writer - .into_inner() - .map_err(|_| anyhow!("error flushing buffered archive writer")) - } -} - -fn make_tar_header( - path: &str, - size: usize, - mtime_source: MtimeSource, -) -> tar::Header { - let mtime = mtime_source.into_mtime(); - - let mut header = tar::Header::new_ustar(); - header.set_username("root").unwrap(); - header.set_uid(0); - header.set_groupname("root").unwrap(); - header.set_gid(0); - header.set_path(path).unwrap(); - header.set_size(size as u64); - header.set_mode(0o444); - header.set_entry_type(tar::EntryType::Regular); - header.set_mtime(mtime); - header.set_cksum(); - - header -} - -/// How to obtain the `mtime` field for a tar header. -#[derive(Copy, Clone, Debug)] -pub enum MtimeSource { - /// Use a fixed timestamp of zero seconds past the Unix epoch. - Zero, - - /// Use the current time. - Now, -} - -impl MtimeSource { - pub(crate) fn into_mtime(self) -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - - match self { - Self::Zero => 0, - Self::Now => { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - } - } - } -} diff --git a/tufaceous-lib/src/assemble/build.rs b/tufaceous-lib/src/assemble/build.rs deleted file mode 100644 index 4cb636c9d3c..00000000000 --- a/tufaceous-lib/src/assemble/build.rs +++ /dev/null @@ -1,135 +0,0 @@ -// 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/. - -use anyhow::{Context, Result}; -use camino::{Utf8Path, Utf8PathBuf}; -use chrono::{DateTime, Utc}; - -use crate::{AddArtifact, Key, OmicronRepo}; - -use super::ArtifactManifest; - -/// Assembles a TUF repo from a list of artifacts. -#[derive(Debug)] -pub struct OmicronRepoAssembler { - log: slog::Logger, - manifest: ArtifactManifest, - build_dir: Option, - keys: Vec, - expiry: DateTime, - output_path: Utf8PathBuf, -} - -impl OmicronRepoAssembler { - pub fn new( - log: &slog::Logger, - manifest: ArtifactManifest, - keys: Vec, - expiry: DateTime, - output_path: Utf8PathBuf, - ) -> Self { - Self { - log: log.new(slog::o!("component" => "OmicronRepoAssembler")), - manifest, - build_dir: None, - keys, - expiry, - output_path, - } - } - - pub fn set_build_dir(&mut self, build_dir: Utf8PathBuf) -> &mut Self { - self.build_dir = Some(build_dir); - self - } - - pub async fn build(&self) -> Result<()> { - let (build_dir, is_temp) = match &self.build_dir { - Some(dir) => (dir.clone(), false), - None => { - // Create a new temporary directory. - let dir = camino_tempfile::Builder::new() - .prefix("tufaceous") - .tempdir()?; - // This will cause the directory to be preserved -- we're going - // to clean it up if it's successful. - let path = dir.into_path(); - (path, true) - } - }; - - slog::info!(self.log, "assembling repository in `{build_dir}`"); - - match self.build_impl(&build_dir).await { - Ok(()) => { - if is_temp { - slog::debug!(self.log, "assembly successful, cleaning up"); - // Log, but otherwise ignore, errors while cleaning up. - if let Err(error) = fs_err::remove_dir_all(&build_dir) { - slog::warn!( - self.log, - "failed to clean up temporary directory {build_dir}: {error}" - ) - } - } - - slog::info!( - self.log, - "artifacts assembled and archived to `{}`", - self.output_path - ); - } - Err(error) => { - slog::error!(self.log, "assembly failed: {error:?}"); - slog::info!( - self.log, - "failing build directory preserved: `{build_dir}`" - ); - } - } - - Ok(()) - } - - async fn build_impl(&self, build_dir: &Utf8Path) -> Result<()> { - let mut repository = OmicronRepo::initialize( - &self.log, - build_dir, - self.manifest.system_version.clone(), - self.keys.clone(), - self.expiry, - ) - .await? - .into_editor() - .await?; - - // Add all the artifacts. - for (kind, entries) in &self.manifest.artifacts { - for data in entries { - let new_artifact = AddArtifact::new( - (*kind).into(), - data.name.clone(), - data.version.clone(), - data.source.clone(), - ); - repository.add_artifact(&new_artifact).with_context(|| { - format!("error adding artifact with kind `{kind}`") - })?; - } - } - - // Write out the repository. - repository.sign_and_finish(self.keys.clone(), self.expiry).await?; - - // Now reopen the repository to archive it into a zip file. - let repo2 = OmicronRepo::load_untrusted(&self.log, build_dir) - .await - .context("error reopening repository to archive")?; - repo2 - .archive(&self.output_path) - .context("error archiving repository")?; - - Ok(()) - } -} diff --git a/tufaceous-lib/src/assemble/manifest.rs b/tufaceous-lib/src/assemble/manifest.rs deleted file mode 100644 index d6cd9eeb48e..00000000000 --- a/tufaceous-lib/src/assemble/manifest.rs +++ /dev/null @@ -1,669 +0,0 @@ -// 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/. - -use std::collections::{BTreeMap, BTreeSet}; - -use anyhow::{bail, ensure, Context, Result}; -use camino::{Utf8Path, Utf8PathBuf}; -use omicron_common::api::internal::nexus::KnownArtifactKind; -use parse_size::parse_size; -use semver::Version; -use serde::{Deserialize, Serialize}; - -use crate::{ - make_filler_text, ArtifactSource, CompositeControlPlaneArchiveBuilder, - CompositeEntry, CompositeHostArchiveBuilder, CompositeRotArchiveBuilder, - MtimeSource, -}; - -static FAKE_MANIFEST_TOML: &str = - include_str!("../../../tufaceous/manifests/fake.toml"); - -/// A list of components in a TUF repo representing a single update. -#[derive(Clone, Debug)] -pub struct ArtifactManifest { - pub system_version: Version, - pub artifacts: BTreeMap>, -} - -impl ArtifactManifest { - /// Reads a manifest in from a TOML file. - pub fn from_path(path: &Utf8Path) -> Result { - let input = fs_err::read_to_string(path)?; - let base_dir = path - .parent() - .with_context(|| format!("path `{path}` did not have a parent"))?; - Self::from_str(base_dir, &input) - } - - /// Deserializes a manifest from an input string. - pub fn from_str(base_dir: &Utf8Path, input: &str) -> Result { - let manifest = DeserializedManifest::from_str(input)?; - Self::from_deserialized(base_dir, manifest) - } - - /// Creates a manifest from a [`DeserializedManifest`]. - pub fn from_deserialized( - base_dir: &Utf8Path, - manifest: DeserializedManifest, - ) -> Result { - // Replace all paths in the deserialized manifest with absolute ones, - // and do some processing to support flexible manifests: - // - // 1. assemble any composite artifacts from their pieces - // 2. replace any "fake" artifacts with in-memory buffers - // - // Currently both of those transformations produce - // `ArtifactSource::Memory(_)` variants (i.e., composite and fake - // artifacts all sit in-memory until we're done with the manifest), - // which puts some limits on how large the inputs to the manifest can - // practically be. If this becomes onerous, we could instead write the - // transformed artifacts to temporary files. - // - // We do some additional error checking here to make sure the - // `CompositeZZZ` variants are only used with their corresponding - // `KnownArtifactKind`s. It would be nicer to enforce this more - // statically and let serde do these checks, but that seems relatively - // tricky in comparison to these checks. - - Ok(ArtifactManifest { - system_version: manifest.system_version, - artifacts: manifest - .artifacts - .into_iter() - .map(|(kind, entries)| { - Self::parse_deserialized_entries(base_dir, kind, entries) - }) - .collect::>()?, - }) - } - - fn parse_deserialized_entries( - base_dir: &Utf8Path, - kind: KnownArtifactKind, - entries: Vec, - ) -> Result<(KnownArtifactKind, Vec)> { - let entries = entries - .into_iter() - .map(|data| { - let source = match data.source { - DeserializedArtifactSource::File { path } => { - ArtifactSource::File(base_dir.join(path)) - } - DeserializedArtifactSource::Fake { size } => { - let fake_data = - FakeDataAttributes::new(kind, &data.version) - .make_data(size as usize); - ArtifactSource::Memory(fake_data.into()) - } - DeserializedArtifactSource::CompositeHost { - phase_1, - phase_2, - } => { - ensure!( - matches!( - kind, - KnownArtifactKind::Host - | KnownArtifactKind::Trampoline - ), - "`composite_host` source cannot be used with \ - artifact kind {kind:?}" - ); - - let mtime_source = - if phase_1.is_fake() && phase_2.is_fake() { - // Ensure stability of fake artifacts. - MtimeSource::Zero - } else { - MtimeSource::Now - }; - - let mut builder = CompositeHostArchiveBuilder::new( - Vec::new(), - mtime_source, - )?; - phase_1.with_entry( - FakeDataAttributes::new(kind, &data.version), - |entry| builder.append_phase_1(entry), - )?; - phase_2.with_entry( - FakeDataAttributes::new(kind, &data.version), - |entry| builder.append_phase_2(entry), - )?; - ArtifactSource::Memory(builder.finish()?.into()) - } - DeserializedArtifactSource::CompositeRot { - archive_a, - archive_b, - } => { - ensure!( - matches!( - kind, - KnownArtifactKind::GimletRot - | KnownArtifactKind::SwitchRot - | KnownArtifactKind::PscRot - ), - "`composite_rot` source cannot be used with \ - artifact kind {kind:?}" - ); - - let mtime_source = - if archive_a.is_fake() && archive_b.is_fake() { - // Ensure stability of fake artifacts. - MtimeSource::Zero - } else { - MtimeSource::Now - }; - - let mut builder = CompositeRotArchiveBuilder::new( - Vec::new(), - mtime_source, - )?; - archive_a.with_entry( - FakeDataAttributes::new(kind, &data.version), - |entry| builder.append_archive_a(entry), - )?; - archive_b.with_entry( - FakeDataAttributes::new(kind, &data.version), - |entry| builder.append_archive_b(entry), - )?; - ArtifactSource::Memory(builder.finish()?.into()) - } - DeserializedArtifactSource::CompositeControlPlane { - zones, - } => { - ensure!( - kind == KnownArtifactKind::ControlPlane, - "`composite_control_plane` source cannot be \ - used with artifact kind {kind:?}" - ); - - // Ensure stability of fake artifacts. - let mtime_source = if zones.iter().all(|z| z.is_fake()) - { - MtimeSource::Zero - } else { - MtimeSource::Now - }; - - let data = Vec::new(); - let mut builder = - CompositeControlPlaneArchiveBuilder::new( - data, - mtime_source, - )?; - - for zone in zones { - zone.with_name_and_entry(|name, entry| { - builder.append_zone(name, entry) - })?; - } - ArtifactSource::Memory(builder.finish()?.into()) - } - }; - let data = ArtifactData { - name: data.name, - version: data.version, - source, - }; - Ok(data) - }) - .collect::>()?; - Ok((kind, entries)) - } - - /// Returns a fake manifest. Useful for testing. - pub fn new_fake() -> Self { - // The base directory doesn't matter for fake manifests. - Self::from_str(".".into(), FAKE_MANIFEST_TOML) - .expect("the fake manifest is a valid manifest") - } - - /// Checks that all expected artifacts are present, returning an error with - /// details if any artifacts are missing. - pub fn verify_all_present(&self) -> Result<()> { - let all_artifacts: BTreeSet<_> = KnownArtifactKind::iter() - .filter(|k| !matches!(k, KnownArtifactKind::Zone)) - .collect(); - let present_artifacts: BTreeSet<_> = - self.artifacts.keys().copied().collect(); - - let missing = &all_artifacts - &present_artifacts; - if !missing.is_empty() { - bail!( - "manifest has missing artifacts: {}", - itertools::join(missing, ", ") - ); - } - - Ok(()) - } -} - -#[derive(Debug)] -struct FakeDataAttributes<'a> { - kind: KnownArtifactKind, - version: &'a Version, -} - -impl<'a> FakeDataAttributes<'a> { - fn new(kind: KnownArtifactKind, version: &'a Version) -> Self { - Self { kind, version } - } - - fn make_data(&self, size: usize) -> Vec { - use hubtools::{CabooseBuilder, HubrisArchiveBuilder}; - - let board = match self.kind { - KnownArtifactKind::GimletRotBootloader - | KnownArtifactKind::PscRotBootloader - | KnownArtifactKind::SwitchRotBootloader => "SimRotStage0", - // non-Hubris artifacts: just make fake data - KnownArtifactKind::Host - | KnownArtifactKind::Trampoline - | KnownArtifactKind::ControlPlane - | KnownArtifactKind::Zone => return make_filler_text(size), - - // hubris artifacts: build a fake archive (SimGimletSp and - // SimGimletRot are used by sp-sim) - KnownArtifactKind::GimletSp => "SimGimletSp", - KnownArtifactKind::GimletRot => "SimRot", - KnownArtifactKind::PscSp => "fake-psc-sp", - KnownArtifactKind::PscRot => "fake-psc-rot", - KnownArtifactKind::SwitchSp => "SimSidecarSp", - KnownArtifactKind::SwitchRot => "SimRot", - }; - - // For our purposes sign = board represents what we want for the RoT - // and we don't care about the sign value for the SP - // We now have an assumption that board == name for our production - // images - let caboose = CabooseBuilder::default() - .git_commit("this-is-fake-data") - .board(board) - .version(self.version.to_string()) - .name(board) - .sign(board) - .build(); - - let mut builder = HubrisArchiveBuilder::with_fake_image(); - builder.write_caboose(caboose.as_slice()).unwrap(); - builder.build_to_vec().unwrap() - } -} - -/// Information about an individual artifact. -#[derive(Clone, Debug)] -pub struct ArtifactData { - pub name: String, - pub version: Version, - pub source: ArtifactSource, -} - -/// Deserializable version of [`ArtifactManifest`]. -/// -/// Since manifests require a base directory to be deserialized properly, -/// we don't expose the `Deserialize` impl on `ArtifactManifest, forcing -/// consumers to go through [`ArtifactManifest::from_path`] or -/// [`ArtifactManifest::from_str`]. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub struct DeserializedManifest { - pub system_version: Version, - #[serde(rename = "artifact")] - pub artifacts: BTreeMap>, -} - -impl DeserializedManifest { - pub fn from_path(path: &Utf8Path) -> Result { - let input = fs_err::read_to_string(path)?; - Self::from_str(&input).with_context(|| { - format!("error deserializing manifest from {path}") - }) - } - - pub fn from_str(input: &str) -> Result { - let de = toml::Deserializer::new(input); - serde_path_to_error::deserialize(de) - .context("error deserializing manifest") - } - - pub fn to_toml(&self) -> Result { - toml::to_string(self).context("error serializing manifest to TOML") - } - - /// For fake manifests, applies a set of changes to them. - /// - /// Intended for testing. - pub fn apply_tweaks(&mut self, tweaks: &[ManifestTweak]) -> Result<()> { - for tweak in tweaks { - match tweak { - ManifestTweak::SystemVersion(version) => { - self.system_version = version.clone(); - } - ManifestTweak::ArtifactVersion { kind, version } => { - let entries = - self.artifacts.get_mut(kind).with_context(|| { - format!( - "manifest does not have artifact kind \ - {kind}", - ) - })?; - for entry in entries { - entry.version = version.clone(); - } - } - ManifestTweak::ArtifactContents { kind, size_delta } => { - let entries = - self.artifacts.get_mut(kind).with_context(|| { - format!( - "manifest does not have artifact kind \ - {kind}", - ) - })?; - - for entry in entries { - entry.source.apply_size_delta(*size_delta)?; - } - } - } - } - - Ok(()) - } - - /// Returns the fake manifest. - pub fn fake() -> Self { - Self::from_str(FAKE_MANIFEST_TOML).unwrap() - } - - /// Returns a version of the fake manifest with a set of changes applied. - /// - /// This is primarily intended for testing. - pub fn tweaked_fake(tweaks: &[ManifestTweak]) -> Self { - let mut manifest = Self::fake(); - manifest - .apply_tweaks(tweaks) - .expect("builtin fake manifest should accept all tweaks"); - - manifest - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub struct DeserializedArtifactData { - pub name: String, - pub version: Version, - pub source: DeserializedArtifactSource, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(tag = "kind", rename_all = "kebab-case")] -pub enum DeserializedArtifactSource { - File { - path: Utf8PathBuf, - }, - Fake { - #[serde(deserialize_with = "deserialize_byte_size")] - size: u64, - }, - CompositeHost { - phase_1: DeserializedFileArtifactSource, - phase_2: DeserializedFileArtifactSource, - }, - CompositeRot { - archive_a: DeserializedFileArtifactSource, - archive_b: DeserializedFileArtifactSource, - }, - CompositeControlPlane { - zones: Vec, - }, -} - -impl DeserializedArtifactSource { - fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { - match self { - DeserializedArtifactSource::File { .. } => { - bail!("cannot apply size delta to `file` source") - } - DeserializedArtifactSource::Fake { size } => { - *size = (*size).saturating_add_signed(size_delta); - Ok(()) - } - DeserializedArtifactSource::CompositeHost { phase_1, phase_2 } => { - phase_1.apply_size_delta(size_delta)?; - phase_2.apply_size_delta(size_delta)?; - Ok(()) - } - DeserializedArtifactSource::CompositeRot { - archive_a, - archive_b, - } => { - archive_a.apply_size_delta(size_delta)?; - archive_b.apply_size_delta(size_delta)?; - Ok(()) - } - DeserializedArtifactSource::CompositeControlPlane { zones } => { - for zone in zones { - zone.apply_size_delta(size_delta)?; - } - Ok(()) - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum DeserializedFileArtifactSource { - File { - path: Utf8PathBuf, - }, - Fake { - #[serde(deserialize_with = "deserialize_byte_size")] - size: u64, - }, -} - -impl DeserializedFileArtifactSource { - fn is_fake(&self) -> bool { - matches!(self, DeserializedFileArtifactSource::Fake { .. }) - } - - fn with_entry(&self, fake_attr: FakeDataAttributes, f: F) -> Result - where - F: FnOnce(CompositeEntry<'_>) -> Result, - { - let (data, mtime_source) = match self { - DeserializedFileArtifactSource::File { path } => { - let data = std::fs::read(path) - .with_context(|| format!("failed to read {path}"))?; - // For now, always use the current time as the source. (Maybe - // change this to use the mtime on disk in the future?) - (data, MtimeSource::Now) - } - DeserializedFileArtifactSource::Fake { size } => { - (fake_attr.make_data(*size as usize), MtimeSource::Zero) - } - }; - let entry = CompositeEntry { data: &data, mtime_source }; - f(entry) - } - - fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { - match self { - DeserializedFileArtifactSource::File { .. } => { - bail!("cannot apply size delta to `file` source") - } - DeserializedFileArtifactSource::Fake { size } => { - *size = (*size).saturating_add_signed(size_delta); - Ok(()) - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum DeserializedControlPlaneZoneSource { - File { - path: Utf8PathBuf, - #[serde(skip_serializing_if = "Option::is_none")] - file_name: Option, - }, - Fake { - name: String, - #[serde(deserialize_with = "deserialize_byte_size")] - size: u64, - }, -} - -impl DeserializedControlPlaneZoneSource { - fn is_fake(&self) -> bool { - matches!(self, DeserializedControlPlaneZoneSource::Fake { .. }) - } - - fn with_name_and_entry(&self, f: F) -> Result - where - F: FnOnce(&str, CompositeEntry<'_>) -> Result, - { - let (name, data, mtime_source) = match self { - DeserializedControlPlaneZoneSource::File { path, file_name } => { - let data = std::fs::read(path) - .with_context(|| format!("failed to read {path}"))?; - let name = file_name - .as_deref() - .or_else(|| path.file_name()) - .with_context(|| { - format!("zone path missing file name: {path}") - })?; - // For now, always use the current time as the source. (Maybe - // change this to use the mtime on disk in the future?) - (name.to_owned(), data, MtimeSource::Now) - } - DeserializedControlPlaneZoneSource::Fake { name, size } => { - use flate2::{write::GzEncoder, Compression}; - use omicron_brand_metadata::{ - ArchiveType, LayerInfo, Metadata, - }; - - let mut tar = tar::Builder::new(GzEncoder::new( - Vec::new(), - Compression::fast(), - )); - - let metadata = Metadata::new(ArchiveType::Layer(LayerInfo { - pkg: name.clone(), - version: semver::Version::new(0, 0, 0), - })); - metadata.append_to_tar(&mut tar, 0)?; - - let mut h = tar::Header::new_ustar(); - h.set_entry_type(tar::EntryType::Regular); - h.set_path("fake")?; - h.set_mode(0o444); - h.set_size(*size); - h.set_mtime(0); - h.set_cksum(); - tar.append(&h, make_filler_text(*size as usize).as_slice())?; - - let data = tar.into_inner()?.finish()?; - (format!("{name}.tar.gz"), data, MtimeSource::Zero) - } - }; - let entry = CompositeEntry { data: &data, mtime_source }; - f(&name, entry) - } - - fn apply_size_delta(&mut self, size_delta: i64) -> Result<()> { - match self { - DeserializedControlPlaneZoneSource::File { .. } => { - bail!("cannot apply size delta to `file` source") - } - DeserializedControlPlaneZoneSource::Fake { size, .. } => { - (*size) = (*size).saturating_add_signed(size_delta); - Ok(()) - } - } - } -} -/// A change to apply to a manifest. -#[derive(Clone, Debug)] -pub enum ManifestTweak { - /// Update the system version. - SystemVersion(Version), - - /// Update the versions for this artifact. - ArtifactVersion { kind: KnownArtifactKind, version: Version }, - - /// Update the contents of this artifact (only support changing the size). - ArtifactContents { kind: KnownArtifactKind, size_delta: i64 }, -} - -fn deserialize_byte_size<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - // Attempt to deserialize the size as either a string or an integer. - - struct Visitor; - - impl serde::de::Visitor<'_> for Visitor { - type Value = u64; - - fn expecting( - &self, - formatter: &mut std::fmt::Formatter, - ) -> std::fmt::Result { - formatter - .write_str("a string representing a byte size or an integer") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - parse_size(value).map_err(|_| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Str(value), - &self, - ) - }) - } - - // TOML uses i64, not u64 - fn visit_i64(self, value: i64) -> Result - where - E: serde::de::Error, - { - Ok(value as u64) - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - Ok(value) - } - } - - deserializer.deserialize_any(Visitor) -} - -#[cfg(test)] -mod tests { - use super::*; - - // Ensure that the fake manifest roundtrips after serialization and - // deserialization. - #[test] - fn fake_roundtrip() { - let manifest = DeserializedManifest::fake(); - let toml = toml::to_string(&manifest).unwrap(); - let deserialized = DeserializedManifest::from_str(&toml) - .expect("fake manifest is a valid manifest"); - assert_eq!(manifest, deserialized); - } -} diff --git a/tufaceous-lib/src/assemble/mod.rs b/tufaceous-lib/src/assemble/mod.rs deleted file mode 100644 index f8b5b2f7314..00000000000 --- a/tufaceous-lib/src/assemble/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// 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/. - -mod build; -mod manifest; - -pub use build::*; -pub use manifest::*; diff --git a/tufaceous-lib/src/key.rs b/tufaceous-lib/src/key.rs deleted file mode 100644 index b5aa558df6b..00000000000 --- a/tufaceous-lib/src/key.rs +++ /dev/null @@ -1,79 +0,0 @@ -use anyhow::{bail, Result}; -use aws_lc_rs::rand::SystemRandom; -use aws_lc_rs::signature::Ed25519KeyPair; -use base64::{engine::general_purpose::URL_SAFE, Engine}; -use std::fmt::Display; -use std::str::FromStr; -use tough::async_trait; -use tough::key_source::KeySource; -use tough::sign::{Sign, SignKeyPair}; - -pub(crate) fn boxed_keys(keys: Vec) -> Vec> { - keys.into_iter().map(|k| Box::new(k) as Box).collect() -} - -#[derive(Debug, Clone)] -pub enum Key { - Ed25519 { pkcs8: Vec }, -} - -impl Key { - pub fn generate_ed25519() -> Result { - let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new())?; - Ok(Key::Ed25519 { pkcs8: pkcs8.as_ref().to_vec() }) - } - - fn as_sign_key_pair(&self) -> Result { - match self { - Key::Ed25519 { pkcs8 } => { - Ok(SignKeyPair::ED25519(Ed25519KeyPair::from_pkcs8(pkcs8)?)) - } - } - } - - pub(crate) fn as_tuf_key(&self) -> Result { - Ok(self.as_sign_key_pair()?.tuf_key()) - } -} - -#[async_trait] -impl KeySource for Key { - async fn as_sign( - &self, - ) -> Result, Box> - { - Ok(Box::new(self.as_sign_key_pair()?)) - } - - async fn write( - &self, - _value: &str, - _key_id_hex: &str, - ) -> Result<(), Box> { - Err("cannot write key back to key source".into()) - } -} - -impl FromStr for Key { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.split_once(':') { - Some(("ed25519", base64)) => { - Ok(Key::Ed25519 { pkcs8: URL_SAFE.decode(base64)? }) - } - Some((kind, _)) => bail!("Invalid key source kind: {}", kind), - None => bail!("Invalid key source (format is `kind:data`)"), - } - } -} - -impl Display for Key { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Key::Ed25519 { pkcs8 } => { - write!(f, "ed25519:{}", URL_SAFE.encode(pkcs8)) - } - } - } -} diff --git a/tufaceous-lib/src/lib.rs b/tufaceous-lib/src/lib.rs deleted file mode 100644 index bf6fd5e03a7..00000000000 --- a/tufaceous-lib/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -// 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/. - -mod archive; -mod artifact; -pub mod assemble; -mod key; -mod repository; -mod root; -mod target; - -pub use archive::*; -pub use artifact::*; -pub use key::*; -pub use repository::*; diff --git a/tufaceous-lib/src/repository.rs b/tufaceous-lib/src/repository.rs deleted file mode 100644 index c36413ffced..00000000000 --- a/tufaceous-lib/src/repository.rs +++ /dev/null @@ -1,415 +0,0 @@ -// 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/. - -use crate::{key::Key, target::TargetWriter, AddArtifact, ArchiveBuilder}; -use anyhow::{anyhow, bail, Context, Result}; -use buf_list::BufList; -use camino::{Utf8Path, Utf8PathBuf}; -use chrono::{DateTime, Utc}; -use fs_err::{self as fs}; -use futures::TryStreamExt; -use omicron_common::update::{Artifact, ArtifactsDocument}; -use semver::Version; -use std::{collections::BTreeSet, num::NonZeroU64}; -use tough::{ - editor::{signed::SignedRole, RepositoryEditor}, - schema::{Root, Target}, - ExpirationEnforcement, Repository, RepositoryLoader, TargetName, -}; -use url::Url; - -/// A TUF repository describing Omicron. -pub struct OmicronRepo { - log: slog::Logger, - repo: Repository, - repo_path: Utf8PathBuf, -} - -impl OmicronRepo { - /// Initializes a new repository at the given path, writing it to disk. - pub async fn initialize( - log: &slog::Logger, - repo_path: &Utf8Path, - system_version: Version, - keys: Vec, - expiry: DateTime, - ) -> Result { - let root = crate::root::new_root(keys.clone(), expiry).await?; - let editor = OmicronRepoEditor::initialize( - repo_path.to_owned(), - root, - system_version, - ) - .await?; - - editor - .sign_and_finish(keys, expiry) - .await - .context("error signing new repository")?; - - // In theory we "trust" the key we just used to sign this repository, - // but the code path is equivalent to `load_untrusted`. - Self::load_untrusted(log, repo_path).await - } - - /// Loads a repository from the given path. - /// - /// This method enforces expirations. To load without expiration enforcement, use - /// [`Self::load_untrusted_ignore_expiration`]. - pub async fn load_untrusted( - log: &slog::Logger, - repo_path: &Utf8Path, - ) -> Result { - Self::load_untrusted_impl(log, repo_path, ExpirationEnforcement::Safe) - .await - } - - /// Loads a repository from the given path, ignoring expiration. - /// - /// Use cases for this include: - /// - /// 1. When you're editing an existing repository and will re-sign it afterwards. - /// 2. In an environment in which time isn't available. - pub async fn load_untrusted_ignore_expiration( - log: &slog::Logger, - repo_path: &Utf8Path, - ) -> Result { - Self::load_untrusted_impl(log, repo_path, ExpirationEnforcement::Unsafe) - .await - } - - async fn load_untrusted_impl( - log: &slog::Logger, - repo_path: &Utf8Path, - exp: ExpirationEnforcement, - ) -> Result { - let log = log.new(slog::o!("component" => "OmicronRepo")); - let repo_path = repo_path.canonicalize_utf8()?; - let root_json = repo_path.join("metadata").join("1.root.json"); - let root = tokio::fs::read(&root_json) - .await - .with_context(|| format!("error reading from {root_json}"))?; - - let repo = RepositoryLoader::new( - &root, - Url::from_file_path(repo_path.join("metadata")) - .expect("the canonical path is not absolute?"), - Url::from_file_path(repo_path.join("targets")) - .expect("the canonical path is not absolute?"), - ) - .expiration_enforcement(exp) - .load() - .await?; - - Ok(Self { log, repo, repo_path }) - } - - /// Returns a canonicalized form of the repository path. - pub fn repo_path(&self) -> &Utf8Path { - &self.repo_path - } - - /// Returns the repository. - pub fn repo(&self) -> &Repository { - &self.repo - } - - /// Reads the artifacts document from the repo. - pub async fn read_artifacts(&self) -> Result { - let reader = self - .repo - .read_target(&"artifacts.json".try_into()?) - .await? - .ok_or_else(|| anyhow!("artifacts.json should be present"))?; - let buf_list = reader - .try_collect::() - .await - .context("error reading from artifacts.json")?; - serde_json::from_reader(buf_list::Cursor::new(&buf_list)) - .context("error deserializing artifacts.json") - } - - /// Archives the repository to the given path as a zip file. - /// - /// ## Why zip and not tar? - /// - /// The main reason is that zip supports random access to files and tar does - /// not. - /// - /// In principle it should be possible to read the repository out of a zip - /// file from memory, but we ran into [this - /// issue](https://github.com/awslabs/tough/pull/563) while implementing it. - /// Once that is resolved (or we write our own TUF crate) it should be - /// possible to do that. - /// - /// Regardless of this roadblock, we don't want to foreclose that option - /// forever, so this code uses zip rather than having to deal with a - /// migration in the future. - pub fn archive(&self, output_path: &Utf8Path) -> Result<()> { - let mut builder = ArchiveBuilder::new(output_path.to_owned())?; - - let metadata_dir = self.repo_path.join("metadata"); - - // Gather metadata files. - for entry in metadata_dir.read_dir_utf8().with_context(|| { - format!("error reading entries from {metadata_dir}") - })? { - let entry = - entry.context("error reading entry from {metadata_dir}")?; - let file_name = entry.file_name(); - if file_name.ends_with(".root.json") - || file_name == "timestamp.json" - || file_name.ends_with(".snapshot.json") - || file_name.ends_with(".targets.json") - { - // This is a valid metadata file. - builder.write_file( - entry.path(), - &Utf8Path::new("metadata").join(file_name), - )?; - } - } - - let targets_dir = self.repo_path.join("targets"); - - // Gather all targets. - for (name, target) in self.repo.targets().signed.targets_iter() { - let target_filename = self.target_filename(target, name); - let target_path = targets_dir.join(&target_filename); - slog::trace!(self.log, "adding {} to archive", name.resolved()); - builder.write_file( - &target_path, - &Utf8Path::new("targets").join(&target_filename), - )?; - } - - builder.finish()?; - - Ok(()) - } - - /// Converts `self` into an `OmicronRepoEditor`, which can be used to perform - /// modifications to the repository. - pub async fn into_editor(self) -> Result { - OmicronRepoEditor::new(self).await - } - - /// Prepends the target digest to the name if using consistent snapshots. Returns both the - /// digest and the filename. - /// - /// Adapted from tough's source. - fn target_filename(&self, target: &Target, name: &TargetName) -> String { - let sha256 = &target.hashes.sha256.clone().into_vec(); - if self.repo.root().signed.consistent_snapshot { - format!("{}.{}", hex::encode(sha256), name.resolved()) - } else { - name.resolved().to_owned() - } - } -} - -/// An [`OmicronRepo`] than can be edited. -/// -/// Created by [`OmicronRepo::into_editor`]. -pub struct OmicronRepoEditor { - editor: RepositoryEditor, - repo_path: Utf8PathBuf, - artifacts: ArtifactsDocument, - - // Set of `TargetName::resolved()` names for every target that existed when - // the repo was opened. We use this to ensure we don't overwrite an existing - // target when adding new artifacts. - existing_target_names: BTreeSet, -} - -impl OmicronRepoEditor { - async fn new(repo: OmicronRepo) -> Result { - let artifacts = repo.read_artifacts().await?; - - let existing_target_names = repo - .repo - .targets() - .signed - .targets_iter() - .map(|(name, _)| name.resolved().to_string()) - .collect::>(); - - let editor = RepositoryEditor::from_repo( - repo.repo_path - .join("metadata") - .join(format!("{}.root.json", repo.repo.root().signed.version)), - repo.repo, - ) - .await?; - - Ok(Self { - editor, - repo_path: repo.repo_path, - artifacts, - existing_target_names, - }) - } - - async fn initialize( - repo_path: Utf8PathBuf, - root: SignedRole, - system_version: Version, - ) -> Result { - let metadata_dir = repo_path.join("metadata"); - let targets_dir = repo_path.join("targets"); - let root_path = metadata_dir - .join(format!("{}.root.json", root.signed().signed.version)); - - fs::create_dir_all(&metadata_dir)?; - fs::create_dir_all(&targets_dir)?; - fs::write(&root_path, root.buffer())?; - - let editor = RepositoryEditor::new(&root_path).await?; - - Ok(Self { - editor, - repo_path, - artifacts: ArtifactsDocument::empty(system_version), - existing_target_names: BTreeSet::new(), - }) - } - - /// Adds an artifact to the repository. - pub fn add_artifact(&mut self, new_artifact: &AddArtifact) -> Result<()> { - let target_name = format!( - "{}-{}-{}.tar.gz", - new_artifact.kind(), - new_artifact.name(), - new_artifact.version(), - ); - - // make sure we're not overwriting an existing target (either one that - // existed when we opened the repo, or one that's been added via this - // method) - if !self.existing_target_names.insert(target_name.clone()) { - bail!( - "a target named {target_name} already exists in the repository", - ); - } - - self.artifacts.artifacts.push(Artifact { - name: new_artifact.name().to_owned(), - version: new_artifact.version().to_owned(), - kind: new_artifact.kind().clone(), - target: target_name.clone(), - }); - - let targets_dir = self.repo_path.join("targets"); - - let mut file = TargetWriter::new(&targets_dir, target_name.clone())?; - new_artifact.write_to(&mut file).with_context(|| { - format!("error writing artifact `{target_name}") - })?; - file.finish(&mut self.editor)?; - - Ok(()) - } - - /// Consumes self, signing the repository and writing out this repository to disk. - pub async fn sign_and_finish( - mut self, - keys: Vec, - expiry: DateTime, - ) -> Result<()> { - let targets_dir = self.repo_path.join("targets"); - - let mut file = TargetWriter::new(&targets_dir, "artifacts.json")?; - serde_json::to_writer_pretty(&mut file, &self.artifacts)?; - file.finish(&mut self.editor)?; - - update_versions(&mut self.editor, expiry)?; - - let signed = self - .editor - .sign(&crate::key::boxed_keys(keys)) - .await - .context("error signing keys")?; - signed - .write(self.repo_path.join("metadata")) - .await - .context("error writing repository")?; - Ok(()) - } -} - -fn update_versions( - editor: &mut RepositoryEditor, - expiry: DateTime, -) -> Result<()> { - let version = u64::try_from(Utc::now().timestamp()) - .and_then(NonZeroU64::try_from) - .expect("bad epoch"); - editor.snapshot_version(version); - editor.targets_version(version)?; - editor.timestamp_version(version); - editor.snapshot_expires(expiry); - editor.targets_expires(expiry)?; - editor.timestamp_expires(expiry); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ArtifactSource; - use buf_list::BufList; - use camino_tempfile::Utf8TempDir; - use chrono::Days; - use omicron_test_utils::dev::test_setup_log; - - #[tokio::test] - async fn reject_artifacts_with_the_same_filename() { - let logctx = test_setup_log("reject_artifacts_with_the_same_filename"); - let tempdir = Utf8TempDir::new().unwrap(); - let mut repo = OmicronRepo::initialize( - &logctx.log, - tempdir.path(), - "0.0.0".parse().unwrap(), - vec![Key::generate_ed25519().unwrap()], - Utc::now() + Days::new(1), - ) - .await - .unwrap() - .into_editor() - .await - .unwrap(); - - // Targets are uniquely identified by their kind/name/version triple; - // trying to add two artifacts with identical triples should fail. - let kind = "test-kind"; - let name = "test-artifact-name"; - let version = "1.0.0"; - - repo.add_artifact(&AddArtifact::new( - kind.parse().unwrap(), - name.to_string(), - version.parse().unwrap(), - ArtifactSource::Memory(BufList::new()), - )) - .unwrap(); - - let err = repo - .add_artifact(&AddArtifact::new( - kind.parse().unwrap(), - name.to_string(), - version.parse().unwrap(), - ArtifactSource::Memory(BufList::new()), - )) - .unwrap_err() - .to_string(); - - assert!(err.contains("a target named")); - assert!(err.contains(kind)); - assert!(err.contains(name)); - assert!(err.contains(version)); - assert!(err.contains("already exists")); - - logctx.cleanup_successful(); - } -} diff --git a/tufaceous-lib/src/root.rs b/tufaceous-lib/src/root.rs deleted file mode 100644 index f8fc2d0c067..00000000000 --- a/tufaceous-lib/src/root.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::key::Key; -use anyhow::Result; -use aws_lc_rs::rand::SystemRandom; -use chrono::{DateTime, Utc}; -use std::collections::HashMap; -use std::num::NonZeroU64; -use tough::editor::signed::SignedRole; -use tough::schema::{KeyHolder, RoleKeys, RoleType, Root}; - -pub(crate) async fn new_root( - keys: Vec, - expires: DateTime, -) -> Result> { - let mut root = Root { - spec_version: "1.0.0".to_string(), - consistent_snapshot: true, - version: NonZeroU64::new(1).unwrap(), - expires, - keys: HashMap::new(), - roles: HashMap::new(), - _extra: HashMap::new(), - }; - for key in &keys { - let key = key.as_tuf_key()?; - root.keys.insert(key.key_id()?, key); - } - for kind in [ - RoleType::Root, - RoleType::Snapshot, - RoleType::Targets, - RoleType::Timestamp, - ] { - root.roles.insert( - kind, - RoleKeys { - keyids: root.keys.keys().cloned().collect(), - threshold: NonZeroU64::new(1).unwrap(), - _extra: HashMap::new(), - }, - ); - } - - let keys = crate::key::boxed_keys(keys); - Ok(SignedRole::new( - root.clone(), - &KeyHolder::Root(root), - &keys, - &SystemRandom::new(), - ) - .await?) -} diff --git a/tufaceous-lib/src/target.rs b/tufaceous-lib/src/target.rs deleted file mode 100644 index 7450913c945..00000000000 --- a/tufaceous-lib/src/target.rs +++ /dev/null @@ -1,67 +0,0 @@ -use anyhow::Result; -use camino::Utf8PathBuf; -use camino_tempfile::NamedUtf8TempFile; -use sha2::{Digest, Sha256}; -use std::collections::HashMap; -use std::io::Write; -use tough::editor::RepositoryEditor; -use tough::schema::{Hashes, Target}; - -pub(crate) struct TargetWriter { - file: NamedUtf8TempFile, - targets_dir: Utf8PathBuf, - name: String, - length: u64, - hasher: Sha256, -} - -impl TargetWriter { - pub(crate) fn new( - targets_dir: impl Into, - name: impl Into, - ) -> Result { - let targets_dir = targets_dir.into(); - Ok(TargetWriter { - file: NamedUtf8TempFile::new_in(&targets_dir)?, - targets_dir, - name: name.into(), - length: 0, - hasher: Sha256::default(), - }) - } - - pub(crate) fn finish(self, editor: &mut RepositoryEditor) -> Result<()> { - let digest = self.hasher.finalize(); - self.file.persist(self.targets_dir.join(format!( - "{}.{}", - hex::encode(digest), - self.name - )))?; - editor.add_target( - self.name, - Target { - length: self.length, - hashes: Hashes { - sha256: digest.to_vec().into(), - _extra: HashMap::new(), - }, - custom: HashMap::new(), - _extra: HashMap::new(), - }, - )?; - Ok(()) - } -} - -impl Write for TargetWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let n = self.file.write(buf)?; - self.length += u64::try_from(n).unwrap(); - self.hasher.update(&buf[..n]); - Ok(n) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.file.flush() - } -} diff --git a/tufaceous/.gitignore b/tufaceous/.gitignore deleted file mode 100644 index 64bcebfd9d8..00000000000 --- a/tufaceous/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/repo diff --git a/tufaceous/Cargo.toml b/tufaceous/Cargo.toml deleted file mode 100644 index 37d3974aff8..00000000000 --- a/tufaceous/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "tufaceous" -version = "0.1.0" -edition = "2021" -license = "MPL-2.0" -publish = false - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true, features = ["backtrace"] } -camino.workspace = true -clap = { workspace = true, features = ["derive", "env"] } -chrono.workspace = true -console = { version = "0.15.10", default-features = false } -humantime.workspace = true -omicron-common.workspace = true -slog.workspace = true -slog-async.workspace = true -slog-envlogger.workspace = true -slog-term.workspace = true -tokio.workspace = true -tufaceous-lib.workspace = true -omicron-workspace-hack.workspace = true -semver.workspace = true - -[dev-dependencies] -assert_cmd.workspace = true -datatest-stable.workspace = true -fs-err.workspace = true -omicron-test-utils.workspace = true -predicates.workspace = true -tempfile.workspace = true -tokio = { workspace = true, features = ["test-util"] } - -[[test]] -name = "manifest-tests" -harness = false diff --git a/tufaceous/README.adoc b/tufaceous/README.adoc deleted file mode 100644 index 86e4cfc4e53..00000000000 --- a/tufaceous/README.adoc +++ /dev/null @@ -1,36 +0,0 @@ -# tufaceous - -Rack update repository generation tool. - -## TUF, keys and lifetime - -Rack update repositories use TUF. Consider reading https://theupdateframework.io/overview/[the TUF overview] and https://theupdateframework.io/metadata/[a high level summary of the metadata mandated by the specification]. - -The only keys currently supported by tufaceous are Ed25519 keys. Support for hardware-backed keys is planned. - -Each role has an expiration date. The default is one week, suitable for development testing. This can be modified with the `--expiry` option. - -## init - -Create a new repository in the current directory with `tufaceous init`. - -To change the target directory, use `-r/--repo`. This is accepted on all subcommands, and needs to come before the subcommand because Clap is picky. - -This will generate a new Ed25519 private key and display it on stderr if no keys are provided. - -Currently if keys are provided, they are allowed to sign all roles. For the time being if you need more advanced editing of the root role, use https://crates.io/crates/tuftool[tuftool]'s `root` subcommands. - -## add zones - -Usage: - ----- -tuftool [-r PATH/TO/REPO] add-zone [--name NAME] ZONE_TAR_GZ VERSION ----- - -Example: - ----- -$ tuftool add-zone out/nexus.tar.gz 0.0.0 -added zone nexus, version 0.0.0 ----- diff --git a/tufaceous/src/date.rs b/tufaceous/src/date.rs deleted file mode 100644 index 4d905df3614..00000000000 --- a/tufaceous/src/date.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Result; -use chrono::{DateTime, Duration, Timelike, Utc}; - -/// Parser for datelike command line arguments. Can accept a duration (e.g. -/// "1w") or an ISO8601 timestamp. -pub(crate) fn parse_duration_or_datetime(s: &str) -> Result> { - match humantime::parse_duration(s) { - Ok(duration) => { - // Remove nanoseconds from the timestamp to keep it less - // overwhelming. `Timelike::with_nanosecond` returns None only when - // passed a value over 2 billion - let now = Utc::now().with_nanosecond(0).unwrap(); - Ok(now + Duration::from_std(duration)?) - } - Err(_) => Ok(s.parse()?), - } -} diff --git a/tufaceous/src/dispatch.rs b/tufaceous/src/dispatch.rs deleted file mode 100644 index fc8281b511f..00000000000 --- a/tufaceous/src/dispatch.rs +++ /dev/null @@ -1,260 +0,0 @@ -// 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/. - -use anyhow::{bail, Context, Result}; -use camino::Utf8PathBuf; -use chrono::{DateTime, Utc}; -use clap::{CommandFactory, Parser}; -use omicron_common::update::ArtifactKind; -use semver::Version; -use tufaceous_lib::{ - assemble::{ArtifactManifest, OmicronRepoAssembler}, - AddArtifact, ArchiveExtractor, Key, OmicronRepo, -}; - -#[derive(Debug, Parser)] -pub struct Args { - #[clap(subcommand)] - command: Command, - - #[clap( - short = 'k', - long = "key", - env = "TUFACEOUS_KEY", - required = false, - global = true - )] - keys: Vec, - - #[clap(long, value_parser = crate::date::parse_duration_or_datetime, default_value = "7d", global = true)] - expiry: DateTime, - - /// TUF repository path (default: current working directory) - #[clap(short = 'r', long, global = true)] - repo: Option, -} - -impl Args { - /// Executes these arguments. - pub async fn exec(self, log: &slog::Logger) -> Result<()> { - let repo_path = match self.repo { - Some(repo) => repo, - None => std::env::current_dir()?.try_into()?, - }; - - match self.command { - Command::Init { system_version, no_generate_key } => { - let keys = maybe_generate_keys(self.keys, no_generate_key)?; - - let repo = OmicronRepo::initialize( - &log, - &repo_path, - system_version, - keys, - self.expiry, - ) - .await?; - slog::info!( - log, - "Initialized TUF repository in {}", - repo.repo_path() - ); - Ok(()) - } - Command::Add { kind, allow_unknown_kinds, path, name, version } => { - if !allow_unknown_kinds { - // Try converting kind to a known kind. - if kind.to_known().is_none() { - // Simulate a failure to parse (though ideally there would - // be a way to also specify the underlying error -- there - // doesn't appear to be a public API to do so in clap 4). - let mut error = clap::Error::new( - clap::error::ErrorKind::ValueValidation, - ) - .with_cmd(&Args::command()); - error.insert( - clap::error::ContextKind::InvalidArg, - clap::error::ContextValue::String( - "".to_owned(), - ), - ); - error.insert( - clap::error::ContextKind::InvalidValue, - clap::error::ContextValue::String(kind.to_string()), - ); - error.exit(); - } - } - - let repo = OmicronRepo::load_untrusted_ignore_expiration( - &log, &repo_path, - ) - .await?; - let mut editor = repo.into_editor().await?; - - let new_artifact = - AddArtifact::from_path(kind, name, version, path)?; - - editor - .add_artifact(&new_artifact) - .context("error adding artifact")?; - editor.sign_and_finish(self.keys, self.expiry).await?; - println!( - "added {} {}, version {}", - new_artifact.kind(), - new_artifact.name(), - new_artifact.version() - ); - Ok(()) - } - Command::Archive { output_path } => { - // The filename must end with "zip". - if output_path.extension() != Some("zip") { - bail!("output path `{output_path}` must end with .zip"); - } - - let repo = OmicronRepo::load_untrusted_ignore_expiration( - &log, &repo_path, - ) - .await?; - repo.archive(&output_path)?; - - Ok(()) - } - Command::Extract { archive_file, dest } => { - let mut extractor = ArchiveExtractor::from_path(&archive_file)?; - extractor.extract(&dest)?; - - // Now load the repository and ensure it's valid. - let repo = OmicronRepo::load_untrusted(&log, &dest) - .await - .with_context(|| { - format!( - "error loading extracted repository at `{dest}` \ - (extracted files are still available)" - ) - })?; - repo.read_artifacts().await.with_context(|| { - format!( - "error loading artifacts.json from extracted archive \ - at `{dest}`" - ) - })?; - - Ok(()) - } - Command::Assemble { - manifest_path, - output_path, - build_dir, - no_generate_key, - skip_all_present, - } => { - // The filename must end with "zip". - if output_path.extension() != Some("zip") { - bail!("output path `{output_path}` must end with .zip"); - } - - let manifest = ArtifactManifest::from_path(&manifest_path) - .context("error reading manifest")?; - if !skip_all_present { - manifest.verify_all_present()?; - } - - let keys = maybe_generate_keys(self.keys, no_generate_key)?; - let mut assembler = OmicronRepoAssembler::new( - &log, - manifest, - keys, - self.expiry, - output_path, - ); - if let Some(dir) = build_dir { - assembler.set_build_dir(dir); - } - - assembler.build().await?; - - Ok(()) - } - } - } -} - -#[derive(Debug, Parser)] -enum Command { - /// Create a new rack update TUF repository - Init { - /// The system version. - system_version: Version, - - /// Disable random key generation and exit if no keys are provided - #[clap(long)] - no_generate_key: bool, - }, - Add { - /// The kind of artifact this is. - kind: ArtifactKind, - - /// Allow artifact kinds that aren't known to tufaceous - #[clap(long)] - allow_unknown_kinds: bool, - - /// Path to the artifact. - path: Utf8PathBuf, - - /// Override the name for this artifact (default: filename with extension stripped) - #[clap(long)] - name: Option, - - /// Artifact version. - version: Version, - }, - /// Archives this repository to a zip file. - Archive { - /// The path to write the archive to (must end with .zip). - output_path: Utf8PathBuf, - }, - /// Validates and extracts a repository created by the `archive` command. - Extract { - /// The file to extract. - archive_file: Utf8PathBuf, - - /// The destination to extract the file to. - dest: Utf8PathBuf, - }, - /// Assembles a repository from a provided manifest. - Assemble { - /// Path to artifact manifest. - manifest_path: Utf8PathBuf, - - /// The path to write the archive to (must end with .zip). - output_path: Utf8PathBuf, - - /// Directory to use for building artifacts [default: temporary directory] - #[clap(long)] - build_dir: Option, - - /// Disable random key generation and exit if no keys are provided - #[clap(long)] - no_generate_key: bool, - - /// Skip checking to ensure all expected artifacts are present. - #[clap(long)] - skip_all_present: bool, - }, -} - -fn maybe_generate_keys( - keys: Vec, - no_generate_key: bool, -) -> Result> { - Ok(if !no_generate_key && keys.is_empty() { - let key = Key::generate_ed25519()?; - crate::hint::generated_key(&key); - vec![key] - } else { - keys - }) -} diff --git a/tufaceous/src/hint.rs b/tufaceous/src/hint.rs deleted file mode 100644 index 743082bd7a1..00000000000 --- a/tufaceous/src/hint.rs +++ /dev/null @@ -1,25 +0,0 @@ -use tufaceous_lib::Key; - -fn print_hint(hint: &str) { - for line in hint.trim().lines() { - eprintln!("{}", console::style(format!("hint: {}", line)).yellow()); - } -} - -pub(crate) fn generated_key(key: &Key) { - print_hint(&format!( - r#" -Generated a random key: - - {key} - -To modify this repository, you will need this key. Use the -k/--key -command line flag or the TUFACEOUS_KEY environment variable: - - export TUFACEOUS_KEY={key} - -To prevent this default behavior, use --no-generate-key. - "#, - key = console::style(key).italic() - )) -} diff --git a/tufaceous/src/lib.rs b/tufaceous/src/lib.rs deleted file mode 100644 index 65ff581a00c..00000000000 --- a/tufaceous/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -// 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/. - -mod date; -mod dispatch; -mod hint; - -pub use dispatch::*; diff --git a/tufaceous/src/main.rs b/tufaceous/src/main.rs deleted file mode 100644 index 014817ee53b..00000000000 --- a/tufaceous/src/main.rs +++ /dev/null @@ -1,35 +0,0 @@ -// 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/. - -use anyhow::Result; -use clap::Parser; -use slog::Drain; -use tufaceous::Args; - -#[tokio::main] -async fn main() -> Result<()> { - let log = setup_log(); - let args = Args::parse(); - args.exec(&log).await -} - -fn setup_log() -> slog::Logger { - let stderr_drain = stderr_env_drain("RUST_LOG"); - let drain = slog_async::Async::new(stderr_drain).build().fuse(); - slog::Logger::root(drain, slog::o!()) -} - -fn stderr_env_drain(env_var: &str) -> impl Drain { - let stderr_decorator = slog_term::TermDecorator::new().build(); - let stderr_drain = - slog_term::FullFormat::new(stderr_decorator).build().fuse(); - let mut builder = slog_envlogger::LogBuilder::new(stderr_drain); - if let Ok(s) = std::env::var(env_var) { - builder = builder.parse(&s); - } else { - // Log at the info level by default. - builder = builder.filter(None, slog::FilterLevel::Info); - } - builder.build() -} diff --git a/tufaceous/tests/integration-tests/command_tests.rs b/tufaceous/tests/integration-tests/command_tests.rs deleted file mode 100644 index fcdaf949e6a..00000000000 --- a/tufaceous/tests/integration-tests/command_tests.rs +++ /dev/null @@ -1,152 +0,0 @@ -// 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/. - -use std::path::Path; - -use anyhow::Result; -use assert_cmd::Command; -use camino::Utf8PathBuf; -use omicron_common::{ - api::internal::nexus::KnownArtifactKind, update::ArtifactKind, -}; -use omicron_test_utils::dev::test_setup_log; -use predicates::prelude::*; -use tufaceous_lib::{Key, OmicronRepo}; - -#[tokio::test] -async fn test_init_and_add() -> Result<()> { - let logctx = test_setup_log("test_init_and_add"); - let tempdir = tempfile::tempdir().unwrap(); - let key = Key::generate_ed25519()?; - - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["init", "0.0.0"]); - cmd.assert().success(); - - // Create a couple of stub files on disk. - let nexus_path = tempdir.path().join("nexus.tar.gz"); - fs_err::write(&nexus_path, "test")?; - let unknown_path = tempdir.path().join("my-unknown-kind.tar.gz"); - fs_err::write(&unknown_path, "unknown test")?; - - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "gimlet_sp"]); - cmd.arg(&nexus_path); - cmd.arg("42.0.0"); - cmd.assert().success(); - - // Try adding an unknown kind without --allow-unknown-kinds. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "my_unknown_kind"]); - cmd.arg(&nexus_path); - cmd.arg("0.0.0"); - cmd.assert().failure().stderr(predicate::str::contains( - "invalid value 'my_unknown_kind' for ''", - )); - - // Try adding one with --allow-unknown-kinds. - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.args(["add", "my_unknown_kind", "--allow-unknown-kinds"]); - cmd.arg(&unknown_path); - cmd.arg("0.1.0"); - cmd.assert().success(); - - // Now read the repository and ensure the list of expected artifacts. - let repo_path: Utf8PathBuf = tempdir.path().join("repo").try_into()?; - let repo = OmicronRepo::load_untrusted(&logctx.log, &repo_path).await?; - - let artifacts = repo.read_artifacts().await?; - assert_eq!( - artifacts.artifacts.len(), - 2, - "repo should contain exactly 2 artifacts: {artifacts:?}" - ); - - let mut artifacts_iter = artifacts.artifacts.into_iter(); - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "nexus", "artifact name"); - assert_eq!(artifact.version, "42.0.0".parse().unwrap(), "artifact version"); - assert_eq!( - artifact.kind, - ArtifactKind::from_known(KnownArtifactKind::GimletSp), - "artifact kind" - ); - assert_eq!( - artifact.target, "gimlet_sp-nexus-42.0.0.tar.gz", - "artifact target" - ); - - let artifact = artifacts_iter.next().unwrap(); - assert_eq!(artifact.name, "my-unknown-kind", "artifact name"); - assert_eq!(artifact.version, "0.1.0".parse().unwrap(), "artifact version"); - assert_eq!( - artifact.kind, - ArtifactKind::new("my_unknown_kind".to_owned()), - "artifact kind" - ); - assert_eq!( - artifact.target, "my_unknown_kind-my-unknown-kind-0.1.0.tar.gz", - "artifact target" - ); - - // Create an archive from the given path. - let archive_path = tempdir.path().join("archive.zip"); - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.arg("archive"); - cmd.arg(&archive_path); - cmd.assert().success(); - - // Extract the archive to a new directory. - let dest_path = tempdir.path().join("dest"); - let mut cmd = make_cmd_with_repo(tempdir.path(), &key); - cmd.arg("extract"); - cmd.arg(&archive_path); - cmd.arg(&dest_path); - - cmd.assert().success(); - - logctx.cleanup_successful(); - Ok(()) -} - -#[test] -fn test_assemble_fake() -> Result<()> { - let logctx = test_setup_log("test_assemble_fake"); - let tempdir = tempfile::tempdir().unwrap(); - let key = Key::generate_ed25519()?; - - let archive_path = tempdir.path().join("archive.zip"); - - let mut cmd = make_cmd(&key); - cmd.args(["assemble", "manifests/fake.toml"]); - cmd.arg(&archive_path); - cmd.assert().success(); - - // Extract the archive to a new directory. - let dest_path = tempdir.path().join("dest"); - let mut cmd = make_cmd(&key); - cmd.arg("extract"); - cmd.arg(&archive_path); - cmd.arg(&dest_path); - - cmd.assert().success(); - - logctx.cleanup_successful(); - Ok(()) -} - -fn make_cmd(key: &Key) -> Command { - let mut cmd = Command::cargo_bin("tufaceous").unwrap(); - cmd.env("TUFACEOUS_KEY", key.to_string()); - - cmd -} - -fn make_cmd_with_repo(tempdir: &Path, key: &Key) -> Command { - let mut cmd = make_cmd(key); - cmd.arg("--repo"); - cmd.arg(tempdir.join("repo")); - - cmd -} diff --git a/tufaceous/tests/integration-tests/main.rs b/tufaceous/tests/integration-tests/main.rs deleted file mode 100644 index fdf11b8cd75..00000000000 --- a/tufaceous/tests/integration-tests/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -// 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/. - -mod command_tests; diff --git a/tufaceous/tests/manifest-tests.rs b/tufaceous/tests/manifest-tests.rs deleted file mode 100644 index e099f31a92d..00000000000 --- a/tufaceous/tests/manifest-tests.rs +++ /dev/null @@ -1,18 +0,0 @@ -// 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/. - -use std::path::Path; - -use tufaceous_lib::assemble::ArtifactManifest; - -fn check_manifest(path: &Path) -> datatest_stable::Result<()> { - let path = path.try_into()?; - let manifest = - ArtifactManifest::from_path(path).expect("failed to load manifest"); - manifest.verify_all_present()?; - - Ok(()) -} - -datatest_stable::harness!(check_manifest, "manifests", r"^.*\.toml"); diff --git a/update-common/Cargo.toml b/update-common/Cargo.toml index fde6cb2e471..490de6fb7ae 100644 --- a/update-common/Cargo.toml +++ b/update-common/Cargo.toml @@ -28,11 +28,12 @@ tokio-util.workspace = true tough.workspace = true tufaceous-lib.workspace = true omicron-workspace-hack.workspace = true -omicron-brand-metadata.workspace = true +tufaceous-brand-metadata.workspace = true tar.workspace = true flate2.workspace = true fs-err = { workspace = true, features = ["tokio"] } semver.workspace = true +tufaceous-artifact.workspace = true [dev-dependencies] clap.workspace = true diff --git a/tufaceous/manifests/fake.toml b/update-common/manifests/fake.toml similarity index 100% rename from tufaceous/manifests/fake.toml rename to update-common/manifests/fake.toml diff --git a/update-common/src/artifacts/artifacts_with_plan.rs b/update-common/src/artifacts/artifacts_with_plan.rs index 2a7a27a9f66..0bdffae04a6 100644 --- a/update-common/src/artifacts/artifacts_with_plan.rs +++ b/update-common/src/artifacts/artifacts_with_plan.rs @@ -244,7 +244,7 @@ impl ArtifactsWithPlan { })?; builder - .add_artifact(artifact.clone().into_id(), artifact_hash, stream) + .add_artifact(artifact.clone().into(), artifact_hash, stream) .await?; } @@ -361,11 +361,9 @@ mod tests { use camino::Utf8Path; use camino_tempfile::Utf8TempDir; use clap::Parser; - use omicron_common::{ - api::internal::nexus::KnownArtifactKind, update::ArtifactKind, - }; use omicron_test_utils::dev::test_setup_log; use std::{collections::BTreeSet, time::Duration}; + use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; /// Test that `ArtifactsWithPlan` can extract the fake repository generated /// by tufaceous. @@ -552,7 +550,7 @@ mod tests { let args = tufaceous::Args::try_parse_from([ "tufaceous", "assemble", - "../tufaceous/manifests/fake.toml", + "manifests/fake.toml", archive_path.as_str(), ]) .context("error parsing args")?; diff --git a/update-common/src/artifacts/extracted_artifacts.rs b/update-common/src/artifacts/extracted_artifacts.rs index dd5b1edc8ba..33abc195cf7 100644 --- a/update-common/src/artifacts/extracted_artifacts.rs +++ b/update-common/src/artifacts/extracted_artifacts.rs @@ -11,7 +11,6 @@ use futures::Stream; use futures::StreamExt; use omicron_common::update::ArtifactHash; use omicron_common::update::ArtifactHashId; -use omicron_common::update::ArtifactKind; use sha2::Digest; use sha2::Sha256; use slog::info; @@ -22,6 +21,7 @@ use std::sync::Arc; use tokio::io::AsyncRead; use tokio::io::AsyncWriteExt; use tokio_util::io::ReaderStream; +use tufaceous_artifact::ArtifactKind; /// Handle to the data of an extracted artifact. /// diff --git a/update-common/src/artifacts/update_plan.rs b/update-common/src/artifacts/update_plan.rs index 062d6cd1845..f2344a823c7 100644 --- a/update-common/src/artifacts/update_plan.rs +++ b/update-common/src/artifacts/update_plan.rs @@ -22,11 +22,9 @@ use futures::StreamExt; use futures::TryStreamExt; use hubtools::RawHubrisArchive; use omicron_common::api::external::TufArtifactMeta; -use omicron_common::api::internal::nexus::KnownArtifactKind; use omicron_common::update::ArtifactHash; use omicron_common::update::ArtifactHashId; use omicron_common::update::ArtifactId; -use omicron_common::update::ArtifactKind; use semver::Version; use slog::info; use slog::Logger; @@ -37,6 +35,8 @@ use std::collections::HashMap; use std::io; use tokio::io::AsyncReadExt; use tokio::runtime::Handle; +use tufaceous_artifact::ArtifactKind; +use tufaceous_artifact::KnownArtifactKind; use tufaceous_lib::ControlPlaneZoneImages; use tufaceous_lib::HostPhaseImages; use tufaceous_lib::RotArchives; @@ -927,7 +927,7 @@ impl<'a> UpdatePlanBuilder<'a> { })?; let mut tar = tar::Archive::new(flate2::read::GzDecoder::new(file)); let metadata = - omicron_brand_metadata::Metadata::read_from_tar(&mut tar)?; + tufaceous_brand_metadata::Metadata::read_from_tar(&mut tar)?; let info = metadata.layer_info()?; let artifact_id = ArtifactId { @@ -1279,10 +1279,10 @@ mod tests { use bytes::Bytes; use flate2::{write::GzEncoder, Compression}; use futures::StreamExt; - use omicron_brand_metadata::{ArchiveType, LayerInfo, Metadata}; use omicron_test_utils::dev::test_setup_log; use rand::{distributions::Standard, thread_rng, Rng}; use sha2::{Digest, Sha256}; + use tufaceous_brand_metadata::{ArchiveType, LayerInfo, Metadata}; use tufaceous_lib::{ CompositeControlPlaneArchiveBuilder, CompositeEntry, MtimeSource, }; diff --git a/update-common/src/errors.rs b/update-common/src/errors.rs index 1ce9c6f5a14..f0670875312 100644 --- a/update-common/src/errors.rs +++ b/update-common/src/errors.rs @@ -7,11 +7,11 @@ use camino::Utf8PathBuf; use display_error_chain::DisplayErrorChain; use dropshot::HttpError; -use omicron_common::api::internal::nexus::KnownArtifactKind; -use omicron_common::update::{ArtifactHashId, ArtifactId, ArtifactKind}; +use omicron_common::update::{ArtifactHashId, ArtifactId}; use semver::Version; use slog::error; use thiserror::Error; +use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; #[derive(Debug, Error)] pub enum RepositoryError { diff --git a/wicket/Cargo.toml b/wicket/Cargo.toml index fefecd060c5..c9e43716304 100644 --- a/wicket/Cargo.toml +++ b/wicket/Cargo.toml @@ -52,6 +52,7 @@ wicket-common.workspace = true wicketd-client.workspace = true omicron-workspace-hack.workspace = true semver.workspace = true +tufaceous-artifact.workspace = true [dev-dependencies] assert_cmd.workspace = true diff --git a/wicket/src/state/inventory.rs b/wicket/src/state/inventory.rs index d7488381802..faee5cd47e5 100644 --- a/wicket/src/state/inventory.rs +++ b/wicket/src/state/inventory.rs @@ -5,14 +5,13 @@ //! Information about all top-level Oxide components (sleds, switches, PSCs) use anyhow::{bail, Context as _, Result}; -use omicron_common::api::{ - external::SwitchLocation, internal::nexus::KnownArtifactKind, -}; +use omicron_common::api::external::SwitchLocation; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt::Display; use std::iter::Iterator; use std::sync::LazyLock; +use tufaceous_artifact::KnownArtifactKind; use wicket_common::inventory::{ RackV1Inventory, RotInventory, RotSlot, SpComponentCaboose, SpComponentInfo, SpIgnition, SpState, SpType, Transceiver, diff --git a/wicket/src/state/update.rs b/wicket/src/state/update.rs index 7bbcf8ca41e..1b4481018b3 100644 --- a/wicket/src/state/update.rs +++ b/wicket/src/state/update.rs @@ -17,12 +17,12 @@ use crate::{ }; use super::{ComponentId, ParsableComponentId, ALL_COMPONENT_IDS}; -use omicron_common::api::internal::nexus::KnownArtifactKind; use semver::Version; use serde::{Deserialize, Serialize}; use slog::Logger; use std::collections::BTreeMap; use std::fmt::Display; +use tufaceous_artifact::KnownArtifactKind; // Represents a version and the signature (optional) associated // with a particular artifact. This allows for multiple versions diff --git a/wicket/src/ui/panes/update.rs b/wicket/src/ui/panes/update.rs index 1b2a1ed295d..1e4799721fd 100644 --- a/wicket/src/ui/panes/update.rs +++ b/wicket/src/ui/panes/update.rs @@ -19,7 +19,6 @@ use crate::ui::widgets::{ use crate::ui::wrap::wrap_text; use crate::{Action, Cmd, State}; use indexmap::IndexMap; -use omicron_common::api::internal::nexus::KnownArtifactKind; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{ @@ -28,6 +27,7 @@ use ratatui::widgets::{ }; use ratatui::Frame; use slog::{info, o, Logger}; +use tufaceous_artifact::KnownArtifactKind; use tui_tree_widget::{Tree, TreeItem, TreeState}; use update_engine::display::ProgressRatioDisplay; use update_engine::{ diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index 5bd7538ade9..15de73c6753 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -69,6 +69,7 @@ wicketd-api.workspace = true wicketd-client.workspace = true omicron-workspace-hack.workspace = true semver.workspace = true +tufaceous-artifact.workspace = true [[bin]] name = "wicketd" diff --git a/wicketd/tests/integration_tests/updates.rs b/wicketd/tests/integration_tests/updates.rs index d912438bee9..27687b288cd 100644 --- a/wicketd/tests/integration_tests/updates.rs +++ b/wicketd/tests/integration_tests/updates.rs @@ -13,11 +13,9 @@ use gateway_messages::SpPort; use gateway_test_utils::setup as gateway_setup; use installinator::HOST_PHASE_2_FILE_NAME; use maplit::btreeset; -use omicron_common::{ - api::internal::nexus::KnownArtifactKind, - update::{ArtifactHashId, ArtifactKind}, -}; +use omicron_common::update::ArtifactHashId; use tokio::sync::oneshot; +use tufaceous_artifact::{ArtifactKind, KnownArtifactKind}; use update_engine::NestedError; use uuid::Uuid; use wicket::OutputKind; @@ -45,7 +43,7 @@ async fn test_updates() { let args = tufaceous::Args::try_parse_from([ "tufaceous", "assemble", - "../tufaceous/manifests/fake.toml", + "../update-common/manifests/fake.toml", archive_path.as_str(), ]) .expect("args parsed correctly"); @@ -283,7 +281,7 @@ async fn test_installinator_fetch() { let args = tufaceous::Args::try_parse_from([ "tufaceous", "assemble", - "../tufaceous/manifests/fake.toml", + "../update-common/manifests/fake.toml", archive_path.as_str(), ]) .expect("args parsed correctly"); @@ -422,7 +420,7 @@ async fn test_update_races() { let args = tufaceous::Args::try_parse_from([ "tufaceous", "assemble", - "../tufaceous/manifests/fake.toml", + "../update-common/manifests/fake.toml", archive_path.as_str(), ]) .expect("args parsed correctly"); diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 70f7854912a..3af370d2220 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -25,11 +25,10 @@ aws-lc-rs = { version = "1.12.4", features = ["prebuilt-nasm"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } base64ct = { version = "1.6.0", default-features = false, features = ["std"] } -bit-set = { version = "0.5.3", default-features = false, features = ["std"] } -bit-vec = { version = "0.6.3", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.6.0", default-features = false, features = ["serde"] } bstr = { version = "1.10.0" } +buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] } byteorder = { version = "1.5.0" } bytes = { version = "1.10.0", features = ["serde"] } chrono = { version = "0.4.40", features = ["serde"] } @@ -99,7 +98,7 @@ qorb = { version = "0.2.1", features = ["qtop"] } quote = { version = "1.0.38" } rand = { version = "0.8.5", features = ["small_rng"] } regex = { version = "1.11.1" } -regex-automata = { version = "0.4.8", default-features = false, features = ["dfa", "hybrid", "meta", "nfa", "perf", "unicode"] } +regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-onepass", "dfa-search", "hybrid", "meta", "nfa", "perf", "unicode"] } regex-syntax = { version = "0.8.5" } reqwest = { version = "0.12.12", features = ["blocking", "cookies", "json", "rustls-tls", "stream"] } rsa = { version = "0.9.6", features = ["serde", "sha2"] } @@ -135,7 +134,8 @@ x509-cert = { version = "0.2.5" } zerocopy-c38e5c1d305a1b54 = { package = "zerocopy", version = "0.8.10", default-features = false, features = ["derive", "simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } zeroize = { version = "1.8.1", features = ["std", "zeroize_derive"] } -zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } +zip-3b31131e45eafb45 = { package = "zip", version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } +zip-f595c2ba2a3f28df = { package = "zip", version = "2.1.3", default-features = false, features = ["bzip2", "deflate"] } [build-dependencies] ahash = { version = "0.8.11" } @@ -145,11 +145,10 @@ aws-lc-rs = { version = "1.12.4", features = ["prebuilt-nasm"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } base64ct = { version = "1.6.0", default-features = false, features = ["std"] } -bit-set = { version = "0.5.3", default-features = false, features = ["std"] } -bit-vec = { version = "0.6.3", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.6.0", default-features = false, features = ["serde"] } bstr = { version = "1.10.0" } +buf-list = { version = "1.0.3", default-features = false, features = ["tokio1"] } byteorder = { version = "1.5.0" } bytes = { version = "1.10.0", features = ["serde"] } cc = { version = "1.2.15", default-features = false, features = ["parallel"] } @@ -220,7 +219,7 @@ qorb = { version = "0.2.1", features = ["qtop"] } quote = { version = "1.0.38" } rand = { version = "0.8.5", features = ["small_rng"] } regex = { version = "1.11.1" } -regex-automata = { version = "0.4.8", default-features = false, features = ["dfa", "hybrid", "meta", "nfa", "perf", "unicode"] } +regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-onepass", "dfa-search", "hybrid", "meta", "nfa", "perf", "unicode"] } regex-syntax = { version = "0.8.5" } reqwest = { version = "0.12.12", features = ["blocking", "cookies", "json", "rustls-tls", "stream"] } rsa = { version = "0.9.6", features = ["serde", "sha2"] } @@ -259,7 +258,8 @@ x509-cert = { version = "0.2.5" } zerocopy-c38e5c1d305a1b54 = { package = "zerocopy", version = "0.8.10", default-features = false, features = ["derive", "simd"] } zerocopy-ca01ad9e24f5d932 = { package = "zerocopy", version = "0.7.35", features = ["derive", "simd"] } zeroize = { version = "1.8.1", features = ["std", "zeroize_derive"] } -zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } +zip-3b31131e45eafb45 = { package = "zip", version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } +zip-f595c2ba2a3f28df = { package = "zip", version = "2.1.3", default-features = false, features = ["bzip2", "deflate"] } [target.x86_64-unknown-linux-gnu.dependencies] bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.6.0", default-features = false, features = ["std"] }