diff --git a/Cargo.lock b/Cargo.lock index 93da480bb50..85d38f2791c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4062,6 +4062,7 @@ dependencies = [ "dns-service-client", "dropshot", "expectorate", + "flate2", "futures", "http", "illumos-utils", @@ -4087,6 +4088,7 @@ dependencies = [ "rand 0.8.5", "reqwest", "schemars", + "semver 1.0.17", "serde", "serde_json", "serde_with", @@ -4102,6 +4104,7 @@ dependencies = [ "sprockets-common", "sprockets-host", "subprocess", + "tar", "tempfile", "thiserror", "tofino", diff --git a/Cargo.toml b/Cargo.toml index 2314816dc95..e7f697d1447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,6 +141,7 @@ dns-service-client = { path = "dns-service-client" } dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", features = [ "usdt-probes" ] } expectorate = "1.0.6" fatfs = "0.3.6" +flate2 = "1.0.25" fs-err = "2.9.0" futures = "0.3.27" gateway-client = { path = "gateway-client" } diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 46ee801728f..11bae08d29a 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -21,6 +21,7 @@ ddm-admin-client.workspace = true dns-server.workspace = true dns-service-client.workspace = true dropshot.workspace = true +flate2.workspace = true futures.workspace = true illumos-utils.workspace = true internal-dns-names.workspace = true @@ -40,6 +41,7 @@ propolis-client.workspace = true rand = { workspace = true, features = ["getrandom"] } reqwest = { workspace = true, features = ["rustls-tls", "stream"] } schemars = { workspace = true, features = [ "chrono", "uuid1" ] } +semver.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true @@ -51,6 +53,7 @@ smf.workspace = true sp-sim.workspace = true sprockets-common.workspace = true sprockets-host.workspace = true +tar.workspace = true tempfile.workspace = true thiserror.workspace = true tofino.workspace = true diff --git a/sled-agent/src/bin/sled-agent.rs b/sled-agent/src/bin/sled-agent.rs index 4a938e92d91..9a5657448c7 100644 --- a/sled-agent/src/bin/sled-agent.rs +++ b/sled-agent/src/bin/sled-agent.rs @@ -99,6 +99,7 @@ async fn do_run() -> Result<(), CmdError> { id: Uuid::new_v4(), link, log: config.log.clone(), + updates: config.updates.clone(), rss_config, sp_config, }; diff --git a/sled-agent/src/bootstrap/agent.rs b/sled-agent/src/bootstrap/agent.rs index 9bcc11f629f..b35b817b80b 100644 --- a/sled-agent/src/bootstrap/agent.rs +++ b/sled-agent/src/bootstrap/agent.rs @@ -5,7 +5,9 @@ //! Bootstrap-related APIs. use super::client::Client as BootstrapAgentClient; -use super::config::{Config, BOOTSTRAP_AGENT_PORT}; +use super::config::{ + Config, BOOTSTRAP_AGENT_HTTP_PORT, BOOTSTRAP_AGENT_SPROCKETS_PORT, +}; use super::ddm_admin_client::{DdmAdminClient, DdmError}; use super::hardware::HardwareMonitor; use super::params::SledAgentRequest; @@ -20,6 +22,7 @@ use crate::config::Config as SledConfig; use crate::server::Server as SledServer; use crate::services::ServiceManager; use crate::sp::SpHandle; +use crate::updates::UpdateManager; use futures::stream::{self, StreamExt, TryStreamExt}; use illumos_utils::dladm::{self, Dladm, GetMacError, PhysicalLink}; use illumos_utils::zfs::{ @@ -99,6 +102,9 @@ pub enum BootstrapError { #[error("Error managing guest networking: {0}")] Opte(#[from] illumos_utils::opte::Error), + + #[error("Error accessing version information: {0}")] + Version(#[from] crate::updates::Error), } impl From for ExternalError { @@ -126,7 +132,9 @@ pub(crate) struct Agent { /// Store the parent log - without "component = BootstrapAgent" - so /// other launched components can set their own value. parent_log: Logger, - address: SocketAddrV6, + + /// Bootstrap network address. + ip: Ipv6Addr, /// Our share of the rack secret, if we have one. share: Mutex>, @@ -159,6 +167,8 @@ fn mac_to_bootstrap_ip(mac: MacAddr, interface_id: u64) -> Ipv6Addr { ) } +// TODO(https://github.com/oxidecomputer/omicron/issues/945): This address +// could be randomly generated when it no longer needs to be durable. fn bootstrap_ip( link: PhysicalLink, interface_id: u64, @@ -167,17 +177,6 @@ fn bootstrap_ip( Ok(mac_to_bootstrap_ip(mac, interface_id)) } -// TODO(https://github.com/oxidecomputer/omicron/issues/945): This address -// could be randomly generated when it no longer needs to be durable. -fn bootstrap_address( - link: PhysicalLink, - interface_id: u64, - port: u16, -) -> Result { - let ip = bootstrap_ip(link, interface_id)?; - Ok(SocketAddrV6::new(ip, port, 0, 0)) -} - // Deletes all state which may be left-over from a previous execution of the // Sled Agent. // @@ -244,7 +243,7 @@ impl Agent { "component" => "BootstrapAgent", )); - let address = bootstrap_address(link.clone(), 1, BOOTSTRAP_AGENT_PORT)?; + let ip = bootstrap_ip(link.clone(), 1)?; // The only zone with a bootstrap ip address besides the global zone, // is the switch zone. We allocate this address here since we have @@ -294,7 +293,7 @@ impl Agent { Zones::ensure_has_global_zone_v6_address( bootstrap_etherstub_vnic.clone(), - *address.ip(), + ip, "bootstrap6", ) .map_err(|err| BootstrapError::BootstrapAddress { err })?; @@ -302,7 +301,7 @@ impl Agent { // Start trying to notify ddmd of our bootstrap address so it can // advertise it to other sleds. let ddmd_client = DdmAdminClient::new(log.clone())?; - ddmd_client.advertise_prefix(Ipv6Subnet::new(*address.ip())); + ddmd_client.advertise_prefix(Ipv6Subnet::new(ip)); // Before we start creating zones, we need to ensure that the // necessary ZFS and Zone resources are ready. @@ -360,7 +359,7 @@ impl Agent { let agent = Agent { log: ba_log, parent_log: log, - address, + ip, share: Mutex::new(None), rss: Mutex::new(None), sled_state: Mutex::new(SledAgentState::Before(Some( @@ -633,7 +632,7 @@ impl Agent { .map(|addr| { let addr = SocketAddrV6::new( addr, - BOOTSTRAP_AGENT_PORT, + BOOTSTRAP_AGENT_SPROCKETS_PORT, 0, 0, ); @@ -714,7 +713,7 @@ impl Agent { let rss = RssHandle::start_rss( &self.parent_log, rss_config.clone(), - *self.address.ip(), + self.ip, self.sp.clone(), // TODO-cleanup: Remove this arg once RSS can discover the trust // quorum members over the management network. @@ -729,9 +728,22 @@ impl Agent { Ok(()) } - /// Return the global zone address that the bootstrap agent binds to. - pub fn address(&self) -> SocketAddrV6 { - self.address + pub async fn components_get( + &self, + ) -> Result, BootstrapError> { + let updates = UpdateManager::new(self.sled_config.updates.clone()); + let components = updates.components_get().await?; + Ok(components) + } + + /// The GZ address used by the bootstrap agent for Sprockets. + pub fn sprockets_address(&self) -> SocketAddrV6 { + SocketAddrV6::new(self.ip, BOOTSTRAP_AGENT_SPROCKETS_PORT, 0, 0) + } + + /// The address used by the bootstrap agent to serve a dropshot interface. + pub fn http_address(&self) -> SocketAddrV6 { + SocketAddrV6::new(self.ip, BOOTSTRAP_AGENT_HTTP_PORT, 0, 0) } } diff --git a/sled-agent/src/bootstrap/config.rs b/sled-agent/src/bootstrap/config.rs index c3b072db85f..92917f5b4f0 100644 --- a/sled-agent/src/bootstrap/config.rs +++ b/sled-agent/src/bootstrap/config.rs @@ -5,13 +5,15 @@ //! Interfaces for working with bootstrap agent configuration use crate::sp::SimSpConfig; +use crate::updates::ConfigUpdates; use dropshot::ConfigLogging; use illumos_utils::dladm::PhysicalLink; use serde::Deserialize; use serde::Serialize; use uuid::Uuid; -pub const BOOTSTRAP_AGENT_PORT: u16 = 12346; +pub const BOOTSTRAP_AGENT_HTTP_PORT: u16 = 80; +pub const BOOTSTRAP_AGENT_SPROCKETS_PORT: u16 = 12346; /// Configuration for a bootstrap agent #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] @@ -19,6 +21,7 @@ pub struct Config { pub id: Uuid, pub link: PhysicalLink, pub log: ConfigLogging, + pub updates: ConfigUpdates, pub rss_config: Option, pub sp_config: Option, } diff --git a/sled-agent/src/bootstrap/http_entrypoints.rs b/sled-agent/src/bootstrap/http_entrypoints.rs new file mode 100644 index 00000000000..dc23e8d8b9c --- /dev/null +++ b/sled-agent/src/bootstrap/http_entrypoints.rs @@ -0,0 +1,50 @@ +// 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/. + +//! HTTP entrypoint functions for the bootstrap agent's API. +//! +//! Note that the bootstrap agent also communicates over Sprockets, +//! and has a separate interface for establishing the trust quorum. + +use crate::bootstrap::agent::Agent; +use crate::updates::Component; +use dropshot::{ + endpoint, ApiDescription, HttpError, HttpResponseOk, RequestContext, +}; +use omicron_common::api::external::Error; +use std::sync::Arc; + +type BootstrapApiDescription = ApiDescription>; + +/// Returns a description of the bootstrap agent API +pub(crate) fn api() -> BootstrapApiDescription { + fn register_endpoints( + api: &mut BootstrapApiDescription, + ) -> Result<(), String> { + api.register(components_get)?; + Ok(()) + } + + let mut api = BootstrapApiDescription::new(); + if let Err(err) = register_endpoints(&mut api) { + panic!("failed to register entrypoints: {}", err); + } + api +} + +/// Provides a list of components known to the bootstrap agent. +/// +/// This API is intended to allow early boot services (such as Wicket) +/// to query the underlying component versions installed on a sled. +#[endpoint { + method = GET, + path = "/components", +}] +async fn components_get( + rqctx: RequestContext>, +) -> Result>, HttpError> { + let ba = rqctx.context(); + let components = ba.components_get().await.map_err(|e| Error::from(e))?; + Ok(HttpResponseOk(components)) +} diff --git a/sled-agent/src/bootstrap/mod.rs b/sled-agent/src/bootstrap/mod.rs index 2d203967856..0f6dabdad34 100644 --- a/sled-agent/src/bootstrap/mod.rs +++ b/sled-agent/src/bootstrap/mod.rs @@ -9,6 +9,7 @@ pub mod client; pub mod config; pub mod ddm_admin_client; mod hardware; +mod http_entrypoints; mod maghemite; pub(crate) mod params; pub(crate) mod rss_handle; diff --git a/sled-agent/src/bootstrap/server.rs b/sled-agent/src/bootstrap/server.rs index ba6e537d026..6949610b7ed 100644 --- a/sled-agent/src/bootstrap/server.rs +++ b/sled-agent/src/bootstrap/server.rs @@ -12,6 +12,7 @@ use super::params::RequestEnvelope; use super::trust_quorum::ShareDistribution; use super::views::Response; use super::views::ResponseEnvelope; +use crate::bootstrap::http_entrypoints::api as http_api; use crate::bootstrap::maghemite; use crate::config::Config as SledConfig; use crate::sp::AsyncReadWrite; @@ -20,6 +21,7 @@ use crate::sp::SprocketsRole; use sled_hardware::underlay; use slog::Drain; use slog::Logger; +use std::net::SocketAddr; use std::sync::Arc; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; @@ -41,7 +43,8 @@ pub enum TrustQuorumMembership { /// via an HTTP interface. pub struct Server { bootstrap_agent: Arc, - inner: JoinHandle>, + sprockets_server_handle: JoinHandle>, + _http_server: dropshot::HttpServer>, } impl Server { @@ -97,23 +100,43 @@ impl Server { .map_err(|e| e.to_string())?; let bootstrap_agent = Arc::new(bootstrap_agent); - let ba_log = log.new(o!("component" => "BootstrapAgentServer")); - let inner = Inner::start( + let mut dropshot_config = dropshot::ConfigDropshot::default(); + dropshot_config.request_body_max_bytes = 1024 * 1024; + dropshot_config.bind_address = + SocketAddr::V6(bootstrap_agent.http_address()); + let dropshot_log = + log.new(o!("component" => "dropshot (BootstrapAgent)")); + let http_server = dropshot::HttpServerStarter::new( + &dropshot_config, + http_api(), + bootstrap_agent.clone(), + &dropshot_log, + ) + .map_err(|error| format!("initializing server: {}", error))? + .start(); + + let sprockets_log = + log.new(o!("component" => "sprockets (BootstrapAgent)")); + let sprockets_server_handle = Inner::start_sprockets( sp.clone(), trust_quorum, Arc::clone(&bootstrap_agent), - ba_log, + sprockets_log, ) .await?; - let server = Server { bootstrap_agent, inner }; + let server = Server { + bootstrap_agent, + sprockets_server_handle, + _http_server: http_server, + }; // Initialize the bootstrap agent *after* the server has started. // This ordering allows the bootstrap agent to communicate with // other bootstrap agents on the rack during the initialization // process. if let Err(e) = server.bootstrap_agent.start_rss(&config).await { - server.inner.abort(); + server.sprockets_server_handle.abort(); return Err(e.to_string()); } @@ -121,13 +144,13 @@ impl Server { } pub async fn wait_for_finish(self) -> Result<(), String> { - match self.inner.await { + match self.sprockets_server_handle.await { Ok(result) => result, Err(err) => { if err.is_cancelled() { - // We control cancellation of `inner`, which only happens if - // we intentionally abort it in `close()`; that should not - // result in an error here. + // We control cancellation of `sprockets_server_handle`, + // which only happens if we intentionally abort it in + // `close()`; that should not result in an error here. Ok(()) } else { Err(format!("Join on server tokio task failed: {err}")) @@ -137,7 +160,7 @@ impl Server { } pub async fn close(self) -> Result<(), String> { - self.inner.abort(); + self.sprockets_server_handle.abort(); self.wait_for_finish().await } } @@ -151,7 +174,7 @@ struct Inner { } impl Inner { - async fn start( + async fn start_sprockets( // TODO-cleanup `sp` is optional because we support running without an // SP / any trust quorum mechanisms. Eventually it should be required. sp: Option, @@ -159,7 +182,7 @@ impl Inner { bootstrap_agent: Arc, log: Logger, ) -> Result>, String> { - let bind_address = bootstrap_agent.address(); + let bind_address = bootstrap_agent.sprockets_address(); let listener = TcpListener::bind(bind_address).await.map_err(|err| { diff --git a/sled-agent/src/config.rs b/sled-agent/src/config.rs index 7a0c2b627f8..f46f401531c 100644 --- a/sled-agent/src/config.rs +++ b/sled-agent/src/config.rs @@ -4,6 +4,7 @@ //! Interfaces for working with sled agent configuration +use crate::updates::ConfigUpdates; use dropshot::ConfigLogging; use illumos_utils::dladm::Dladm; use illumos_utils::dladm::FindPhysicalLinkError; @@ -40,6 +41,9 @@ pub struct Config { /// This allows continued support for development and testing on emulated /// systems. pub data_link: Option, + + #[serde(default)] + pub updates: ConfigUpdates, } #[derive(Debug, thiserror::Error)] diff --git a/sled-agent/src/rack_setup/plan/sled.rs b/sled-agent/src/rack_setup/plan/sled.rs index 04167f98a7f..319b947bea4 100644 --- a/sled-agent/src/rack_setup/plan/sled.rs +++ b/sled-agent/src/rack_setup/plan/sled.rs @@ -5,7 +5,7 @@ //! Plan generation for "how should sleds be initialized". use crate::bootstrap::{ - config::BOOTSTRAP_AGENT_PORT, + config::BOOTSTRAP_AGENT_SPROCKETS_PORT, params::SledAgentRequest, trust_quorum::{RackSecret, ShareDistribution}, }; @@ -126,8 +126,12 @@ impl Plan { let bootstrap_addrs = bootstrap_addrs.into_iter().enumerate(); let allocations = bootstrap_addrs.map(|(idx, bootstrap_addr)| { info!(log, "Creating plan for the sled at {:?}", bootstrap_addr); - let bootstrap_addr = - SocketAddrV6::new(bootstrap_addr, BOOTSTRAP_AGENT_PORT, 0, 0); + let bootstrap_addr = SocketAddrV6::new( + bootstrap_addr, + BOOTSTRAP_AGENT_SPROCKETS_PORT, + 0, + 0, + ); let sled_subnet_index = u8::try_from(idx + 1).expect("Too many peers!"); let subnet = config.sled_subnet(sled_subnet_index); diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 4f59547e570..c43f9e53c56 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -16,7 +16,7 @@ use crate::params::{ }; use crate::services::{self, ServiceManager}; use crate::storage_manager::StorageManager; -use crate::updates::{ConfigUpdates, UpdateManager}; +use crate::updates::UpdateManager; use dropshot::HttpError; use illumos_utils::{execute, PFEXEC}; use omicron_common::address::{ @@ -33,7 +33,6 @@ use sled_hardware::underlay; use sled_hardware::HardwareManager; use slog::Logger; use std::net::{Ipv6Addr, SocketAddrV6}; -use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; @@ -273,9 +272,7 @@ impl SledAgent { let hardware = HardwareManager::new(&parent_log, config.stub_scrimlet) .map_err(|e| Error::Hardware(e))?; - let update_config = - ConfigUpdates { zone_artifact_path: PathBuf::from("/opt/oxide") }; - let updates = UpdateManager::new(update_config); + let updates = UpdateManager::new(config.updates.clone()); services .sled_agent_started( diff --git a/sled-agent/src/updates.rs b/sled-agent/src/updates.rs index 9a523203051..75819d30890 100644 --- a/sled-agent/src/updates.rs +++ b/sled-agent/src/updates.rs @@ -6,12 +6,14 @@ use crate::nexus::NexusClient; use futures::{TryFutureExt, TryStreamExt}; +use omicron_common::api::external::SemverVersion; use omicron_common::api::internal::nexus::{ KnownArtifactKind, UpdateArtifactId, }; -use serde::Deserialize; -use serde::Serialize; -use std::path::PathBuf; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use tokio::io::AsyncWriteExt; @@ -30,16 +32,57 @@ pub enum Error { )] UnsupportedKind(UpdateArtifactId), + #[error("Version not found in artifact {}", .0.display())] + VersionNotFound(PathBuf), + + #[error("Cannot parse json: {0}")] + Json(#[from] serde_json::Error), + + #[error("Malformed version in artifact {path}: {why}", path = path.display())] + VersionMalformed { path: PathBuf, why: String }, + + #[error("Cannot parse semver in {path}: {err}", path = path.display())] + Semver { path: PathBuf, err: semver::Error }, + #[error("Failed request to Nexus: {0}")] Response(nexus_client::Error), } +fn default_zone_artifact_path() -> PathBuf { + PathBuf::from("/opt/oxide") +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct ConfigUpdates { // Path where zone artifacts are stored. + #[serde(default = "default_zone_artifact_path")] pub zone_artifact_path: PathBuf, } +impl Default for ConfigUpdates { + fn default() -> Self { + Self { zone_artifact_path: default_zone_artifact_path() } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct Component { + pub name: String, + pub version: SemverVersion, +} + +// Helper functions for returning errors +fn version_malformed_err(path: &Path, key: &str) -> Error { + Error::VersionMalformed { + path: path.to_path_buf(), + why: format!("Missing '{key}'"), + } +} + +fn io_err(path: &Path, err: std::io::Error) -> Error { + Error::Io { message: format!("Cannot access {}", path.display()), err } +} + pub struct UpdateManager { config: ConfigUpdates, } @@ -122,6 +165,95 @@ impl UpdateManager { _ => Err(Error::UnsupportedKind(artifact)), } } + + // Gets the component version information from a single zone artifact. + async fn component_get_zone_version( + &self, + path: &Path, + ) -> Result { + // Decode the zone image + let file = + std::fs::File::open(path).map_err(|err| io_err(path, err))?; + let gzr = flate2::read::GzDecoder::new(file); + let mut component_reader = tar::Archive::new(gzr); + let entries = + component_reader.entries().map_err(|err| io_err(path, err))?; + + // Look for the JSON file which contains the package information + for entry in entries { + let mut entry = entry.map_err(|err| io_err(path, err))?; + let entry_path = entry.path().map_err(|err| io_err(path, err))?; + if entry_path == Path::new("oxide.json") { + let mut contents = String::new(); + entry + .read_to_string(&mut contents) + .map_err(|err| io_err(path, err))?; + let json: serde_json::Value = + serde_json::from_str(contents.as_str())?; + + // Parse keys from the JSON file + let serde_json::Value::String(pkg) = &json["pkg"] else { + return Err(version_malformed_err(path, "pkg")); + }; + let serde_json::Value::String(version) = &json["version"] else { + return Err(version_malformed_err(path, "version")); + }; + + // Extract the name and semver version + let name = pkg.to_string(); + let version = omicron_common::api::external::SemverVersion( + semver::Version::parse(version).map_err(|err| { + Error::Semver { path: path.to_path_buf(), err } + })?, + ); + return Ok(crate::updates::Component { name, version }); + } + } + Err(Error::VersionNotFound(path.to_path_buf())) + } + + pub async fn components_get(&self) -> Result, Error> { + let mut components = vec![]; + + let dir = &self.config.zone_artifact_path; + for entry in std::fs::read_dir(dir).map_err(|err| io_err(dir, err))? { + let entry = entry.map_err(|err| io_err(dir, err))?; + let file_type = + entry.file_type().map_err(|err| io_err(dir, err))?; + + if file_type.is_file() + && entry.file_name().to_string_lossy().ends_with(".tar.gz") + { + // Zone Images are currently identified as individual components. + // + // This logic may be tweaked in the future, depending on how we + // bundle together zones. + components.push( + self.component_get_zone_version(&entry.path()).await?, + ); + } else if file_type.is_dir() + && entry.file_name().to_string_lossy() == "sled-agent" + { + // Sled Agent is the only non-zone file recognized as a component. + let version_path = entry.path().join("VERSION"); + let version = tokio::fs::read_to_string(&version_path) + .await + .map_err(|err| io_err(&version_path, err))?; + + // Extract the name and semver version + let name = "sled-agent".to_string(); + let version = omicron_common::api::external::SemverVersion( + semver::Version::parse(&version).map_err(|err| { + Error::Semver { path: version_path.to_path_buf(), err } + })?, + ); + + components.push(crate::updates::Component { name, version }); + } + } + + Ok(components) + } } #[cfg(test)] @@ -129,9 +261,12 @@ mod test { use super::*; use crate::mocks::MockNexusClient; use bytes::Bytes; + use flate2::write::GzEncoder; use http::StatusCode; use progenitor::progenitor_client::{ByteStream, ResponseValue}; use reqwest::{header::HeaderMap, Result}; + use std::io::Write; + use tar::Builder; #[tokio::test] #[serial_test::serial] @@ -182,4 +317,81 @@ mod test { let contents = tokio::fs::read(&expected_path).await.unwrap(); assert_eq!(std::str::from_utf8(&contents).unwrap(), expected_contents); } + + #[tokio::test] + async fn test_query_no_components() { + let tempdir = tempfile::tempdir().expect("Failed to make tempdir"); + let config = + ConfigUpdates { zone_artifact_path: tempdir.path().to_path_buf() }; + let um = UpdateManager::new(config); + let components = + um.components_get().await.expect("Failed to get components"); + assert!(components.is_empty()); + } + + #[tokio::test] + async fn test_query_zone_version() { + let tempdir = tempfile::tempdir().expect("Failed to make tempdir"); + + // Construct something that looks like a zone image in the tempdir. + let zone_path = tempdir.path().join("test-pkg.tar.gz"); + let file = std::fs::File::create(&zone_path).unwrap(); + let gzw = GzEncoder::new(file, flate2::Compression::fast()); + let mut archive = Builder::new(gzw); + archive.mode(tar::HeaderMode::Deterministic); + + let mut json = tempfile::NamedTempFile::new().unwrap(); + json.write_all( + &r#"{"v":"1","t":"layer","pkg":"test-pkg","version":"2.0.0"}"# + .as_bytes(), + ) + .unwrap(); + archive.append_path_with_name(json.path(), "oxide.json").unwrap(); + let mut other_data = tempfile::NamedTempFile::new().unwrap(); + other_data + .write_all("lets throw in another file for good measure".as_bytes()) + .unwrap(); + archive.append_path_with_name(json.path(), "oxide.json").unwrap(); + archive.into_inner().unwrap().finish().unwrap(); + + drop(json); + drop(other_data); + + let config = + ConfigUpdates { zone_artifact_path: tempdir.path().to_path_buf() }; + let um = UpdateManager::new(config); + + let components = + um.components_get().await.expect("Failed to get components"); + assert_eq!(components.len(), 1); + assert_eq!(components[0].name, "test-pkg".to_string()); + assert_eq!( + components[0].version, + SemverVersion(semver::Version::new(2, 0, 0)) + ); + } + + #[tokio::test] + async fn test_query_sled_agent_version() { + let tempdir = tempfile::tempdir().expect("Failed to make tempdir"); + + // Construct something that looks like the sled agent. + let sled_agent_dir = tempdir.path().join("sled-agent"); + std::fs::create_dir(&sled_agent_dir).unwrap(); + std::fs::write(sled_agent_dir.join("VERSION"), "1.2.3".as_bytes()) + .unwrap(); + + let config = + ConfigUpdates { zone_artifact_path: tempdir.path().to_path_buf() }; + let um = UpdateManager::new(config); + + let components = + um.components_get().await.expect("Failed to get components"); + assert_eq!(components.len(), 1); + assert_eq!(components[0].name, "sled-agent".to_string()); + assert_eq!( + components[0].version, + SemverVersion(semver::Version::new(1, 2, 3)) + ); + } }