1use std::collections::HashMap;
4
5use anyhow::{bail, Context};
6use bytesize::ByteSize;
7
8use super::{CronJobSpecV1, StringWebcIdent};
9
10#[derive(
15 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
16)]
17pub struct AppConfigV1 {
18 pub name: String,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
29 pub app_id: Option<String>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
35 pub owner: Option<String>,
36
37 pub package: StringWebcIdent,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
45 pub domains: Option<Vec<String>>,
46
47 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
49 pub env: HashMap<String, String>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
54 pub cli_args: Option<Vec<String>>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub capabilities: Option<AppConfigCapabilityMapV1>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub scheduled_tasks: Option<Vec<AppScheduledTask>>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub volumes: Option<Vec<AppVolume>>,
64
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub debug: Option<bool>,
68
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub scaling: Option<AppScalingConfigV1>,
71
72 #[serde(flatten)]
74 pub extra: HashMap<String, serde_json::Value>,
75}
76
77#[derive(
78 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
79)]
80pub struct AppScalingConfigV1 {
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub concurrent_requests: Option<u64>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub max_instances_per_node: Option<u64>,
88}
89
90#[derive(
91 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
92)]
93pub struct AppVolume {
94 pub name: String,
95 pub mounts: Vec<AppVolumeMount>,
96}
97
98#[derive(
99 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
100)]
101pub struct AppVolumeMount {
102 pub mount_path: String,
104 pub sub_path: Option<String>,
106}
107
108#[derive(
109 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
110)]
111pub struct AppScheduledTask {
112 pub name: String,
113 #[serde(flatten)]
114 pub spec: CronJobSpecV1,
115}
116
117impl AppConfigV1 {
118 pub const KIND: &'static str = "wasmer.io/App.v0";
119 pub const CANONICAL_FILE_NAME: &'static str = "app.yaml";
120
121 pub fn to_yaml_value(self) -> Result<serde_yaml::Value, serde_yaml::Error> {
122 let obj = match serde_yaml::to_value(self)? {
125 serde_yaml::Value::Mapping(m) => m,
126 _ => unreachable!(),
127 };
128 let mut m = serde_yaml::Mapping::new();
129 m.insert("kind".into(), Self::KIND.into());
130 for (k, v) in obj.into_iter() {
131 m.insert(k, v);
132 }
133 Ok(m.into())
134 }
135
136 pub fn to_yaml(self) -> Result<String, serde_yaml::Error> {
137 serde_yaml::to_string(&self.to_yaml_value()?)
138 }
139
140 pub fn parse_yaml(value: &str) -> Result<Self, anyhow::Error> {
141 let raw = serde_yaml::from_str::<serde_yaml::Value>(value).context("invalid yaml")?;
142 let kind = raw
143 .get("kind")
144 .context("invalid app config: no 'kind' field found")?
145 .as_str()
146 .context("invalid app config: 'kind' field is not a string")?;
147 match kind {
148 Self::KIND => {}
149 other => {
150 bail!(
151 "invalid app config: unspported kind '{}', expected {}",
152 other,
153 Self::KIND
154 );
155 }
156 }
157
158 let data = serde_yaml::from_value(raw).context("could not deserialize app config")?;
159 Ok(data)
160 }
161}
162
163#[derive(
166 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
167)]
168pub struct AppConfigCapabilityMapV1 {
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub memory: Option<AppConfigCapabilityMemoryV1>,
172}
173
174#[derive(
180 serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
181)]
182pub struct AppConfigCapabilityMemoryV1 {
183 #[schemars(with = "Option<String>")]
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub limit: Option<ByteSize>,
189}
190
191#[cfg(test)]
192mod tests {
193 use pretty_assertions::assert_eq;
194
195 use super::*;
196
197 #[test]
198 fn test_app_config_v1_deser() {
199 let config = r#"
200kind: wasmer.io/App.v0
201name: test
202package: ns/name@0.1.0
203debug: true
204env:
205 e1: v1
206 E2: V2
207cli_args:
208 - arg1
209 - arg2
210scheduled_tasks:
211 - name: backup
212 schedule: 1day
213 max_retries: 3
214 timeout: 10m
215 invoke:
216 fetch:
217 url: /api/do-backup
218 headers:
219 h1: v1
220 success_status_codes: [200, 201]
221 "#;
222
223 let parsed = AppConfigV1::parse_yaml(config).unwrap();
224
225 assert_eq!(
226 parsed,
227 AppConfigV1 {
228 name: "test".to_string(),
229 app_id: None,
230 package: "ns/name@0.1.0".parse().unwrap(),
231 owner: None,
232 domains: None,
233 env: [
234 ("e1".to_string(), "v1".to_string()),
235 ("E2".to_string(), "V2".to_string())
236 ]
237 .into_iter()
238 .collect(),
239 volumes: None,
240 cli_args: Some(vec!["arg1".to_string(), "arg2".to_string()]),
241 capabilities: None,
242 scaling: None,
243 scheduled_tasks: Some(vec![AppScheduledTask {
244 name: "backup".to_string(),
245 spec: CronJobSpecV1 {
246 schedule: "1day".to_string(),
247 max_schedule_drift: None,
248 job: crate::schema::JobDefinition {
249 max_retries: Some(3),
250 timeout: Some(std::time::Duration::from_secs(10 * 60).into()),
251 invoke: crate::schema::JobInvoke::Fetch(
252 crate::schema::JobInvokeFetch {
253 url: "/api/do-backup".parse().unwrap(),
254 headers: Some(
255 [("h1".to_string(), "v1".to_string())]
256 .into_iter()
257 .collect()
258 ),
259 success_status_codes: Some(vec![200, 201]),
260 method: None,
261 }
262 )
263 },
264 }
265 }]),
266 extra: [(
267 "kind".to_string(),
268 serde_json::Value::from("wasmer.io/App.v0")
269 ),]
270 .into_iter()
271 .collect(),
272 debug: Some(true),
273 }
274 );
275 }
276}