Skip to content

Commit b5a2cda

Browse files
authored
Implement Package Caching (#60)
* wip cache structures * Walker, but no real caching yet * WIP adding cache * Better blake3 * Fix tests, fix stamping * Split out digest, switch to camino * update tests * tests, docs, cache disabling option * Update tests, identify that this is version breaking (0.11.0) * once_cell less * more context, less with_context * bail
1 parent f70713a commit b5a2cda

File tree

11 files changed

+1669
-582
lines changed

11 files changed

+1669
-582
lines changed

Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[package]
22
name = "omicron-zone-package"
3-
version = "0.10.1"
3+
version = "0.11.0"
44
authors = ["Sean Klein <[email protected]>"]
5-
edition = "2018"
5+
edition = "2021"
66
#
77
# Report a specific error in the case that the toolchain is too old for
88
# let-else:
@@ -15,18 +15,23 @@ description = "Packaging tools for Oxide's control plane software"
1515
[dependencies]
1616
anyhow = "1.0"
1717
async-trait = "0.1.67"
18+
blake3 = { version = "1.5", features = ["mmap", "rayon"] }
19+
camino = { version = "1.1", features = ["serde1"] }
20+
camino-tempfile = "1.1"
1821
chrono = "0.4.24"
1922
filetime = "0.2"
2023
flate2 = "1.0.25"
24+
futures = "0.3"
2125
futures-util = "0.3"
2226
hex = "0.4.3"
2327
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "stream"] }
2428
ring = "0.16.20"
2529
semver = { version = "1.0.17", features = ["std", "serde"] }
2630
serde = { version = "1.0", features = [ "derive" ] }
2731
serde_derive = "1.0"
32+
serde_json = "1.0"
33+
slog = "2.7"
2834
tar = "0.4"
29-
tempfile = "3.4"
3035
thiserror = "1.0"
3136
tokio = { version = "1.26", features = [ "full" ] }
3237
toml = "0.7.3"

