Skip to content

Commit 1e4af29

Browse files
authored
Merge pull request #68 from cgwalters/use-thiserror
Port to thiserror, release 0.7
2 parents fc8fd00 + cdff1c9 commit 1e4af29

File tree

2 files changed

+98
-36
lines changed

2 files changed

+98
-36
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ license = "MIT OR Apache-2.0"
55
name = "containers-image-proxy"
66
readme = "README.md"
77
repository = "https://github.com/containers/containers-image-proxy-rs"
8-
version = "0.6.0"
8+
version = "0.7.0"
99
rust-version = "1.70.0"
1010

1111
[dependencies]
12-
anyhow = "1.0"
1312
fn-error-context = "0.2.0"
1413
futures-util = "0.3.13"
1514
# NOTE when bumping this in a semver-incompatible way, because we re-export it you
@@ -19,12 +18,14 @@ rustix = { version = "0.38", features = ["process", "net"] }
1918
serde = { features = ["derive"], version = "1.0.125" }
2019
serde_json = "1.0.64"
2120
semver = "1.0.4"
21+
thiserror = "1"
2222
tokio = { features = ["fs", "io-util", "macros", "process", "rt", "sync"], version = "1" }
2323
tracing = "0.1"
2424
# We support versions 2, 3 and 4
2525
cap-std-ext = ">= 2.0, <= 4.0"
2626

2727
[dev-dependencies]
28+
anyhow = "1.0"
2829
bytes = "1.5"
2930
clap = { version = "4.4", features = ["derive"] }
3031

src/imageproxy.rs

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
//!
55
//! More information: <https://github.com/containers/skopeo/pull/1476>
66
7-
use anyhow::{anyhow, Context, Result};
87
use cap_std_ext::prelude::CapStdExtCommandExt;
98
use cap_std_ext::{cap_std, cap_tempfile};
109
use futures_util::Future;
@@ -18,11 +17,59 @@ use std::path::PathBuf;
1817
use std::pin::Pin;
1918
use std::process::{Command, Stdio};
2019
use std::sync::{Arc, Mutex, OnceLock};
20+
use thiserror::Error;
2121
use tokio::io::{AsyncBufRead, AsyncReadExt};
2222
use tokio::sync::Mutex as AsyncMutex;
2323
use tokio::task::JoinError;
2424
use tracing::instrument;
2525

