Skip to content

Commit 21fb969

Browse files
authored
repo-depot-standalone should assume an Omicron repo and unpack composite artifacts (#8684)
1 parent eb8ce24 commit 21fb969

File tree

3 files changed

+100
-57
lines changed

3 files changed

+100
-57
lines changed

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dev-tools/repo-depot-standalone/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ camino.workspace = true
1515
clap.workspace = true
1616
dropshot.workspace = true
1717
futures.workspace = true
18+
libc.workspace = true
1819
oxide-tokio-rt.workspace = true
1920
repo-depot-api.workspace = true
2021
serde_json.workspace = true
22+
signal-hook-tokio.workspace = true
2123
slog.workspace = true
2224
slog-error-chain.workspace = true
2325
tokio = { workspace = true, features = [ "full" ] }
24-
tough.workspace = true
26+
tokio-util.workspace = true
2527
tufaceous-artifact.workspace = true
26-
tufaceous-lib.workspace = true
28+
update-common.workspace = true
2729
omicron-workspace-hack.workspace = true
2830

2931
[[bin]]

dev-tools/repo-depot-standalone/src/main.rs

Lines changed: 92 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
//! Serve the Repo Depot API from one or more extracted TUF repos
5+
//! Serve the Repo Depot API from one or more Omicron TUF repos
66
77
use anyhow::Context;
88
use anyhow::anyhow;
@@ -16,18 +16,24 @@ use dropshot::HttpResponseOk;
1616
use dropshot::Path;
1717
use dropshot::RequestContext;
1818
use dropshot::ServerBuilder;
19+
use futures::StreamExt;
1920
use futures::stream::TryStreamExt;
21+
use libc::SIGINT;
2022
use repo_depot_api::ArtifactPathParams;
2123
use repo_depot_api::RepoDepotApi;
24+
use signal_hook_tokio::Signals;
2225
use slog::info;
2326
use slog_error_chain::InlineErrorChain;
2427
use std::collections::BTreeMap;
2528
use std::net::SocketAddr;
2629
use std::sync::Arc;
27-
use tough::Repository;
28-
use tough::TargetName;
30+
use tokio::io::AsyncRead;
31+
use tokio_util::io::ReaderStream;
2932
use tufaceous_artifact::ArtifactHash;
30-
use tufaceous_lib::OmicronRepo;
33+
use tufaceous_artifact::ArtifactHashId;
34+
use update_common::artifacts::{
35+
ArtifactsWithPlan, ControlPlaneZonesMode, VerificationMode,
36+
};
3137

3238
fn main() -> Result<(), anyhow::Error> {
3339
oxide_tokio_rt::run(async {
@@ -42,7 +48,7 @@ fn main() -> Result<(), anyhow::Error> {
4248
})
4349
}
4450

45-
/// Serve the Repo Depot API from one or more extracted TUF repos
51+
/// Serve the Repo Depot API from one or more Omicron TUF repos
4652
#[derive(Debug, Parser)]
4753
struct RepoDepotStandalone {
4854
/// log level filter
@@ -58,9 +64,9 @@ struct RepoDepotStandalone {
5864
#[arg(long, default_value = "[::]:0")]
5965
listen_addr: SocketAddr,
6066

61-
/// paths to local extracted Omicron TUF repositories
67+
/// paths to Omicron TUF repositories (zip files)
6268
#[arg(required = true, num_args = 1..)]
63-
repo_paths: Vec<Utf8PathBuf>,
69+
zip_files: Vec<Utf8PathBuf>,
6470
}
6571

6672
fn parse_dropshot_log_level(
@@ -77,15 +83,33 @@ impl RepoDepotStandalone {
7783
.to_logger("repo-depot-standalone")
7884
.context("failed to create logger")?;
7985

86+
// Gracefully handle SIGINT so that we clean up the files that got
87+
// extracted to a temporary directory.
88+
let signals =
89+
Signals::new(&[SIGINT]).expect("failed to wait for SIGINT");
90+
let mut signal_stream = signals.fuse();
91+
8092
let mut ctx = RepoMetadata::new();
81-
for repo_path in &self.repo_paths {
82-
let omicron_repo =
83-
OmicronRepo::load_untrusted_ignore_expiration(&log, repo_path)
84-
.await
85-
.with_context(|| {
86-
format!("loading repository at {repo_path}")
87-
})?;
88-
ctx.load_repo(omicron_repo)
93+
for repo_path in &self.zip_files {
94+
let file = std::fs::File::open(repo_path)
95+
.with_context(|| format!("open {:?}", repo_path))?;
96+
let buf = std::io::BufReader::new(file);
97+
info!(
98+
&log,
99+
"extracting Omicron TUF repository";
100+
"path" => %repo_path
101+
);
102+
let plan = ArtifactsWithPlan::from_zip(
103+
buf,
104+
None,
105+
ArtifactHash([0; 32]),
106+
ControlPlaneZonesMode::Split,
107+
VerificationMode::BlindlyTrustAnything,
108+
&log,
109+
)
110+
.await
111+
.with_context(|| format!("load {:?}", repo_path))?;
112+
ctx.load_repo(plan)
89113
.context("loading artifacts from repository at {repo_path}")?;
90114
info!(&log, "loaded Omicron TUF repository"; "path" => %repo_path);
91115
}
@@ -95,59 +119,84 @@ impl RepoDepotStandalone {
95119
>()
96120
.unwrap();
97121

98-
let server = ServerBuilder::new(my_api, Arc::new(ctx), log)
122+
let server = ServerBuilder::new(my_api, Arc::new(ctx), log.clone())
99123
.config(dropshot::ConfigDropshot {
100124
bind_address: self.listen_addr,
101125
..Default::default()
102126
})
103127
.start()
104128
.context("failed to create server")?;
105129

106-
server.await.map_err(|error| anyhow!("server shut down: {error}"))
130+
// Wait for a signal.
131+
let caught_signal = signal_stream.next().await;
132+
assert_eq!(caught_signal.unwrap(), SIGINT);
133+
info!(
134+
&log,
135+
"caught signal, shutting down and removing \
136+
temporary directories"
137+
);
138+
139+
// The temporary files are deleted by `Drop` handlers so all we need to
140+
// do is shut down gracefully.
141+
server
142+
.close()
143+
.await
144+
.map_err(|e| anyhow!("error closing HTTP server: {e}"))
107145
}
108146
}
109147

110148
/// Keeps metadata that allows us to fetch a target from any of the TUF repos
111149
/// based on its hash.
112150
struct RepoMetadata {
113-
repos: Vec<OmicronRepo>,
114-
targets_by_hash: BTreeMap<ArtifactHash, (usize, TargetName)>,
151+
repos: Vec<ArtifactsWithPlan>,
152+
targets_by_hash: BTreeMap<ArtifactHash, (usize, ArtifactHashId)>,
115153
}
116154

117155
impl RepoMetadata {
118156
pub fn new() -> RepoMetadata {
119157
RepoMetadata { repos: Vec::new(), targets_by_hash: BTreeMap::new() }
120158
}
121159

122-
pub fn load_repo(
123-
&mut self,
124-
omicron_repo: OmicronRepo,
125-
) -> anyhow::Result<()> {
160+
pub fn load_repo(&mut self, plan: ArtifactsWithPlan) -> anyhow::Result<()> {
126161
let repo_index = self.repos.len();
127162

128-
let tuf_repo = omicron_repo.repo();
129-
for (target_name, target) in &tuf_repo.targets().signed.targets {
130-
let target_hash: &[u8] = &target.hashes.sha256;
131-
let target_hash_array: [u8; 32] = target_hash
132-
.try_into()
133-
.context("sha256 hash wasn't 32 bytes")?;
134-
let artifact_hash = ArtifactHash(target_hash_array);
163+
for artifact_meta in &plan.description().artifacts {
164+
let artifact_hash = artifact_meta.hash;
165+
let artifact_id = &artifact_meta.id;
166+
let artifact_hash_id = ArtifactHashId {
167+
kind: artifact_id.kind.clone(),
168+
hash: artifact_hash,
169+
};
170+
171+
// Some hashes appear multiple times, whether in the same repo or
172+
// different repos. That's fine. They all have the same contents
173+
// so we can serve any of them when this hash is requested.
135174
self.targets_by_hash
136-
.insert(artifact_hash, (repo_index, target_name.clone()));
175+
.insert(artifact_meta.hash, (repo_index, artifact_hash_id));
137176
}
138177

139-
self.repos.push(omicron_repo);
178+
self.repos.push(plan);
140179
Ok(())
141180
}
142181

143-
pub fn repo_and_target_name_for_hash(
182+
pub async fn data_for_hash(
144183
&self,
145184
requested_sha: &ArtifactHash,
146-
) -> Option<(&Repository, &TargetName)> {
147-
let (repo_index, target_name) =
185+
) -> Option<anyhow::Result<ReaderStream<impl AsyncRead>>> {
186+
let (repo_index, artifact_hash_id) =
148187
self.targets_by_hash.get(requested_sha)?;
149-
let omicron_repo = &self.repos[*repo_index];
150-
Some((omicron_repo.repo(), target_name))
188+
let repo = &self.repos[*repo_index];
189+
Some(
190+
repo.get_by_hash(artifact_hash_id)
191+
.unwrap_or_else(|| {
192+
panic!(
193+
"artifact hash unexpectedly missing from the repo that \
194+
we recorded having found it in"
195+
)
196+
})
197+
.reader_stream()
198+
.await,
199+
)
151200
}
152201
}
153202

@@ -162,29 +211,19 @@ impl RepoDepotApi for StandaloneApiImpl {
162211
) -> Result<HttpResponseOk<FreeformBody>, HttpError> {
163212
let repo_metadata = rqctx.context();
164213
let requested_sha = &path_params.into_inner().sha256;
165-
let (tuf_repo, target_name) = repo_metadata
166-
.repo_and_target_name_for_hash(requested_sha)
214+
let reader = repo_metadata
215+
.data_for_hash(requested_sha)
216+
.await
167217
.ok_or_else(|| {
168218
HttpError::for_not_found(
169219
None,
170220
String::from("found no target with this hash"),
171221
)
172-
})?;
173-
174-
let reader = tuf_repo
175-
.read_target(&target_name)
176-
.await
222+
})?
177223
.map_err(|error| {
178224
HttpError::for_internal_error(format!(
179-
"failed to read target from TUF repo: {}",
180-
InlineErrorChain::new(&error),
181-
))
182-
})?
183-
.ok_or_else(|| {
184-
// We already checked above that the hash is present in the TUF
185-
// repo so this should not be a 404.
186-
HttpError::for_internal_error(String::from(
187-
"missing target from TUF repo",
225+
"loading file from TUF repo: {}",
226+
InlineErrorChain::new(&*error),
188227
))
189228
})?;
190229
let mut buf_list =

0 commit comments

Comments
 (0)