use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use anyhow::{anyhow, bail, Result};
use oci_distribution::manifest::OciImageManifest;
use oci_distribution::{
client::{Client, ClientConfig, ClientProtocol, Config, ImageLayer},
secrets::RegistryAuth,
Reference,
};
use provider_archive::ProviderArchive;
use regex::RegexBuilder;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
const PROVIDER_ARCHIVE_MEDIA_TYPE: &str = "application/vnd.wasmcloud.provider.archive.layer.v1+par";
const PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE: &str =
"application/vnd.wasmcloud.provider.archive.config";
const WASM_MEDIA_TYPE: &str = "application/vnd.module.wasm.content.layer.v1+wasm";
const WASM_CONFIG_MEDIA_TYPE: &str = "application/vnd.wasmcloud.actor.archive.config";
const OCI_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
pub const REFERENCE_REGEXP: &str = r"^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$";
#[derive(Default)]
pub struct OciPullOptions {
pub digest: Option<String>,
pub allow_latest: bool,
pub user: Option<String>,
pub password: Option<String>,
pub insecure: bool,
}
#[derive(Default)]
pub struct OciPushOptions {
pub config: Option<PathBuf>,
pub allow_latest: bool,
pub user: Option<String>,
pub password: Option<String>,
pub insecure: bool,
pub annotations: Option<HashMap<String, String>>,
}
pub enum SupportedArtifacts {
Par,
Wasm,
}
pub async fn get_oci_artifact(
url_or_file: String,
cache_file: Option<PathBuf>,
options: OciPullOptions,
) -> Result<Vec<u8>> {
if let Ok(mut local_artifact) = File::open(&url_or_file).await {
let mut buf = Vec::new();
local_artifact.read_to_end(&mut buf).await?;
return Ok(buf);
} else if let Some(cache_path) = cache_file {
if let Ok(mut cached_artifact) = File::open(cache_path).await {
let mut buf = Vec::new();
cached_artifact.read_to_end(&mut buf).await?;
return Ok(buf);
}
}
pull_oci_artifact(url_or_file, options).await
}
pub async fn pull_oci_artifact(url: String, options: OciPullOptions) -> Result<Vec<u8>> {
let image: Reference = url.to_lowercase().parse()?;
let re = RegexBuilder::new(REFERENCE_REGEXP)
.size_limit(10 * (1 << 21))
.build()?;
let input_tag = match re.captures(&url) {
Some(caps) => caps.get(2).map(|m| m.as_str().to_owned()),
None => bail!("Invalid OCI reference URL."),
}
.unwrap_or(String::from(""));
if !options.allow_latest {
if input_tag == "latest" {
bail!("Pulling artifacts with tag 'latest' is prohibited. This can be overriden with the flag '--allow-latest'.");
} else if input_tag.is_empty() {
bail!("Registry URLs must have explicit tag. To default missing tags to 'latest', use the flag '--allow-latest'.");
}
}
let mut client = Client::new(ClientConfig {
protocol: if options.insecure {
ClientProtocol::Http
} else {
ClientProtocol::Https
},
..Default::default()
});
let auth = match (options.user, options.password) {
(Some(user), Some(password)) => RegistryAuth::Basic(user, password),
_ => RegistryAuth::Anonymous,
};
let image_data = client
.pull(
&image,
&auth,
vec![PROVIDER_ARCHIVE_MEDIA_TYPE, WASM_MEDIA_TYPE, OCI_MEDIA_TYPE],
)
.await?;
let digest = match options.digest {
Some(d) if d.starts_with("sha256:") => Some(d),
Some(d) => Some(format!("sha256:{d}")),
None => None,
};
match (digest, image_data.digest) {
(Some(digest), Some(image_digest)) if digest != image_digest => {
return Err(anyhow!(
"Image digest did not match provided digest, aborting"
))
}
_ => (),
};
Ok(image_data
.layers
.iter()
.flat_map(|l| l.data.clone())
.collect::<Vec<_>>())
}
pub async fn push_oci_artifact(
url: String,
artifact: impl AsRef<Path>,
options: OciPushOptions,
) -> Result<()> {
let image: Reference = url.to_lowercase().parse()?;
if image.tag().unwrap() == "latest" && !options.allow_latest {
bail!("Pushing artifacts with tag 'latest' is prohibited");
};
let mut artifact_buf = vec![];
let mut f = File::open(artifact).await?;
f.read_to_end(&mut artifact_buf).await?;
let (artifact_media_type, config_media_type) = match validate_artifact(&artifact_buf).await? {
SupportedArtifacts::Wasm => (WASM_MEDIA_TYPE, WASM_CONFIG_MEDIA_TYPE),
SupportedArtifacts::Par => (
PROVIDER_ARCHIVE_MEDIA_TYPE,
PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE,
),
};
let mut config_buf = vec![];
match options.config {
Some(config_file) => {
let mut f = File::open(config_file).await?;
f.read_to_end(&mut config_buf).await?;
}
None => {
config_buf = b"{}".to_vec();
}
};
let config = Config {
data: config_buf,
media_type: config_media_type.to_string(),
annotations: None,
};
let layer = vec![ImageLayer {
data: artifact_buf,
media_type: artifact_media_type.to_string(),
annotations: None,
}];
let mut client = Client::new(ClientConfig {
protocol: if options.insecure {
ClientProtocol::Http
} else {
ClientProtocol::Https
},
..Default::default()
});
let auth = match (options.user, options.password) {
(Some(user), Some(password)) => RegistryAuth::Basic(user, password),
_ => RegistryAuth::Anonymous,
};
let manifest = OciImageManifest::build(&layer, &config, options.annotations);
client
.push(&image, &layer, config, &auth, Some(manifest))
.await?;
Ok(())
}
pub async fn validate_artifact(artifact: &[u8]) -> Result<SupportedArtifacts> {
match validate_actor_module(artifact) {
Ok(_) => Ok(SupportedArtifacts::Wasm),
Err(_) => match validate_provider_archive(artifact).await {
Ok(_) => Ok(SupportedArtifacts::Par),
Err(_) => bail!("Unsupported artifact type"),
},
}
}
fn validate_actor_module(artifact: &[u8]) -> Result<()> {
match wascap::wasm::extract_claims(artifact) {
Ok(Some(_token)) => Ok(()),
Ok(None) => bail!("No capabilities discovered in actor module"),
Err(e) => Err(anyhow!("{}", e)),
}
}
async fn validate_provider_archive(artifact: &[u8]) -> Result<()> {
match ProviderArchive::try_load(artifact).await {
Ok(_par) => Ok(()),
Err(e) => bail!("Invalid provider archive: {}", e),
}
}