edge_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: &'static str = "wasmer.io/app_id";
21
22    /// Well-known annotation for specifying the app version id.
23    pub const ANNOTATION_BACKEND_APP_VERSION_ID: &'static 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    /// Domains where the app is accessible.
86    #[serde(default)]
87    #[serde(skip_serializing_if = "Vec::is_empty")]
88    pub domains: Vec<String>,
89
90    /// The primary workload to execute.
91    pub workload: WorkloadV2,
92}
93
94#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
95pub struct AppStateV1 {}
96
97impl EntityDescriptorConst for AppV1Spec {
98    const NAMESPACE: &'static str = "wasmer.io";
99    const NAME: &'static str = "App";
100    const VERSION: &'static str = "1-alpha1";
101    const KIND: &'static str = "wasmer.io/App.v1";
102
103    type Spec = AppV1Spec;
104    type State = AppStateV1;
105}
106
107pub type AppV1 = super::Entity<AppV1Spec, AnyEntity>;
108
109#[cfg(test)]
110mod tests {
111    use pretty_assertions::assert_eq;
112
113    use crate::schema::{EntityMeta, EnvVarV1};
114
115    use super::*;
116
117    /// Tests serilization and deserialization of the [`AppV1`] struct.
118    #[test]
119    fn test_deser_app_v1_sparse() {
120        let inp = r#"
121kind: wasmer.io/App.v1
122meta:
123  name: my-app
124spec:
125  workload:
126    source: theduke/amaze
127  domains:
128    - a.com
129    - b.com
130"#;
131
132        let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
133
134        assert_eq!(
135            a1,
136            AppV1 {
137                meta: EntityMeta::new("my-app"),
138                spec: AppV1Spec {
139                    aliases: Vec::new(),
140                    domains: vec!["a.com".to_string(), "b.com".to_string()],
141                    workload: crate::schema::WorkloadV2 {
142                        source: "theduke/amaze".parse().unwrap(),
143                        capabilities: Default::default(),
144                    },
145                },
146                children: None,
147            },
148        );
149    }
150
151    #[test]
152    fn test_deser_app_v1_full() {
153        let inp = r#"
154kind: wasmer.io/App.v1
155meta:
156  name: my-app
157  description: hello
158  labels:
159    "my/label": "value"
160  annotations:
161    "my/annotation": {nested: [1, 2, 3]}
162spec:
163  aliases:
164    - a
165    - b
166  workload:
167    source: "theduke/my-app"
168"#;
169
170        let a1 = serde_yaml::from_str::<AppV1>(inp).unwrap();
171
172        let expected = AppV1 {
173            meta: EntityMeta {
174                uid: None,
175                name: "my-app".to_string(),
176                description: Some("hello".to_string()),
177                labels: vec![("my/label".to_string(), "value".to_string())]
178                    .into_iter()
179                    .collect(),
180                annotations: vec![(
181                    "my/annotation".to_string(),
182                    serde_json::json!({
183                        "nested": [1, 2, 3],
184                    }),
185                )]
186                .into_iter()
187                .collect(),
188                parent: None,
189            },
190            spec: AppV1Spec {
191                aliases: vec!["a".to_string(), "b".to_string()],
192                domains: Vec::new(),
193                workload: WorkloadV2 {
194                    source: "theduke/my-app".parse().unwrap(),
195                    capabilities: Default::default(),
196                },
197            },
198            children: None,
199        };
200
201        assert_eq!(a1, expected,);
202    }
203
204    #[test]
205    fn test_deser_app_v1_with_cli_and_env_cap() {
206        let raw = r#"
207kind: wasmer.io/App.v1
208meta:
209  description: ''
210  name: christoph/pyenv-dump
211spec:
212  workload:
213    capabilities:
214      wasi:
215        cli_args:
216        - /src/main.py
217        env_vars:
218        - name: PORT
219          value: '80'
220    source: wasmer-tests/python-env-dump@0.3.6
221
222"#;
223
224        let a1 = serde_yaml::from_str::<AppV1>(raw).unwrap();
225
226        let expected = AppV1 {
227            meta: EntityMeta {
228                uid: None,
229                name: "christoph/pyenv-dump".to_string(),
230                description: Some("".to_string()),
231                labels: Default::default(),
232                annotations: Default::default(),
233                parent: None,
234            },
235            spec: AppV1Spec {
236                aliases: Vec::new(),
237                domains: Vec::new(),
238                workload: WorkloadV2 {
239                    source: "wasmer-tests/python-env-dump@0.3.6".parse().unwrap(),
240                    capabilities: crate::schema::CapabilityMapV1 {
241                        wasi: Some(crate::schema::CapabilityWasiV1 {
242                            cli_args: Some(vec!["/src/main.py".to_string()]),
243                            env_vars: Some(vec![EnvVarV1 {
244                                name: "PORT".to_string(),
245                                source: crate::schema::EnvVarSourceV1::Value("80".to_string()),
246                            }]),
247                        }),
248                        ..Default::default()
249                    },
250                },
251            },
252            children: None,
253        };
254
255        assert_eq!(a1, expected,);
256    }
257}