oci_wasm/
client.rs

1use std::{collections::BTreeMap, ops::Deref};
2
3use oci_client::{
4    client::{ImageData, ImageLayer, PushResponse},
5    manifest::OciImageManifest,
6    secrets::RegistryAuth,
7    Client, Reference,
8};
9
10use crate::{
11    config::ToConfig, WasmConfig, WASM_LAYER_MEDIA_TYPE, WASM_MANIFEST_CONFIG_MEDIA_TYPE,
12    WASM_MANIFEST_MEDIA_TYPE,
13};
14
15/// A light wrapper around the oci-distribution client to add support for the `application/wasm` type
16pub struct WasmClient {
17    client: Client,
18}
19
20impl AsRef<Client> for WasmClient {
21    fn as_ref(&self) -> &Client {
22        &self.client
23    }
24}
25
26impl Deref for WasmClient {
27    type Target = Client;
28
29    fn deref(&self) -> &Self::Target {
30        &self.client
31    }
32}
33
34impl From<Client> for WasmClient {
35    fn from(value: Client) -> Self {
36        Self { client: value }
37    }
38}
39
40impl From<WasmClient> for Client {
41    fn from(value: WasmClient) -> Self {
42        value.client
43    }
44}
45
46impl WasmClient {
47    /// Create a new client
48    pub fn new(client: Client) -> Self {
49        Self::from(client)
50    }
51
52    /// A convenience wrapper around [`Client::pull`] that pulls a wasm component and errors if
53    /// there are layers that aren't wasm
54    pub async fn pull(&self, image: &Reference, auth: &RegistryAuth) -> anyhow::Result<ImageData> {
55        let image_data = self
56            .client
57            .pull(image, auth, vec![WASM_LAYER_MEDIA_TYPE])
58            .await?;
59        if image_data.layers.len() != 1 {
60            anyhow::bail!("Wasm components must have exactly one layer");
61        }
62
63        if image_data.config.media_type != WASM_MANIFEST_CONFIG_MEDIA_TYPE {
64            anyhow::bail!(
65                "Wasm components must have a config of type {}",
66                WASM_MANIFEST_CONFIG_MEDIA_TYPE
67            );
68        }
69
70        Ok(image_data)
71    }
72
73    /// A convenience wrapper around [`Client::pull_manifest_and_config`] that parses the config as
74    /// a [`WasmConfig`] type
75    pub async fn pull_manifest_and_config(
76        &self,
77        image: &Reference,
78        auth: &RegistryAuth,
79    ) -> anyhow::Result<(OciImageManifest, WasmConfig, String)> {
80        let (manifest, digest, config) = self.client.pull_manifest_and_config(image, auth).await?;
81        if manifest.layers.len() != 1 {
82            anyhow::bail!("Wasm components must have exactly one layer");
83        }
84        if manifest.media_type.as_deref().unwrap_or_default() != WASM_MANIFEST_MEDIA_TYPE {
85            anyhow::bail!(
86                "Wasm components must have a manifest of type {}",
87                WASM_MANIFEST_MEDIA_TYPE
88            );
89        }
90
91        if manifest.config.media_type != WASM_MANIFEST_CONFIG_MEDIA_TYPE {
92            anyhow::bail!(
93                "Wasm components must have a config of type {}",
94                WASM_MANIFEST_CONFIG_MEDIA_TYPE
95            );
96        }
97
98        let config = WasmConfig::try_from(config)?;
99        Ok((manifest, config, digest))
100    }
101
102    /// A convenience wrapper around [`Client::push`] that pushes a wasm component or module with
103    /// the given config and optional annotations for the manifest
104    pub async fn push(
105        &self,
106        image: &Reference,
107        auth: &RegistryAuth,
108        component_layer: ImageLayer,
109        config: impl ToConfig,
110        annotations: Option<BTreeMap<String, String>>,
111    ) -> anyhow::Result<PushResponse> {
112        let layers = vec![component_layer];
113        let config = config.to_config()?;
114        let mut manifest = OciImageManifest::build(&layers, &config, annotations);
115        manifest.media_type = Some(WASM_MANIFEST_MEDIA_TYPE.to_string());
116        self.client
117            .push(image, &layers, config, auth, Some(manifest))
118            .await
119            .map_err(Into::into)
120    }
121}