wasmer_deploy_schema/schema/
app.rs

1use std::collections::HashMap;
2
3use anyhow::Context;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use super::{entity::EntityDescriptorConst, AnyEntity, Entity, WorkloadV2};
8
9pub const APP_ID_PREFIX: &str = "da_";
10
11/// Well-known annotations used by the backend.
12#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
13pub struct AppMeta {
14    pub app_id: String,
15    pub app_version_id: String,
16}
17
18impl AppMeta {
19    /// Well-known annotation for specifying the app id.
20    pub const ANNOTATION_BACKEND_APP_ID: &str = "wasmer.io/app_id";
21
22    /// Well-known annotation for specifying the app version id.
23    pub const ANNOTATION_BACKEND_APP_VERSION_ID: &str = "wasmer.io/app_version_id";
24
25    /// Extract annotations from a generic annotation map.
26    pub fn try_from_annotations(
27        map: &HashMap<String, serde_json::Value>,
28    ) -> Result<Self, anyhow::Error> {
29        let app_id = map
30            .get(Self::ANNOTATION_BACKEND_APP_ID)
31            .context("missing annotation for app id")?
32            .as_str()
33            .context("app id annotation is not a string")?
34            .to_string();
35
36        let app_version_id = map
37            .get(Self::ANNOTATION_BACKEND_APP_VERSION_ID)
38            .context("missing annotation for version id")?
39            .as_str()
40            .context("version id annotation is not a string")?
41            .to_string();
42
43        Ok(Self {
44            app_id,
45            app_version_id,
46        })
47    }
48
49    pub fn try_from_entity<T>(entity: &Entity<T>) -> Result<Self, anyhow::Error> {
50        Self::try_from_annotations(&entity.meta.annotations)
51    }
52
53    pub fn new(app_id: String, app_version_id: String) -> Self {
54        Self {
55            app_id,
56            app_version_id,
57        }
58    }
59
60    pub fn to_annotations_map(self) -> HashMap<String, serde_json::Value> {
61        let mut map = HashMap::new();
62        map.insert(
63            Self::ANNOTATION_BACKEND_APP_ID.to_string(),
64            serde_json::Value::String(self.app_id),
65        );
66        map.insert(
67            Self::ANNOTATION_BACKEND_APP_VERSION_ID.to_string(),
68            serde_json::Value::String(self.app_version_id),
69        );
70        map
71    }
72}
73
74/// Describes a backend application.
75///
76/// Will usually be converted from [`super::AppConfigV1`].
77#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
78pub struct AppV1Spec {
79    /// A list of alias names for the app.
80    /// Aliases can be used to access the app through app domains.
81    #[serde(default)]
82    #[serde(skip_serializing_if = "Vec::is_empty")]
83    pub aliases: Vec<String>,
84
85    /// The primary workload to execute.
86    pub workload: WorkloadV2,
87}
88
89#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
90pub struct AppStateV1 {}
91
92impl EntityDescriptorConst for AppV1Spec {
93    const NAMESPACE: &'static str = "wasmer.io";
94    const NAME: &'static str = "App";
95    const VERSION: &'static str = "1-alpha1";
96    const KIND: &'static str = "wasmer.io/App.v1";
97
98    type Spec = AppV1Spec;
99    type State = AppStateV1;
100}
101
102pub type AppV1 = super::Entity<AppV1Spec, AnyEntity>;
103
104#[cfg(test)]
105mod tests {
106    use pretty_assertions::assert_eq;
107
108    use crate::schema::{EntityMeta, EnvVarV1};
109
110    use super::*;
111
112    /// Tests serilization and deserialization of the [`AppV1`] struct.
113    #[test]
114    fn test_deser_app_v1_sparse() {
115        let inp = r#"
116kind: wasmer.io/App.v1
117meta:
118  name: my-app
119spec:
120  workload:
121    source: theduke/amaze
122"#;
123
124        let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
125
126        assert_eq!(
127            a1,
128            AppV1 {
129                meta: EntityMeta::new("my-app"),
130                spec: AppV1Spec {
131                    aliases: Vec::new(),
132                    workload: crate::schema::WorkloadV2 {
133                        source: "theduke/amaze".parse().unwrap(),
134                        capabilities: Default::default(),
135                    },
136                },
137                children: None,
138            },
139        );
140    }
141
142    #[test]
143    fn test_deser_app_v1_full() {
144        let inp = r#"
145kind: wasmer.io/App.v1
146meta:
147  name: my-app
148  description: hello
149  labels:
150    "my/label": "value"
151  annotations:
152    "my/annotation": {nested: [1, 2, 3]}
153spec:
154  aliases:
155    - a
156    - b
157  workload:
158    source: "theduke/my-app"
159"#;
160
161        let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
162
163        let expected = AppV1 {
164            meta: EntityMeta {
165                name: "my-app".to_string(),
166                description: Some("hello".to_string()),
167                labels: vec![("my/label".to_string(), "value".to_string())]
168                    .into_iter()
169                    .collect(),
170                annotations: vec![(
171                    "my/annotation".to_string(),
172                    serde_json::json!({
173                        "nested": [1, 2, 3],
174                    }),
175                )]
176                .into_iter()
177                .collect(),
178                parent: None,
179            },
180            spec: AppV1Spec {
181                aliases: vec!["a".to_string(), "b".to_string()],
182                workload: WorkloadV2 {
183                    source: "theduke/my-app".parse().unwrap(),
184                    capabilities: Default::default(),
185                },
186            },
187            children: None,
188        };
189
190        assert_eq!(a1, expected,);
191    }
192
193    #[test]
194    fn test_deser_app_v1_with_cli_and_env_cap() {
195        let raw = r#"
196kind: wasmer.io/App.v1
197meta:
198  description: ''
199  name: christoph/pyenv-dump
200spec:
201  workload:
202    capabilities:
203      wasi:
204        cli_args:
205        - /src/main.py
206        env_vars:
207        - name: PORT
208          value: '80'
209    source: wasmer-tests/python-env-dump@0.3.6
210
211"#;
212
213        let a1 = serde_yaml::from_str::<AppV1>(raw).unwrap();
214
215        let expected = AppV1 {
216            meta: EntityMeta {
217                name: "christoph/pyenv-dump".to_string(),
218                description: Some("".to_string()),
219                labels: Default::default(),
220                annotations: Default::default(),
221                parent: None,
222            },
223            spec: AppV1Spec {
224                aliases: Default::default(),
225                workload: WorkloadV2 {
226                    source: "wasmer-tests/python-env-dump@0.3.6".parse().unwrap(),
227                    capabilities: crate::schema::CapabilityMapV1 {
228                        wasi: Some(crate::schema::CapabilityWasiV1 {
229                            cli_args: Some(vec!["/src/main.py".to_string()]),
230                            env_vars: Some(vec![EnvVarV1 {
231                                name: "PORT".to_string(),
232                                source: crate::schema::EnvVarSourceV1::Value("80".to_string()),
233                            }]),
234                        }),
235                        ..Default::default()
236                    },
237                },
238            },
239            children: None,
240        };
241
242        assert_eq!(a1, expected,);
243    }
244}