src/archive.rs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
//! Tools for creating and inserting into tarballs.
6+
7+
use anyhow::{anyhow, bail, Context, Result};
8+
use async_trait::async_trait;
9+
use camino::Utf8Path;
10+
use flate2::write::GzEncoder;
11+
use std::convert::TryInto;
12+
use std::fs::{File, OpenOptions};
13+
use tar::Builder;
14+
15+
/// These interfaces are similar to some methods in [tar::Builder].
16+
///
17+
/// They use [tokio::block_in_place] to avoid blocking other async
18+
/// tasks using the executor.
19+
#[async_trait]
20+
pub trait AsyncAppendFile {
21+
async fn append_file_async<P>(&mut self, path: P, file: &mut File) -> std::io::Result<()>
22+
where
23+
P: AsRef<Utf8Path> + Send;
24+
25+
async fn append_path_with_name_async<P, N>(&mut self, path: P, name: N) -> std::io::Result<()>
26+
where
27+
P: AsRef<Utf8Path> + Send,
28+
N: AsRef<Utf8Path> + Send;
29+
30+
async fn append_dir_all_async<P, Q>(&mut self, path: P, src_path: Q) -> std::io::Result<()>
31+
where
32+
P: AsRef<Utf8Path> + Send,
33+
Q: AsRef<Utf8Path> + Send;
34+
}
35+
36+
#[async_trait]
37+
impl<W: Encoder> AsyncAppendFile for Builder<W> {
38+
async fn append_file_async<P>(&mut self, path: P, file: &mut File) -> std::io::Result<()>
39+
where
40+
P: AsRef<Utf8Path> + Send,
41+
{
42+
tokio::task::block_in_place(move || self.append_file(path.as_ref(), file))
43+
}
44+
45+
async fn append_path_with_name_async<P, N>(&mut self, path: P, name: N) -> std::io::Result<()>
46+
where
47+
P: AsRef<Utf8Path> + Send,
48+
N: AsRef<Utf8Path> + Send,
49+
{
50+
tokio::task::block_in_place(move || {
51+
self.append_path_with_name(path.as_ref(), name.as_ref())
52+
})
53+
}
54+
55+
async fn append_dir_all_async<P, Q>(&mut self, path: P, src_path: Q) -> std::io::Result<()>
56+
where
57+
P: AsRef<Utf8Path> + Send,
58+
Q: AsRef<Utf8Path> + Send,
59+
{
60+
tokio::task::block_in_place(move || self.append_dir_all(path.as_ref(), src_path.as_ref()))
61+
}
62+
}
63+
64+
/// Helper to open a tarfile for reading/writing.
65+
pub fn create_tarfile<P: AsRef<Utf8Path> + std::fmt::Debug>(tarfile: P) -> Result<File> {
66+
OpenOptions::new()
67+
.write(true)
68+
.read(true)
69+
.truncate(true)
70+
.create(true)
71+
.open(tarfile.as_ref())
72+
.map_err(|err| anyhow!("Cannot create tarfile {:?}: {}", tarfile, err))
73+
}
74+
75+
/// Helper to open a tarfile for reading.
76+
pub fn open_tarfile<P: AsRef<Utf8Path> + std::fmt::Debug>(tarfile: P) -> Result<File> {
77+
OpenOptions::new()
78+
.read(true)
79+
.open(tarfile.as_ref())
80+
.map_err(|err| anyhow!("Cannot open tarfile {:?}: {}", tarfile, err))
81+
}
82+
83+
pub trait Encoder: std::io::Write + Send {}
84+
impl<T> Encoder for T where T: std::io::Write + Send {}
85+
86+
pub struct ArchiveBuilder<E: Encoder> {
87+
pub builder: tar::Builder<E>,
88+
}
89+
90+
impl<E: Encoder> ArchiveBuilder<E> {
91+
pub fn new(builder: tar::Builder<E>) -> Self {
92+
Self { builder }
93+
}
94+
95+
pub fn into_inner(self) -> Result<E> {
96+
self.builder.into_inner().context("Finalizing archive")
97+
}
98+
}
99+
100+
/// Adds a package at `package_path` to a new zone image
101+
/// being built using the `archive` builder.
102+
pub async fn add_package_to_zone_archive<E: Encoder>(
103+
archive: &mut ArchiveBuilder<E>,
104+
package_path: &Utf8Path,
105+
) -> Result<()> {
106+
let tmp = camino_tempfile::tempdir()?;
107+
let gzr = flate2::read::GzDecoder::new(open_tarfile(package_path)?);
108+
if gzr.header().is_none() {
109+
bail!(
110+
"Missing gzip header from {} - cannot add it to zone image",
111+
package_path,
112+
);
113+
}
114+
let mut component_reader = tar::Archive::new(gzr);
115+
let entries = component_reader.entries()?;
116+
117+
// First, unpack the existing entries
118+
for entry in entries {
119+
let mut entry = entry?;
120+
121+
// Ignore the JSON header files
122+
let entry_path = entry.path()?;
123+
if entry_path == Utf8Path::new("oxide.json") {
124+
continue;
125+
}
126+
127+
let entry_path: &Utf8Path = entry_path.strip_prefix("root/")?.try_into()?;
128+
let entry_unpack_path = tmp.path().join(entry_path);
129+
entry.unpack(&entry_unpack_path)?;
130+
131+
let entry_path = entry.path()?.into_owned();
132+
let entry_path: &Utf8Path = entry_path.as_path().try_into()?;
133+
assert!(entry_unpack_path.exists());
134+
135+
archive
136+
.builder
137+
.append_path_with_name_async(entry_unpack_path, entry_path)
138+
.await?;
139+
}
140+
Ok(())
141+
}
142+
143+
pub async fn new_compressed_archive_builder(
144+
path: &Utf8Path,
145+
) -> Result<ArchiveBuilder<GzEncoder<File>>> {
146+
let file = create_tarfile(path)?;
147+
// TODO: Consider using async compression, async tar.
148+
// It's not the *worst* thing in the world for a packaging tool to block
149+
// here, but it would help the other async threads remain responsive if
150+
// we avoided blocking.
151+
let gzw = GzEncoder::new(file, flate2::Compression::fast());
152+
let mut archive = Builder::new(gzw);
153+
archive.mode(tar::HeaderMode::Deterministic);
154+
155+
Ok(ArchiveBuilder::new(archive))
156+
}

