oci_wasm/
config.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use std::collections::BTreeMap;

use anyhow::Context;
use chrono::{DateTime, Utc};
use oci_client::client::{Config, ImageLayer};
use serde::{Deserialize, Serialize};
use sha2::Digest;

use crate::{
    Component, COMPONENT_OS, MODULE_OS, WASM_ARCHITECTURE, WASM_LAYER_MEDIA_TYPE,
    WASM_MANIFEST_CONFIG_MEDIA_TYPE,
};

// A convenience trait that indicates a type can be converted into an OCI manifest config
pub trait ToConfig {
    /// Convert the type into an OCI manifest config
    fn to_config(&self) -> anyhow::Result<Config>;
}

/// The config type struct for `application/wasm`
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct WasmConfig {
    /// The time when the config was created.
    pub created: DateTime<Utc>,
    /// The optional name of the author of the config.
    pub author: Option<String>,
    /// The architecture of the artifact. This is always `wasm`.
    pub architecture: String,
    /// The OS name of the artifact. Possible options: wasip1, wasip2. For plain wasm, this should
    /// be wasip1 as this must match a GOOS value and it doesn’t have one for plain Wasm
    ///
    /// Eventually this will go away when we hit a 1.0 but we need it for now
    pub os: String,
    /// This field contains a list of digests of each of the layers from the manifest in the same
    /// order as they are listed in the manfiest. This exists because we need to have a unique list
    /// here so that the hash of the config (used as the ID) is unique every time
    /// (https://github.com/opencontainers/image-spec/pull/1173)
    pub layer_digests: Vec<String>,
    /// Information about the component in the manifest. This is required when the `os` field is
    /// `wasip2`
    pub component: Option<Component>,
}

pub struct AnnotatedWasmConfig<'a> {
    pub config: &'a WasmConfig,
    pub annotations: BTreeMap<String, String>,
}

impl WasmConfig {
    /// A helper for loading a component from a file and returning the proper config and
    /// [`ImageLayer`]. The returned config will have the created time set to now and all other
    /// fields set for a component.
    pub async fn from_component(
        path: impl AsRef<std::path::Path>,
        author: Option<String>,
    ) -> anyhow::Result<(Self, ImageLayer)> {
        let raw = tokio::fs::read(path).await.context("Unable to read file")?;
        Self::from_raw_component(raw, author)
    }

    /// Same as [`WasmConfig::from_component`] but for raw component bytes
    pub fn from_raw_component(
        raw: Vec<u8>,
        author: Option<String>,
    ) -> anyhow::Result<(Self, ImageLayer)> {
        let component = Component::from_raw_component(&raw)?;
        let config = Self {
            created: Utc::now(),
            author,
            architecture: WASM_ARCHITECTURE.to_string(),
            os: COMPONENT_OS.to_string(),
            layer_digests: vec![sha256_digest(&raw)],
            component: Some(component),
        };
        Ok((
            config,
            ImageLayer {
                data: raw,
                media_type: WASM_LAYER_MEDIA_TYPE.to_string(),
                annotations: None,
            },
        ))
    }

    /// A helper for loading a plain wasm module and returning the proper config and [`ImageLayer`].
    /// The returned config will have the created time set to now and all other fields set for a
    /// plain wasm module.
    pub async fn from_module(
        path: impl AsRef<std::path::Path>,
        author: Option<String>,
    ) -> anyhow::Result<(Self, ImageLayer)> {
        let raw = tokio::fs::read(path).await.context("Unable to read file")?;
        Self::from_raw_module(raw, author)
    }

    /// Same as [`WasmConfig::from_module`] but for raw module bytes
    pub fn from_raw_module(
        raw: Vec<u8>,
        author: Option<String>,
    ) -> anyhow::Result<(Self, ImageLayer)> {
        let config = Self {
            created: Utc::now(),
            author,
            architecture: WASM_ARCHITECTURE.to_string(),
            os: MODULE_OS.to_string(),
            layer_digests: vec![sha256_digest(&raw)],
            component: None,
        };
        Ok((
            config,
            ImageLayer {
                data: raw,
                media_type: WASM_LAYER_MEDIA_TYPE.to_string(),
                annotations: None,
            },
        ))
    }

    /// Adds annotations to this [`WasmConfig`].
    #[must_use]
    pub fn with_annotations(
        &'_ self,
        annotations: BTreeMap<String, String>,
    ) -> AnnotatedWasmConfig<'_> {
        AnnotatedWasmConfig {
            config: self,
            annotations,
        }
    }
}

impl<'a> ToConfig for AnnotatedWasmConfig<'a> {
    /// Generate a [`Config`] for this [`WasmConfig`]
    fn to_config(&self) -> anyhow::Result<Config> {
        let mut config = self.config.to_config()?;
        config.annotations = Some(self.annotations.clone());
        Ok(config)
    }
}

impl ToConfig for WasmConfig {
    /// Generate a [`Config`] for this [`WasmConfig`]
    fn to_config(&self) -> anyhow::Result<Config> {
        serde_json::to_vec(self)
            .map(|data| Config {
                data,
                media_type: WASM_MANIFEST_CONFIG_MEDIA_TYPE.to_string(),
                annotations: None,
            })
            .map_err(Into::into)
    }
}

// NOTE: There are a bunch of implementations here because we can't do a generic implementation
// across T for AsRef<[u8]>

impl TryFrom<String> for WasmConfig {
    type Error = anyhow::Error;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        serde_json::from_str(&value).map_err(Into::into)
    }
}

impl TryFrom<Vec<u8>> for WasmConfig {
    type Error = anyhow::Error;

    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
        serde_json::from_slice(&value).map_err(Into::into)
    }
}

impl TryFrom<&str> for WasmConfig {
    type Error = anyhow::Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        serde_json::from_str(value).map_err(Into::into)
    }
}

impl TryFrom<&[u8]> for WasmConfig {
    type Error = anyhow::Error;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        serde_json::from_slice(value).map_err(Into::into)
    }
}

fn sha256_digest(bytes: &[u8]) -> String {
    format!("sha256:{:x}", sha2::Sha256::digest(bytes))
}