mod healthcheck;
mod http;
pub use self::{
healthcheck::{HealthCheckHttpV1, HealthCheckV1},
http::HttpRequest,
};
use std::collections::HashMap;
use anyhow::{bail, Context};
use bytesize::ByteSize;
use crate::package::PackageSource;
#[allow(clippy::declare_interior_mutable_const)]
pub const HEADER_APP_VERSION_ID: &str = "x-edge-app-version-id";
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppConfigV1 {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
pub package: PackageSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub domains: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locality: Option<Locality>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli_args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<AppConfigCapabilityMapV1>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheduled_tasks: Option<Vec<AppScheduledTask>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volumes: Option<Vec<AppVolume>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub health_checks: Option<Vec<HealthCheckV1>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub debug: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scaling: Option<AppScalingConfigV1>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redirect: Option<Redirect>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct Locality {
pub regions: Vec<String>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppScalingConfigV1 {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<AppScalingModeV1>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub enum AppScalingModeV1 {
#[serde(rename = "single_concurrency")]
SingleConcurrency,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppVolume {
pub name: String,
pub mount: String,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppScheduledTask {
pub name: String,
}
impl AppConfigV1 {
pub const KIND: &'static str = "wasmer.io/App.v0";
pub const CANONICAL_FILE_NAME: &'static str = "app.yaml";
pub fn to_yaml_value(self) -> Result<serde_yaml::Value, serde_yaml::Error> {
let obj = match serde_yaml::to_value(self)? {
serde_yaml::Value::Mapping(m) => m,
_ => unreachable!(),
};
let mut m = serde_yaml::Mapping::new();
m.insert("kind".into(), Self::KIND.into());
for (k, v) in obj.into_iter() {
m.insert(k, v);
}
Ok(m.into())
}
pub fn to_yaml(self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(&self.to_yaml_value()?)
}
pub fn parse_yaml(value: &str) -> Result<Self, anyhow::Error> {
let raw = serde_yaml::from_str::<serde_yaml::Value>(value).context("invalid yaml")?;
let kind = raw
.get("kind")
.context("invalid app config: no 'kind' field found")?
.as_str()
.context("invalid app config: 'kind' field is not a string")?;
match kind {
Self::KIND => {}
other => {
bail!(
"invalid app config: unspported kind '{}', expected {}",
other,
Self::KIND
);
}
}
let data = serde_yaml::from_value(raw).context("could not deserialize app config")?;
Ok(data)
}
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppConfigCapabilityMapV1 {
#[serde(skip_serializing_if = "Option::is_none")]
pub memory: Option<AppConfigCapabilityMemoryV1>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instaboot: Option<AppConfigCapabilityInstaBootV1>,
#[serde(flatten)]
pub other: HashMap<String, serde_json::Value>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppConfigCapabilityMemoryV1 {
#[schemars(with = "Option<String>")]
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<ByteSize>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct AppConfigCapabilityInstaBootV1 {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requests: Vec<HttpRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_age: Option<String>,
}
#[derive(
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
)]
pub struct Redirect {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub force_https: Option<bool>,
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_app_config_v1_deser() {
let config = r#"
kind: wasmer.io/App.v0
name: test
package: ns/name@0.1.0
debug: true
env:
e1: v1
E2: V2
cli_args:
- arg1
- arg2
locality:
regions:
- eu-rome
redirect:
force_https: true
scheduled_tasks:
- name: backup
schedule: 1day
max_retries: 3
timeout: 10m
invoke:
fetch:
url: /api/do-backup
headers:
h1: v1
success_status_codes: [200, 201]
"#;
let parsed = AppConfigV1::parse_yaml(config).unwrap();
assert_eq!(
parsed,
AppConfigV1 {
name: "test".to_string(),
app_id: None,
package: "ns/name@0.1.0".parse().unwrap(),
owner: None,
domains: None,
env: [
("e1".to_string(), "v1".to_string()),
("E2".to_string(), "V2".to_string())
]
.into_iter()
.collect(),
volumes: None,
cli_args: Some(vec!["arg1".to_string(), "arg2".to_string()]),
capabilities: None,
scaling: None,
scheduled_tasks: Some(vec![AppScheduledTask {
name: "backup".to_string(),
}]),
health_checks: None,
extra: [(
"kind".to_string(),
serde_json::Value::from("wasmer.io/App.v0")
),]
.into_iter()
.collect(),
debug: Some(true),
redirect: Some(Redirect {
force_https: Some(true)
}),
locality: Some(Locality {
regions: vec!["eu-rome".to_string()]
})
}
);
}
#[test]
fn test_app_config_v1_volumes() {
let config = r#"
kind: wasmer.io/App.v0
name: test
package: ns/name@0.1.0
volumes:
- name: vol1
mount: /vol1
- name: vol2
mount: /vol2
"#;
let parsed = AppConfigV1::parse_yaml(config).unwrap();
let expected_volumes = vec![
AppVolume {
name: "vol1".to_string(),
mount: "/vol1".to_string(),
},
AppVolume {
name: "vol2".to_string(),
mount: "/vol2".to_string(),
},
];
if let Some(actual_volumes) = parsed.volumes {
assert_eq!(actual_volumes, expected_volumes);
} else {
panic!(
"Parsed volumes are None, expected Some({:?})",
expected_volumes
);
}
}
}