src/blob.rs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
//! Tools for downloading blobs
66
77
use anyhow::{anyhow, Context, Result};
8+
use camino::{Utf8Path, Utf8PathBuf};
89
use chrono::{DateTime, FixedOffset, Utc};
910
use futures_util::StreamExt;
1011
use reqwest::header::{CONTENT_LENGTH, LAST_MODIFIED};
1112
use ring::digest::{Context as DigestContext, Digest, SHA256};
12-
use std::path::{Path, PathBuf};
13+
use serde::{Deserialize, Serialize};
1314
use std::str::FromStr;
1415
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
1516

@@ -20,16 +21,16 @@ const S3_BUCKET: &str = "https://oxide-omicron-build.s3.amazonaws.com";
2021
// Name for the directory component where downloaded blobs are stored.
2122
pub(crate) const BLOB: &str = "blob";
2223

23-
#[derive(Debug)]
24-
pub enum Source<'a> {
25-
S3(&'a PathBuf),
26-
Buildomat(&'a crate::package::PrebuiltBlob),
24+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
25+
pub enum Source {
26+
S3(Utf8PathBuf),
27+
Buildomat(crate::package::PrebuiltBlob),
2728
}
2829

29-
impl<'a> Source<'a> {
30+
impl Source {
3031
pub(crate) fn get_url(&self) -> String {
3132
match self {
32-
Self::S3(s) => format!("{}/{}", S3_BUCKET, s.to_string_lossy()),
33+
Self::S3(s) => format!("{}/{}", S3_BUCKET, s),
3334
Self::Buildomat(spec) => {
3435
format!(
3536
"https://buildomat.eng.oxide.computer/public/file/oxidecomputer/{}/{}/{}/{}",
@@ -43,7 +44,7 @@ impl<'a> Source<'a> {
4344
&self,
4445
url: &str,
4546
client: &reqwest::Client,
46-
destination: &Path,
47+
destination: &Utf8Path,
4748
) -> Result<bool> {
4849
if !destination.exists() {
4950
return Ok(true);
@@ -90,14 +91,16 @@ impl<'a> Source<'a> {
9091
}
9192

9293
// Downloads "source" from S3_BUCKET to "destination".
93-
pub async fn download<'a>(
94-
progress: &impl Progress,
95-
source: &Source<'a>,
96-
destination: &Path,
94+
pub async fn download(
95+
progress: &dyn Progress,
96+
source: &Source,
97+
destination: &Utf8Path,
9798
) -> Result<()> {
9899
let blob = destination
99100
.file_name()
100-
.ok_or_else(|| anyhow!("missing blob filename"))?;
101+
.as_ref()
102+
.ok_or_else(|| anyhow!("missing blob filename"))?
103+
.to_string();
101104

102105
let url = source.get_url();
103106
let client = reqwest::Client::new();
@@ -134,15 +137,15 @@ pub async fn download<'a>(
134137
let blob_progress = if let Some(length) = content_length {
135138
progress.sub_progress(length)
136139
} else {
137-
Box::new(NoProgress)
140+
Box::new(NoProgress::new())
138141
};
139-
blob_progress.set_message(blob.to_string_lossy().into_owned().into());
142+
blob_progress.set_message(blob.into());
140143

141144
let mut stream = response.bytes_stream();
142145
while let Some(chunk) = stream.next().await {
143146
let chunk = chunk?;
144147
file.write_all(&chunk).await?;
145-
blob_progress.increment(chunk.len() as u64);
148+
blob_progress.increment_completed(chunk.len() as u64);
146149
}
147150
drop(blob_progress);
148151

@@ -170,7 +173,7 @@ pub async fn download<'a>(
170173
Ok(())
171174
}
172175

173-
async fn get_sha256_digest(path: &Path) -> Result<Digest> {
176+
async fn get_sha256_digest(path: &Utf8Path) -> Result<Digest> {
174177
let mut reader = BufReader::new(
175178
tokio::fs::File::open(path)
176179
.await

0 commit comments

Comments
 (0)