26+
/// Errors returned by this crate.
27+
#[derive(Error, Debug)]
28+
#[non_exhaustive]
29+
pub enum Error {
30+
#[error("i/o error")]
31+
/// An input/output error
32+
Io(#[from] std::io::Error),
33+
#[error("serialization error")]
34+
/// Returned when serialization or deserialization fails
35+
SerDe(#[from] serde_json::Error),
36+
/// The proxy failed to initiate a request
37+
#[error("failed to invoke method {method}: {error}")]
38+
RequestInitiationFailure { method: Box<str>, error: Box<str> },
39+
/// An error returned from the remote proxy
40+
#[error("proxy request returned error")]
41+
RequestReturned(Box<str>),
42+
#[error("semantic version error")]
43+
SemanticVersion(#[from] semver::Error),
44+
#[error("proxy too old (requested={requested_version} found={found_version}) error")]
45+
/// The proxy doesn't support the requested semantic version
46+
ProxyTooOld {
47+
requested_version: Box<str>,
48+
found_version: Box<str>,
49+
},
50+
#[error("configuration error")]
51+
/// Conflicting or missing configuration
52+
Configuration(Box<str>),
53+
#[error("error")]
54+
/// An unknown other error
55+
Other(Box<str>),
56+
}
57+
58+
impl Error {
59+
pub(crate) fn new_other(e: impl Into<Box<str>>) -> Self {
60+
Self::Other(e.into())
61+
}
62+
}
63+
64+
impl From<rustix::io::Errno> for Error {
65+
fn from(value: rustix::io::Errno) -> Self {
66+
Self::Io(value.into())
67+
}
68+
}
69+
70+
/// The error type returned from this crate.
71+
pub type Result<T> = std::result::Result<T, Error>;
72+
2673
/// Re-export because we use this in our public APIs
2774
pub use oci_spec;
2875

@@ -152,14 +199,14 @@ pub struct ImageProxyConfig {
152199
}
153200

154201
impl TryFrom<ImageProxyConfig> for Command {
155-
type Error = anyhow::Error;
202+
type Error = Error;
156203

157-
fn try_from(config: ImageProxyConfig) -> Result<Self, Self::Error> {
204+
fn try_from(config: ImageProxyConfig) -> Result<Self> {
158205
let mut allocated_fds = RESERVED_FD_RANGE.clone();
159206
let mut alloc_fd = || {
160-
allocated_fds
161-
.next()
162-
.ok_or_else(|| anyhow::anyhow!("Ran out of reserved file descriptors for child"))
207+
allocated_fds.next().ok_or_else(|| {
208+
Error::Other("Ran out of reserved file descriptors for child".into())
209+
})
163210
};
164211

165212
// By default, we set up pdeathsig to "lifecycle bind" the child process to us.
@@ -186,7 +233,9 @@ impl TryFrom<ImageProxyConfig> for Command {
186233
.count();
187234
if auth_option_count > 1 {
188235
// This is a programmer error really
189-
anyhow::bail!("Conflicting authentication options");
236+
return Err(Error::Configuration(
237+
"Conflicting authentication options".into(),
238+
));
190239
}
191240
if let Some(authfile) = config.authfile {
192241
c.arg("--authfile");
@@ -197,11 +246,13 @@ impl TryFrom<ImageProxyConfig> for Command {
197246
// the file is only readable to privileged code.
198247
let target_fd = alloc_fd()?;
199248
let tmpd = &cap_std::fs::Dir::open_ambient_dir("/tmp", cap_std::ambient_authority())?;
200-
let mut tempfile = cap_tempfile::TempFile::new_anonymous(tmpd)
201-
.context("Creating temporary file for auth data")
202-
.map(std::io::BufWriter::new)?;
249+
let mut tempfile =
250+
cap_tempfile::TempFile::new_anonymous(tmpd).map(std::io::BufWriter::new)?;
203251
std::io::copy(&mut auth_data, &mut tempfile)?;
204-
let tempfile = tempfile.into_inner()?.into_std();
252+
let tempfile = tempfile
253+
.into_inner()
254+
.map_err(|e| e.into_error())?
255+
.into_std();
205256
let fd = std::sync::Arc::new(tempfile.into());
206257
c.take_fd_n(fd, target_fd);
207258
c.arg("--authfile");
@@ -261,7 +312,7 @@ impl ImageProxy {
261312
None,
262313
)?;
263314
c.stdin(Stdio::from(theirsock));
264-
let child = c.spawn().context("Failed to spawn skopeo")?;
315+
let child = c.spawn()?;
265316
tracing::debug!("Spawned skopeo pid={:?}", child.id());
266317
// Here we use std sync API via thread because tokio installs
267318
// a SIGCHLD handler which can conflict with e.g. the glib one
@@ -283,11 +334,10 @@ impl ImageProxy {
283334
// Previously we had a feature to opt-in to requiring newer versions using `if cfg!()`.
284335
let supported = base_proto_version();
285336
if !supported.matches(&protover) {
286-
return Err(anyhow!(
287-
"Unsupported protocol version {} (compatible: {})",
288-
protover,
289-
supported
290-
));
337+
return Err(Error::ProxyTooOld {
338+
requested_version: protover.to_string().into(),
339+
found_version: supported.to_string().into(),
340+
});
291341
}
292342
r.protover = protover;
293343

@@ -327,28 +377,32 @@ impl ImageProxy {
327377
.flatten()
328378
.next();
329379
let buf = &buf[..nread];
330-
let reply: Reply = serde_json::from_slice(buf).context("Deserializing reply")?;
380+
let reply: Reply = serde_json::from_slice(buf)?;
331381
if !reply.success {
332-
return Err(anyhow!("remote error: {}", reply.error));
382+
return Err(Error::RequestInitiationFailure {
383+
method: req.method.clone().into(),
384+
error: reply.error.into(),
385+
});
333386
}
334387
let fdret = match (fdret, reply.pipeid) {
335388
(Some(fd), n) => {
336389
if n == 0 {
337-
return Err(anyhow!("got fd but no pipeid"));
390+
return Err(Error::Other("got fd but no pipeid".into()));
338391
}
339392
Some((fd, n))
340393
}
341394
(None, n) => {
342395
if n != 0 {
343-
return Err(anyhow!("got no fd with pipeid {}", n));
396+
return Err(Error::Other(format!("got no fd with pipeid {}", n).into()));
344397
}
345398
None
346399
}
347400
};
348-
let reply = serde_json::from_value(reply.value).context("Deserializing value")?;
401+
let reply = serde_json::from_value(reply.value)?;
349402
Ok((reply, fdret))
350403
})
351-
.await??;
404+
.await
405+
.map_err(|e| Error::Other(e.to_string().into()))??;
352406
tracing::trace!("completed request");
353407
Ok(r)
354408
}
@@ -367,12 +421,15 @@ impl ImageProxy {
367421
let mut childwait = self.childwait.lock().await;
368422
tokio::select! {
369423
r = req => {
370-
Ok(r.with_context(|| format!("Failed to invoke skopeo proxy method {method}"))?)
424+
r.map_err(|e| Error::RequestInitiationFailure {
425+
method: method.to_string().into(),
426+
error: e.to_string().into()
427+
})
371428
}
372429
r = childwait.as_mut() => {
373-
let r = r??;
430+
let r = r.map_err(|e| Error::Other(e.to_string().into()))??;
374431
let stderr = String::from_utf8_lossy(&r.stderr);
375-
Err(anyhow::anyhow!("skopeo proxy unexpectedly exited during request method {}: {}\n{}", method, r.status, stderr))
432+
Err(Error::Other(format!("skopeo proxy unexpectedly exited during request method {}: {}\n{}", method, r.status, stderr).into()))
376433
}
377434
}
378435
}
@@ -382,7 +439,7 @@ impl ImageProxy {
382439
tracing::debug!("closing pipe");
383440
let (r, fd) = self.impl_request("FinishPipe", [pipeid]).await?;
384441
if fd.is_some() {
385-
return Err(anyhow!("Unexpected fd in finish_pipe reply"));
442+
return Err(Error::Other("Unexpected fd in finish_pipe reply".into()));
386443
}
387444
Ok(r)
388445
}
@@ -417,7 +474,7 @@ impl ImageProxy {
417474
}
418475

419476
async fn read_all_fd(&self, fd: Option<(OwnedFd, u32)>) -> Result<Vec<u8>> {
420-
let (fd, pipeid) = fd.ok_or_else(|| anyhow!("Missing fd from reply"))?;
477+
let (fd, pipeid) = fd.ok_or_else(|| Error::Other("Missing fd from reply".into()))?;
421478
let fd = tokio::fs::File::from_std(std::fs::File::from(fd));
422479
let mut fd = tokio::io::BufReader::new(fd);
423480
let mut r = Vec::new();
@@ -443,8 +500,7 @@ impl ImageProxy {
443500
img: &OpenedImage,
444501
) -> Result<(String, oci_spec::image::ImageManifest)> {
445502
let (digest, raw) = self.fetch_manifest_raw_oci(img).await?;
446-
let manifest =
447-
serde_json::from_slice(&raw).context("Deserializing manifest from skopeo")?;
503+
let manifest = serde_json::from_slice(&raw)?;
448504
Ok((digest, manifest))
449505
}
450506

@@ -464,7 +520,7 @@ impl ImageProxy {
464520
img: &OpenedImage,
465521
) -> Result<oci_spec::image::ImageConfiguration> {
466522
let raw = self.fetch_config_raw(img).await?;
467-
serde_json::from_slice(&raw).context("Deserializing config from skopeo")
523+
serde_json::from_slice(&raw).map_err(Into::into)
468524
}
469525

470526
/// Fetch a blob identified by e.g. `sha256:<digest>`.
@@ -487,7 +543,7 @@ impl ImageProxy {
487543
let args: Vec<serde_json::Value> =
488544
vec![img.0.into(), digest.to_string().into(), size.into()];
489545
let (_bloblen, fd) = self.impl_request::<i64, _, _>("GetBlob", args).await?;
490-
let (fd, pipeid) = fd.ok_or_else(|| anyhow!("Missing fd from reply"))?;
546+
let (fd, pipeid) = fd.ok_or_else(|| Error::new_other("Missing fd from reply"))?;
491547
let fd = tokio::fs::File::from_std(std::fs::File::from(fd));
492548
let fd = tokio::io::BufReader::new(fd);
493549
let finish = Box::pin(self.finish_pipe(pipeid));
@@ -535,10 +591,15 @@ impl ImageProxy {
535591
drop(sendbuf);
536592
tracing::debug!("sent shutdown request");
537593
let mut childwait = self.childwait.lock().await;
538-
let output = childwait.as_mut().await??;
594+
let output = childwait
595+
.as_mut()
596+
.await
597+
.map_err(|e| Error::new_other(e.to_string()))??;
539598
if !output.status.success() {
540599
let stderr = String::from_utf8_lossy(&output.stderr);
541-
anyhow::bail!("proxy failed: {}\n{}", output.status, stderr)
600+
return Err(Error::RequestReturned(
601+
format!("proxy failed: {}\n{}", output.status, stderr).into(),
602+
));
542603
}
543604
tracing::debug!("proxy exited successfully");
544605
Ok(())

0 commit comments

Comments
 (0)