|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +//! Handling of `oxide.json` metadata files in tarballs. |
| 6 | +//! |
| 7 | +//! `oxide.json` is originally defined by the omicron1(7) zone brand, which |
| 8 | +//! lives at <https://github.com/oxidecomputer/helios-omicron-brand>. tufaceous |
| 9 | +//! extended this format with additional archive types for identifying other |
| 10 | +//! types of tarballs; this crate covers those extensions so they can be used |
| 11 | +//! across the Omicron codebase. |
| 12 | +
|
| 13 | +use std::io::{Error, ErrorKind, Read, Result, Write}; |
| 14 | + |
| 15 | +use serde::{Deserialize, Serialize}; |
| 16 | + |
| 17 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 18 | +pub struct Metadata { |
| 19 | + v: String, |
| 20 | + |
| 21 | + // helios-build-utils defines a top-level `i` field for extra information, |
| 22 | + // but omicron-package doesn't use this for the package name and version. |
| 23 | + // We can also benefit from having rich types for these extra fields, so |
| 24 | + // any additional top-level fields (including `i`) that exist for a given |
| 25 | + // archive type should be deserialized as part of `ArchiveType`. |
| 26 | + #[serde(flatten)] |
| 27 | + t: ArchiveType, |
| 28 | +} |
| 29 | + |
| 30 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 31 | +#[serde(rename_all = "snake_case", tag = "t")] |
| 32 | +pub enum ArchiveType { |
| 33 | + // Originally defined in helios-build-utils (part of helios-omicron-brand): |
| 34 | + Baseline, |
| 35 | + Layer(LayerInfo), |
| 36 | + Os, |
| 37 | + |
| 38 | + // tufaceous extensions: |
| 39 | + Rot, |
| 40 | + ControlPlane, |
| 41 | +} |
| 42 | + |
| 43 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 44 | +pub struct LayerInfo { |
| 45 | + pub pkg: String, |
| 46 | + pub version: semver::Version, |
| 47 | +} |
| 48 | + |
| 49 | +impl Metadata { |
| 50 | + pub fn new(archive_type: ArchiveType) -> Metadata { |
| 51 | + Metadata { v: "1".into(), t: archive_type } |
| 52 | + } |
| 53 | + |
| 54 | + pub fn append_to_tar<T: Write>( |
| 55 | + &self, |
| 56 | + a: &mut tar::Builder<T>, |
| 57 | + mtime: u64, |
| 58 | + ) -> Result<()> { |
| 59 | + let mut b = serde_json::to_vec(self)?; |
| 60 | + b.push(b'\n'); |
| 61 | + |
| 62 | + let mut h = tar::Header::new_ustar(); |
| 63 | + h.set_entry_type(tar::EntryType::Regular); |
| 64 | + h.set_username("root")?; |
| 65 | + h.set_uid(0); |
| 66 | + h.set_groupname("root")?; |
| 67 | + h.set_gid(0); |
| 68 | + h.set_path("oxide.json")?; |
| 69 | + h.set_mode(0o444); |
| 70 | + h.set_size(b.len().try_into().unwrap()); |
| 71 | + h.set_mtime(mtime); |
| 72 | + h.set_cksum(); |
| 73 | + |
| 74 | + a.append(&h, b.as_slice())?; |
| 75 | + Ok(()) |
| 76 | + } |
| 77 | + |
| 78 | + /// Read `Metadata` from a tar archive. |
| 79 | + /// |
| 80 | + /// `oxide.json` is generally the first file in the archive, so this should |
| 81 | + /// be a just-opened archive with no entries already read. |
| 82 | + pub fn read_from_tar<T: Read>(a: &mut tar::Archive<T>) -> Result<Metadata> { |
| 83 | + for entry in a.entries()? { |
| 84 | + let mut entry = entry?; |
| 85 | + if entry.path()? == std::path::Path::new("oxide.json") { |
| 86 | + return Ok(serde_json::from_reader(&mut entry)?); |
| 87 | + } |
| 88 | + } |
| 89 | + Err(Error::new(ErrorKind::InvalidData, "oxide.json is not present")) |
| 90 | + } |
| 91 | + |
| 92 | + pub fn archive_type(&self) -> &ArchiveType { |
| 93 | + &self.t |
| 94 | + } |
| 95 | + |
| 96 | + pub fn is_layer(&self) -> bool { |
| 97 | + matches!(&self.t, ArchiveType::Layer(_)) |
| 98 | + } |
| 99 | + |
| 100 | + pub fn layer_info(&self) -> Result<&LayerInfo> { |
| 101 | + match &self.t { |
| 102 | + ArchiveType::Layer(info) => Ok(info), |
| 103 | + _ => Err(Error::new( |
| 104 | + ErrorKind::InvalidData, |
| 105 | + "archive is not the \"layer\" type", |
| 106 | + )), |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + pub fn is_baseline(&self) -> bool { |
| 111 | + matches!(&self.t, ArchiveType::Baseline) |
| 112 | + } |
| 113 | + |
| 114 | + pub fn is_os(&self) -> bool { |
| 115 | + matches!(&self.t, ArchiveType::Os) |
| 116 | + } |
| 117 | + |
| 118 | + pub fn is_rot(&self) -> bool { |
| 119 | + matches!(&self.t, ArchiveType::Rot) |
| 120 | + } |
| 121 | + |
| 122 | + pub fn is_control_plane(&self) -> bool { |
| 123 | + matches!(&self.t, ArchiveType::ControlPlane) |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +#[cfg(test)] |
| 128 | +mod tests { |
| 129 | + use super::*; |
| 130 | + |
| 131 | + #[test] |
| 132 | + fn test_deserialize() { |
| 133 | + let metadata: Metadata = serde_json::from_str( |
| 134 | + r#"{"v":"1","t":"layer","pkg":"nexus","version":"12.0.0-0.ci+git3a2ed5e97b3"}"#, |
| 135 | + ) |
| 136 | + .unwrap(); |
| 137 | + assert!(metadata.is_layer()); |
| 138 | + let info = metadata.layer_info().unwrap(); |
| 139 | + assert_eq!(info.pkg, "nexus"); |
| 140 | + assert_eq!(info.version, "12.0.0-0.ci+git3a2ed5e97b3".parse().unwrap()); |
| 141 | + |
| 142 | + let metadata: Metadata = serde_json::from_str( |
| 143 | + r#"{"v":"1","t":"os","i":{"checksum":"42eda100ee0e3bf44b9d0bb6a836046fa3133c378cd9d3a4ba338c3ba9e56eb7","name":"ci 3a2ed5e/9d37813 2024-12-20 08:54"}}"#, |
| 144 | + ).unwrap(); |
| 145 | + assert!(metadata.is_os()); |
| 146 | + |
| 147 | + let metadata: Metadata = |
| 148 | + serde_json::from_str(r#"{"v":"1","t":"control_plane"}"#).unwrap(); |
| 149 | + assert!(metadata.is_control_plane()); |
| 150 | + } |
| 151 | +} |
0 commit comments