1use std::{
4 collections::BTreeMap,
5 path::{Path, PathBuf},
6};
7
8use anyhow::{bail, Context as _, Result};
9use oci_client::manifest::OciImageManifest;
10use oci_client::{
11 client::{Client, ClientConfig, ClientProtocol, Config, ImageLayer},
12 secrets::RegistryAuth,
13 Reference,
14};
15use oci_wasm::{ToConfig, WasmConfig, WASM_LAYER_MEDIA_TYPE, WASM_MANIFEST_MEDIA_TYPE};
16use provider_archive::ProviderArchive;
17use sha2::Digest;
18use tokio::fs::File;
19use tokio::io::AsyncReadExt;
20use wasmcloud_core::tls;
21
22const PROVIDER_ARCHIVE_MEDIA_TYPE: &str = "application/vnd.wasmcloud.provider.archive.layer.v1+par";
23const PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE: &str =
24 "application/vnd.wasmcloud.provider.archive.config";
25const WASM_MEDIA_TYPE: &str = "application/vnd.module.wasm.content.layer.v1+wasm";
26const OCI_MEDIA_TYPE: &str = "application/vnd.oci.image.layer.v1.tar";
27
28#[derive(Default)]
30pub struct OciPullOptions {
31 pub digest: Option<String>,
33 pub allow_latest: bool,
35 pub user: Option<String>,
37 pub password: Option<String>,
39 pub insecure: bool,
41 pub insecure_skip_tls_verify: bool,
43}
44
45#[derive(Default)]
47pub struct OciPushOptions {
48 pub config: Option<PathBuf>,
50 pub allow_latest: bool,
52 pub user: Option<String>,
54 pub password: Option<String>,
56 pub insecure: bool,
58 pub insecure_skip_tls_verify: bool,
60 pub annotations: Option<BTreeMap<String, String>>,
62 pub monolithic_push: bool,
64}
65
66pub enum SupportedArtifacts {
68 Par(Config, ImageLayer),
70 Wasm(Config, ImageLayer),
72}
73
74pub enum ArtifactType {
76 Par,
77 Wasm,
78}
79
80fn sha256_digest(bytes: &[u8]) -> String {
85 format!("sha256:{:x}", sha2::Sha256::digest(bytes))
86}
87
88pub async fn get_oci_artifact(
96 url_or_file: String,
97 cache_file: Option<PathBuf>,
98 options: OciPullOptions,
99) -> Result<Vec<u8>> {
100 if let Ok(mut local_artifact) = File::open(&url_or_file).await {
101 let mut buf = Vec::new();
102 local_artifact.read_to_end(&mut buf).await?;
103 return Ok(buf);
104 } else if let Some(cache_path) = cache_file {
105 if let Ok(mut cached_artifact) = File::open(cache_path).await {
106 let mut buf = Vec::new();
107 cached_artifact.read_to_end(&mut buf).await?;
108 return Ok(buf);
109 }
110 }
111 pull_oci_artifact(
112 &url_or_file
113 .try_into()
114 .context("Unable to parse URL as a reference")?,
115 options,
116 )
117 .await
118}
119
120pub async fn pull_oci_artifact(image_ref: &Reference, options: OciPullOptions) -> Result<Vec<u8>> {
122 let input_tag = image_ref.tag();
123
124 if !options.allow_latest {
125 if let Some(tag) = input_tag {
126 if tag == "latest" {
127 bail!("Pulling artifacts with tag 'latest' is prohibited. This can be overridden with the flag '--allow-latest'.");
128 }
129 } else {
130 bail!("Registry URLs must have explicit tag. To default missing tags to 'latest', use the flag '--allow-latest'.");
131 }
132 }
133
134 let client = Client::new(ClientConfig {
135 protocol: if options.insecure {
136 ClientProtocol::Http
137 } else {
138 ClientProtocol::Https
139 },
140 extra_root_certificates: tls::NATIVE_ROOTS_OCI.to_vec(),
141 accept_invalid_certificates: options.insecure_skip_tls_verify,
142 ..Default::default()
143 });
144
145 let auth = match (options.user, options.password) {
146 (Some(user), Some(password)) => RegistryAuth::Basic(user, password),
147 _ => RegistryAuth::Anonymous,
148 };
149
150 let image_data = client
151 .pull(
152 image_ref,
153 &auth,
154 vec![
155 PROVIDER_ARCHIVE_MEDIA_TYPE,
156 WASM_MEDIA_TYPE,
157 OCI_MEDIA_TYPE,
158 WASM_LAYER_MEDIA_TYPE,
159 ],
160 )
161 .await?;
162
163 let digest = match options.digest {
165 Some(d) if d.starts_with("sha256:") => Some(d),
166 Some(d) => Some(format!("sha256:{d}")),
167 None => None,
168 };
169
170 match (digest, image_data.digest) {
171 (Some(digest), Some(image_digest)) if digest != image_digest => {
172 bail!("image digest did not match provided digest, aborting")
173 }
174 _ => (),
175 };
176
177 Ok(image_data
178 .layers
179 .iter()
180 .flat_map(|l| l.data.clone())
181 .collect::<Vec<_>>())
182}
183
184pub async fn push_oci_artifact(
186 url: String,
187 artifact: impl AsRef<Path>,
188 options: OciPushOptions,
189) -> Result<(Option<String>, String)> {
190 let image: Reference = url.to_lowercase().parse()?;
191
192 if image.tag().unwrap_or_default() == "latest" && !options.allow_latest {
193 bail!("Pushing artifacts with tag 'latest' is prohibited");
194 };
195
196 let mut artifact_buf = vec![];
197 let mut f = File::open(&artifact)
198 .await
199 .with_context(|| format!("failed to open artifact [{}]", artifact.as_ref().display()))?;
200 f.read_to_end(&mut artifact_buf).await?;
201
202 let (config, layer, is_wasm) = match parse_and_validate_artifact(&artifact_buf).await? {
203 SupportedArtifacts::Wasm(conf, layer) => (conf, layer, true),
204 SupportedArtifacts::Par(mut conf, layer) => {
205 let mut config_buf = vec![];
206 match options.config {
207 Some(config_file) => {
208 let mut f = File::open(&config_file).await.with_context(|| {
209 format!("failed to open config file [{}]", config_file.display())
210 })?;
211 f.read_to_end(&mut config_buf).await?;
212 }
213 None => {
214 config_buf = b"{}".to_vec();
216 }
217 };
218 conf.data = config_buf;
219 (conf, layer, false)
220 }
221 };
222
223 let layers = vec![layer];
224
225 let client = Client::new(ClientConfig {
226 protocol: if options.insecure {
227 ClientProtocol::Http
228 } else {
229 ClientProtocol::Https
230 },
231 extra_root_certificates: tls::NATIVE_ROOTS_OCI.to_vec(),
232 accept_invalid_certificates: options.insecure_skip_tls_verify,
233 use_monolithic_push: options.monolithic_push,
234 ..Default::default()
235 });
236
237 let auth = match (options.user, options.password) {
238 (Some(user), Some(password)) => RegistryAuth::Basic(user, password),
239 _ => RegistryAuth::Anonymous,
240 };
241
242 let mut manifest = OciImageManifest::build(&layers, &config, options.annotations);
243 if is_wasm {
244 manifest.media_type = Some(WASM_MANIFEST_MEDIA_TYPE.to_string());
245 }
246 let digest =
257 serde_json::to_value(&manifest).map(|value| sha256_digest(value.to_string().as_bytes()))?;
258
259 client
260 .push(&image, &layers, config, &auth, Some(manifest))
261 .await?;
262 Ok((image.tag().map(ToString::to_string), digest))
263}
264
265pub async fn parse_and_validate_artifact(artifact: &[u8]) -> Result<SupportedArtifacts> {
268 match parse_component(artifact.to_owned()) {
272 Ok(art) => Ok(art),
273 Err(_) => match parse_provider_archive(artifact).await {
274 Ok(art) => Ok(art),
275 Err(_) => bail!("Unsupported artifact type"),
276 },
277 }
278}
279
280pub async fn identify_artifact(artifact: &[u8]) -> Result<ArtifactType> {
284 if wasmparser::Parser::is_component(artifact) {
285 return Ok(ArtifactType::Wasm);
286 }
287 parse_provider_archive(artifact)
288 .await
289 .map(|_| ArtifactType::Par)
290}
291
292fn parse_component(artifact: Vec<u8>) -> Result<SupportedArtifacts> {
294 let (conf, layer) = WasmConfig::from_raw_component(artifact, None)?;
295 Ok(SupportedArtifacts::Wasm(conf.to_config()?, layer))
296}
297
298async fn parse_provider_archive(artifact: &[u8]) -> Result<SupportedArtifacts> {
300 match ProviderArchive::try_load(artifact).await {
301 Ok(_par) => Ok(SupportedArtifacts::Par(
302 Config {
303 data: Vec::default(),
304 media_type: PROVIDER_ARCHIVE_CONFIG_MEDIA_TYPE.to_string(),
305 annotations: None,
306 },
307 ImageLayer {
308 data: artifact.to_owned(),
309 media_type: PROVIDER_ARCHIVE_MEDIA_TYPE.to_string(),
310 annotations: None,
311 },
312 )),
313 Err(e) => bail!("Invalid provider archive: {}", e),
314 }
315}