mirror of
https://github.com/mii443/wasmer.git
synced 2025-12-03 11:18:31 +00:00
Implement app.yaml job schema
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -1096,6 +1096,18 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "cron"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbe58904b68d95ad2c69a2e9607ffc70ce196c1910f83a340029c1448e06ed65"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"winnow 0.6.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.14"
|
||||
@@ -6919,6 +6931,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bytesize",
|
||||
"ciborium",
|
||||
"cron",
|
||||
"derive_builder",
|
||||
"hex",
|
||||
"indexmap 2.7.0",
|
||||
|
||||
@@ -67,6 +67,15 @@
|
||||
"$ref": "#/definitions/HealthCheckV1"
|
||||
}
|
||||
},
|
||||
"jobs": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Job"
|
||||
}
|
||||
},
|
||||
"locality": {
|
||||
"description": "Location-related configuration for the app.",
|
||||
"anyOf": [
|
||||
@@ -246,6 +255,85 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExecutableJob": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExecutableJobCompatibilityMapV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cli_args": {
|
||||
"description": "CLI arguments passed to the runner. Only applicable for runners that accept CLI arguments.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"command": {
|
||||
"description": "The command to run. Defaults to the package's entrypoint.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"description": "Environment variables.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"package": {
|
||||
"description": "The package that contains the command to run. Defaults to the app config's package.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PackageSource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"volumes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppVolume"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExecutableJobCompatibilityMapV1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory": {
|
||||
"description": "Instance memory settings.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppConfigCapabilityMemoryV1"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"HealthCheckHttpV1": {
|
||||
"description": "Health check configuration for http endpoints.",
|
||||
"type": "object",
|
||||
@@ -443,6 +531,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Job": {
|
||||
"description": "Job configuration.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"trigger"
|
||||
],
|
||||
"properties": {
|
||||
"execute": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExecutableJob"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fetch": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HttpRequest"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"trigger": {
|
||||
"$ref": "#/definitions/JobTrigger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"JobTrigger": {
|
||||
"type": "string"
|
||||
},
|
||||
"Locality": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -27,6 +27,7 @@ schemars = { version = "0.8.16", features = ["url"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
hex = "0.4.3"
|
||||
ciborium = "0.2.2"
|
||||
cron = { version = "0.14.0", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
239
lib/config/src/app/job.rs
Normal file
239
lib/config/src/app/job.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use std::{borrow::Cow, collections::HashMap, fmt::Display, str::FromStr};
|
||||
|
||||
use serde::{de::Error, Deserialize, Serialize};
|
||||
|
||||
use crate::package::PackageSource;
|
||||
|
||||
use super::{AppConfigCapabilityMemoryV1, AppVolume, HttpRequest};
|
||||
|
||||
/// Job configuration.
|
||||
#[derive(
|
||||
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
|
||||
)]
|
||||
pub struct Job {
|
||||
name: String,
|
||||
trigger: JobTrigger,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fetch: Option<HttpRequest>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
execute: Option<ExecutableJob>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
|
||||
)]
|
||||
pub enum CronType {
|
||||
Daily,
|
||||
Hourly,
|
||||
Weekly,
|
||||
CronExpression(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum JobTrigger {
|
||||
PreDeployment,
|
||||
PostDeployment,
|
||||
Cron(CronType),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
|
||||
)]
|
||||
pub struct ExecutableJob {
|
||||
/// The package that contains the command to run. Defaults to the app config's package.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
package: Option<PackageSource>,
|
||||
|
||||
/// The command to run. Defaults to the package's entrypoint.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
command: Option<String>,
|
||||
|
||||
/// CLI arguments passed to the runner.
|
||||
/// Only applicable for runners that accept CLI arguments.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cli_args: Option<Vec<String>>,
|
||||
|
||||
/// Environment variables.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub capabilities: Option<ExecutableJobCompatibilityMapV1>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<AppVolume>>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
serde::Serialize, serde::Deserialize, schemars::JsonSchema, Clone, Debug, PartialEq, Eq,
|
||||
)]
|
||||
pub struct ExecutableJobCompatibilityMapV1 {
|
||||
/// Instance memory settings.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub memory: Option<AppConfigCapabilityMemoryV1>,
|
||||
|
||||
/// Additional unknown capabilities.
|
||||
///
|
||||
/// This provides a small bit of forwards compatibility for newly added
|
||||
/// capabilities.
|
||||
#[serde(flatten)]
|
||||
pub other: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Serialize for JobTrigger {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for JobTrigger {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let repr: Cow<'de, str> = Cow::deserialize(deserializer)?;
|
||||
repr.parse().map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for JobTrigger {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::PreDeployment => write!(f, "pre-deployment"),
|
||||
Self::PostDeployment => write!(f, "post-deployment"),
|
||||
Self::Cron(CronType::Hourly) => write!(f, "@hourly"),
|
||||
Self::Cron(CronType::Daily) => write!(f, "@daily"),
|
||||
Self::Cron(CronType::Weekly) => write!(f, "@weekly"),
|
||||
Self::Cron(CronType::CronExpression(sched)) => write!(f, "{}", sched),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for JobTrigger {
|
||||
type Err = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == "pre-deployment" {
|
||||
Ok(Self::PreDeployment)
|
||||
} else if s == "post-deployment" {
|
||||
Ok(Self::PostDeployment)
|
||||
} else if let Some(predefined_sched) = s.strip_prefix('@') {
|
||||
match predefined_sched {
|
||||
"hourly" => Ok(Self::Cron(CronType::Hourly)),
|
||||
"daily" => Ok(Self::Cron(CronType::Daily)),
|
||||
"weekly" => Ok(Self::Cron(CronType::Weekly)),
|
||||
_ => Err(format!("Invalid cron expression {s}").into()),
|
||||
}
|
||||
} else {
|
||||
// Let's make sure the input string is valid...
|
||||
match cron::Schedule::from_str(s) {
|
||||
Ok(sched) => Ok(Self::Cron(CronType::CronExpression(
|
||||
sched.source().to_owned(),
|
||||
))),
|
||||
Err(_) => Err(format!("Invalid cron expression {s}").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl schemars::JsonSchema for JobTrigger {
|
||||
fn schema_name() -> String {
|
||||
"JobTrigger".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
String::json_schema(gen)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::JobTrigger;
|
||||
|
||||
use super::Job;
|
||||
|
||||
#[test]
|
||||
pub fn job_trigger_serialization_roundtrip() {
|
||||
fn assert_roundtrip(serialized: &str, value: JobTrigger) {
|
||||
assert_eq!(&value.to_string(), serialized);
|
||||
assert_eq!(serialized.parse::<JobTrigger>().unwrap(), value);
|
||||
}
|
||||
|
||||
assert_roundtrip("pre-deployment", JobTrigger::PreDeployment);
|
||||
assert_roundtrip("post-deployment", JobTrigger::PostDeployment);
|
||||
|
||||
assert_roundtrip("@hourly", JobTrigger::Cron(crate::app::CronType::Hourly));
|
||||
assert_roundtrip("@daily", JobTrigger::Cron(crate::app::CronType::Daily));
|
||||
assert_roundtrip("@weekly", JobTrigger::Cron(crate::app::CronType::Weekly));
|
||||
|
||||
// Note: the parsing code should keep the formatting of the source string.
|
||||
// This is tested in assert_roundtrip.
|
||||
assert_roundtrip(
|
||||
"0 0/2 12 ? JAN-APR 2",
|
||||
JobTrigger::Cron(crate::app::CronType::CronExpression(
|
||||
"0 0/2 12 ? JAN-APR 2".to_owned(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn job_serialization_roundtrip() {
|
||||
let job = Job {
|
||||
name: "my-job".to_owned(),
|
||||
trigger: JobTrigger::Cron(super::CronType::CronExpression(
|
||||
"0 0/2 12 ? JAN-APR 2".to_owned(),
|
||||
)),
|
||||
fetch: None,
|
||||
execute: Some(super::ExecutableJob {
|
||||
package: Some(crate::package::PackageSource::Ident(
|
||||
crate::package::PackageIdent::Named(crate::package::NamedPackageIdent {
|
||||
registry: None,
|
||||
namespace: Some("ns".to_owned()),
|
||||
name: "pkg".to_owned(),
|
||||
tag: None,
|
||||
}),
|
||||
)),
|
||||
command: Some("cmd".to_owned()),
|
||||
cli_args: Some(vec!["arg-1".to_owned(), "arg-2".to_owned()]),
|
||||
env: Some([("VAR1".to_owned(), "Value".to_owned())].into()),
|
||||
capabilities: Some(super::ExecutableJobCompatibilityMapV1 {
|
||||
memory: Some(crate::app::AppConfigCapabilityMemoryV1 {
|
||||
limit: Some(bytesize::ByteSize::gb(1)),
|
||||
}),
|
||||
other: Default::default(),
|
||||
}),
|
||||
volumes: Some(vec![crate::app::AppVolume {
|
||||
name: "vol".to_owned(),
|
||||
mount: "/path/to/volume".to_owned(),
|
||||
}]),
|
||||
}),
|
||||
};
|
||||
|
||||
let serialized = r#"
|
||||
name: my-job
|
||||
trigger: '0 0/2 12 ? JAN-APR 2'
|
||||
execute:
|
||||
package: ns/pkg
|
||||
command: cmd
|
||||
cli_args:
|
||||
- arg-1
|
||||
- arg-2
|
||||
env:
|
||||
VAR1: Value
|
||||
capabilities:
|
||||
memory:
|
||||
limit: '1000.0 MB'
|
||||
volumes:
|
||||
- name: vol
|
||||
mount: /path/to/volume"#;
|
||||
|
||||
assert_eq!(
|
||||
serialized.trim(),
|
||||
serde_yaml::to_string(&job).unwrap().trim()
|
||||
);
|
||||
assert_eq!(job, serde_yaml::from_str(serialized).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
mod healthcheck;
|
||||
mod http;
|
||||
mod job;
|
||||
|
||||
pub use self::{
|
||||
healthcheck::{HealthCheckHttpV1, HealthCheckV1},
|
||||
http::HttpRequest,
|
||||
};
|
||||
pub use self::{healthcheck::*, http::*, job::*};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -95,6 +93,9 @@ pub struct AppConfigV1 {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub redirect: Option<Redirect>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jobs: Option<Vec<Job>>,
|
||||
|
||||
/// Capture extra fields for forwards compatibility.
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
@@ -341,7 +342,8 @@ scheduled_tasks:
|
||||
}),
|
||||
locality: Some(Locality {
|
||||
regions: vec!["eu-rome".to_string()]
|
||||
})
|
||||
}),
|
||||
jobs: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user