Skip to content

Commit

Permalink
feat: custom error types.
Browse files Browse the repository at this point in the history
Add custom error types for all errors generated in the library.

Signed-off-by: José Guilherme Vanz <[email protected]>
  • Loading branch information
jvanz committed Nov 28, 2023
1 parent 30e22b0 commit b1966a7
Show file tree
Hide file tree
Showing 16 changed files with 414 additions and 187 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0"
async-std = "1.9"
async-stream = "0.3"
async-trait = "0.1"
Expand All @@ -36,6 +35,7 @@ sigstore = { version = "0.7.2", default-features = false, features = [
"tuf",
"cached-client",
] }
thiserror = "1.0"
tracing = "0.1"
url = { version = "2.2", features = ["serde"] }
walkdir = "2"
Expand Down
149 changes: 149 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use thiserror::Error;

#[derive(Error, Debug)]
pub enum FetcherErrors {
#[error("Fail to interact with OCI registry: {0}")]
OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError),
#[error("Invalid destination format")]
InvalidDestinationError,

#[error("cannot retrieve path from uri: {0}")]
InvalidFilePathError(String),
#[error("invalid wasm file")]
InvalidWasmFileError,
#[error("wasm module cannot be save to {0:?}: {1}")]
CannotWriteWasmModuleFile(String, #[source] std::io::Error),

#[error(transparent)]
PolicyError(#[from] crate::policy::DigestError),
#[error(transparent)]
VerifyError(#[from] VerifyErrors),
#[error(transparent)]
RegistryError(#[from] RegistryErrors),
#[error(transparent)]
UrlParserError(#[from] url::ParseError),
#[error(transparent)]
SourceError(#[from] SourceErrors),
#[error(transparent)]
StoreError(#[from] StoreErrors),
#[error(transparent)]
InvalidURLError(#[from] InvalidURLError),
#[error(transparent)]
CannotCreateStoragePathError(#[from] CannotCreateStoragePathError),
}

//#[derive(Error, Debug)]
//pub enum InnerErrors{
// #[error("invalid URL: {0}")]
// InvalidURLError(String),
// #[error("Fail to interact with OCI registry: {0}")]
// OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError),
// #[error("Invalid OCI image reference: {0}")]
// InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError),
// #[error("could not pull policy {0}: empty layers")]
// EmptyLayersError(String),
// //#[error("Invalid certificate: {0}")]
// //InvalidCertificateError(String),
// //#[error("Cannot read certificate from file: {0}")]
// //CannotReadCertificateError(#[from] std::io::Error),
//}

#[derive(thiserror::Error, Debug)]
#[error("{0}")]
pub struct FailedToParseYamlDataError(#[from] pub serde_yaml::Error);

#[derive(Error, Debug)]
pub enum SourceErrors {
#[error(transparent)]
InvalidURLError(#[from] InvalidURLError),
#[error("Fail to interact with OCI registry: {0}")]
OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError),
#[error("Invalid OCI image reference: {0}")]
InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError),
#[error("could not pull policy {0}: empty layers")]
EmptyLayersError(String),
#[error("Invalid certificate: {0}")]
InvalidCertificateError(String),
#[error("Cannot read certificate from file: {0}")]
CannotReadCertificateError(#[from] std::io::Error),
#[error(transparent)]
FailedToParseYamlDataError(#[from] FailedToParseYamlDataError),
#[error("failed to create the http client: {0}")]
FailedToCreateHttpClientError(#[from] reqwest::Error),
}

#[derive(thiserror::Error, Debug)]
#[error("invalid URL: {0}")]
pub struct CannotCreateStoragePathError(#[from] pub std::io::Error);

#[derive(Error, Debug)]
pub enum StoreErrors {
#[error(transparent)]
UrlParserError(#[from] url::ParseError),
#[error("faild to read verification file: {0}")]
VerificationFileReadError(#[from] std::io::Error),
#[error("cannot read policy in local storage: {0}")]
FailedToReadPolicyInLocalStorageError(#[from] walkdir::Error),
#[error(transparent)]
PolicyStoragePathError(#[from] std::path::StripPrefixError),
#[error("unknown scheme: {0}")]
UnknownSchemeError(String),
#[error("multiple policies found with the same prefix: {0}")]
MultiplePoliciesFoundError(String),
#[error(transparent)]
DigestErrors(#[from] crate::policy::DigestError),
#[error(transparent)]
CannotCreateStoragePathError(#[from] CannotCreateStoragePathError),
}

#[derive(thiserror::Error, Debug)]
#[error("invalid URL: {0}")]
pub struct InvalidURLError(pub String);

#[derive(Error, Debug)]
pub enum RegistryErrors {
#[error("Fail to interact with OCI registry: {0}")]
OCIRegistryError(#[from] oci_distribution::errors::OciDistributionError),
#[error("Invalid OCI image reference: {0}")]
InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError),
#[error("{0}")]
BuildImmutableReferenceError(String),
#[error("Invalid destination format")]
InvalidDestinationError,
#[error("{0}")]
OtherError(String),
#[error(transparent)]
UrlParserError(#[from] url::ParseError),
#[error(transparent)]
DigestErrors(#[from] crate::policy::DigestError),
#[error(transparent)]
InvalidURLError(#[from] InvalidURLError),
}

#[derive(Error, Debug)]
pub enum VerifyErrors {
#[error("faild to read verification file: {0}")]
VerificationFileReadError(#[from] std::io::Error),
#[error("{0}")]
ChecksumVerificationError(String),
#[error("{0}")]
ImageVerificationError(String),
#[error("{0}")]
InvalidVerifyFileError(String),
#[error("Verification only works with OCI images: Not a valid oci image: {0}")]
InvalidOCIImageReferenceError(#[from] oci_distribution::ParseError),
#[error("key verification failure: {0} ")]
KeyVerificationError(#[source] sigstore::errors::SigstoreError),
#[error("Policy cannot be verified, local wasm file doesn't exist: {0}")]
MissingWasmFileError(String),
#[error("failed to get image trusted layers: {0}")]
FailedToFetchTrustedLayersError(#[from] sigstore::errors::SigstoreError),
#[error(transparent)]
DigestErrors(#[from] crate::policy::DigestError),
#[error(transparent)]
RegistryError(#[from] RegistryErrors),
#[error("{0}")]
GithubUrlParserError(String),
#[error(transparent)]
FailedToParseYamlDataError(#[from] FailedToParseYamlDataError),
}
9 changes: 6 additions & 3 deletions src/fetcher.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use anyhow::Result;
use async_trait::async_trait;
use url::Url;

use crate::sources::Certificate;
use crate::{errors::SourceErrors, sources::Certificate};

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ClientProtocol {
Expand All @@ -22,5 +21,9 @@ pub(crate) enum TlsVerificationMode {
#[async_trait]
pub(crate) trait PolicyFetcher {
// Download and return the bytes of the WASM module
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result<Vec<u8>>;
async fn fetch(
&self,
url: &Url,
client_protocol: ClientProtocol,
) -> std::result::Result<Vec<u8>, SourceErrors>;
}
30 changes: 22 additions & 8 deletions src/https.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#![allow(clippy::upper_case_acronyms)]

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use std::{
boxed::Box,
convert::{TryFrom, TryInto},
};
use url::Url;

use crate::errors::SourceErrors;
use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode};
use crate::sources::Certificate;

Expand All @@ -16,21 +16,35 @@ use crate::sources::Certificate;
pub(crate) struct Https {}

impl TryFrom<&Certificate> for reqwest::Certificate {
type Error = anyhow::Error;
type Error = SourceErrors;

fn try_from(certificate: &Certificate) -> Result<Self> {
fn try_from(certificate: &Certificate) -> std::result::Result<Self, SourceErrors> {
match certificate {
Certificate::Der(certificate) => reqwest::Certificate::from_der(certificate)
.map_err(|err| anyhow!("could not load certificate as DER encoded: {}", err)),
Certificate::Pem(certificate) => reqwest::Certificate::from_pem(certificate)
.map_err(|err| anyhow!("could not load certificate as PEM encoded: {}", err)),
Certificate::Der(certificate) => {
reqwest::Certificate::from_der(certificate).map_err(|err| {
SourceErrors::InvalidCertificateError(format!(
"could not load certificate as DER encoded: {err}"
))
})
}
Certificate::Pem(certificate) => {
reqwest::Certificate::from_pem(certificate).map_err(|err| {
SourceErrors::InvalidCertificateError(format!(
"could not load certificate as PEM encoded: {err}"
))
})
}
}
}
}

#[async_trait]
impl PolicyFetcher for Https {
async fn fetch(&self, url: &Url, client_protocol: ClientProtocol) -> Result<Vec<u8>> {
async fn fetch(
&self,
url: &Url,
client_protocol: ClientProtocol,
) -> std::result::Result<Vec<u8>, SourceErrors> {
let mut client_builder = reqwest::Client::builder();
match client_protocol {
ClientProtocol::Http => {}
Expand Down
58 changes: 36 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ extern crate reqwest;
extern crate rustls;
extern crate walkdir;

use anyhow::{anyhow, Result};
use std::boxed::Box;
use std::fs;
use url::Url;
use verify::errors::StoreErrors;

pub mod errors;
pub mod fetcher;
mod https;
pub mod policy;
Expand All @@ -16,6 +17,7 @@ pub mod sources;
pub mod store;
pub mod verify;

use crate::errors::{CannotCreateStoragePathError, FetcherErrors};
use crate::fetcher::{ClientProtocol, PolicyFetcher, TlsVerificationMode};
use crate::https::Https;
use crate::policy::Policy;
Expand Down Expand Up @@ -78,7 +80,7 @@ pub async fn fetch_policy(
url: &str,
destination: PullDestination,
sources: Option<&Sources>,
) -> Result<Policy> {
) -> std::result::Result<Policy, FetcherErrors> {
let url = parse_url(url)?;
match url.scheme() {
"file" => {
Expand All @@ -87,15 +89,17 @@ pub async fn fetch_policy(
uri: url.to_string(),
local_path: url
.to_file_path()
.map_err(|err| anyhow!("cannot retrieve path from uri {}: {:?}", url, err))?,
.map_err(|_| FetcherErrors::InvalidFilePathError(url.to_string()))?,
});
}
"registry" | "http" | "https" => Ok(()),
_ => Err(anyhow!("unknown scheme: {}", url.scheme())),
_ => Err(StoreErrors::UnknownSchemeError(url.scheme().to_owned())),
}?;
let (store, mut destination) = pull_destination(&url, &destination)?;
if let Some(store) = store {
store.ensure(&store.policy_full_path(url.as_str(), store::PolicyPath::PrefixOnly)?)?;
store
.ensure(&store.policy_full_path(url.as_str(), store::PolicyPath::PrefixOnly)?)
.map_err(CannotCreateStoragePathError)?;
}
match url.scheme() {
"registry" => {
Expand Down Expand Up @@ -134,11 +138,7 @@ pub async fn fetch_policy(
{
Err(err) => {
if !sources.is_insecure_source(&host_and_port(&url)?) {
return Err(anyhow!(
"the policy {} could not be downloaded due to error: {}",
url,
err
));
return Err(FetcherErrors::SourceError(err));
}
}
Ok(bytes) => return create_file_if_valid(&bytes, &destination, url.to_string()),
Expand All @@ -155,11 +155,14 @@ pub async fn fetch_policy(

match policy_fetcher.fetch(&url, ClientProtocol::Http).await {
Ok(bytes) => create_file_if_valid(&bytes, &destination, url.to_string()),
Err(e) => Err(anyhow!("could not pull policy {}: {}", url, e)),
Err(e) => Err(FetcherErrors::SourceError(e)),
}
}

fn client_protocol(url: &Url, sources: &Sources) -> Result<ClientProtocol> {
fn client_protocol(
url: &Url,
sources: &Sources,
) -> std::result::Result<ClientProtocol, errors::InvalidURLError> {
if let Some(certificates) = sources.source_authority(&host_and_port(url)?) {
return Ok(ClientProtocol::Https(
TlsVerificationMode::CustomCaCertificates(certificates),
Expand All @@ -168,7 +171,10 @@ fn client_protocol(url: &Url, sources: &Sources) -> Result<ClientProtocol> {
Ok(ClientProtocol::Https(TlsVerificationMode::SystemCa))
}

fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option<Store>, PathBuf)> {
fn pull_destination(
url: &Url,
destination: &PullDestination,
) -> std::result::Result<(Option<Store>, PathBuf), FetcherErrors> {
Ok(match destination {
PullDestination::MainStore => {
let store = Store::default();
Expand Down Expand Up @@ -196,19 +202,19 @@ fn pull_destination(url: &Url, destination: &PullDestination) -> Result<(Option<
// Helper function, takes the URL of the policy and allocates the
// right struct to interact with it
#[allow(clippy::box_default)]
fn url_fetcher(scheme: &str) -> Result<Box<dyn PolicyFetcher>> {
fn url_fetcher(scheme: &str) -> std::result::Result<Box<dyn PolicyFetcher>, StoreErrors> {
match scheme {
"http" | "https" => Ok(Box::new(Https::default())),
"registry" => Ok(Box::new(Registry::new())),
_ => Err(anyhow!("unknown scheme: {}", scheme)),
_ => Err(StoreErrors::UnknownSchemeError(scheme.to_owned())),
}
}

pub(crate) fn host_and_port(url: &Url) -> Result<String> {
pub(crate) fn host_and_port(url: &Url) -> std::result::Result<String, errors::InvalidURLError> {
Ok(format!(
"{}{}",
url.host_str()
.ok_or_else(|| anyhow!("invalid URL {}", url))?,
.ok_or_else(|| errors::InvalidURLError(url.to_string()))?,
url.port()
.map(|port| format!(":{}", port))
.unwrap_or_default(),
Expand All @@ -222,12 +228,17 @@ pub(crate) fn host_and_port(url: &Url) -> Result<String> {
// https://webassembly.github.io/spec/core/bikeshed/#binary-magic
const WASM_MAGIC_NUMBER: [u8; 4] = [0x00, 0x61, 0x73, 0x6D];

fn create_file_if_valid(bytes: &[u8], destination: &Path, url: String) -> Result<Policy> {
fn create_file_if_valid(
bytes: &[u8],
destination: &Path,
url: String,
) -> std::result::Result<Policy, FetcherErrors> {
if !bytes.starts_with(&WASM_MAGIC_NUMBER) {
return Err(anyhow!("invalid wasm file"));
return Err(FetcherErrors::InvalidWasmFileError);
};
fs::write(destination, bytes)
.map_err(|e| anyhow!("wasm module cannot be save to {:?}: {}", destination, e))?;
fs::write(destination, bytes).map_err(|e| {
FetcherErrors::CannotWriteWasmModuleFile(destination.to_string_lossy().to_string(), e)
})?;

Ok(Policy {
uri: url,
Expand Down Expand Up @@ -359,7 +370,10 @@ mod tests {
port: Some(5000),
path: "/kubewarden/policies/test".to_string(),
}))]
fn url_parsing(#[case] url: &str, #[case] expected: anyhow::Result<UrlParseDetails>) {
fn url_parsing(
#[case] url: &str,
#[case] expected: std::result::Result<UrlParseDetails, FetcherErrors>,
) {
let res = parse_url(url);
println!("{} -> {:?}", url, res);
assert_eq!(res.is_ok(), expected.is_ok());
Expand Down
Loading

0 comments on commit b1966a7

Please sign in to comment.