feat(wfe-yaml): services block in workflow YAML definitions

This commit is contained in:
2026-04-06 17:59:56 +01:00
parent e5db02b4f8
commit 84686672ea
2 changed files with 124 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ use std::time::Duration;
use serde::Serialize; use serde::Serialize;
use wfe_core::models::error_behavior::ErrorBehavior; use wfe_core::models::error_behavior::ErrorBehavior;
use wfe_core::models::service::{ReadinessCheck, ReadinessProbe, ServiceDefinition, ServicePort};
use wfe_core::models::workflow_definition::{StepOutcome, WorkflowDefinition, WorkflowStep}; use wfe_core::models::workflow_definition::{StepOutcome, WorkflowDefinition, WorkflowStep};
use wfe_core::traits::StepBody; use wfe_core::traits::StepBody;
@@ -53,6 +54,9 @@ pub fn compile(spec: &WorkflowSpec) -> Result<CompiledWorkflow, YamlWorkflowErro
compile_steps(&spec.steps, &mut definition, &mut factories, &mut next_id)?; compile_steps(&spec.steps, &mut definition, &mut factories, &mut next_id)?;
// Compile services.
definition.services = compile_services(&spec.services)?;
Ok(CompiledWorkflow { Ok(CompiledWorkflow {
definition, definition,
step_factories: factories, step_factories: factories,
@@ -922,6 +926,68 @@ fn build_kubernetes_config(
Ok((step_config, cluster_config)) Ok((step_config, cluster_config))
} }
fn compile_services(
yaml_services: &std::collections::HashMap<String, crate::schema::YamlService>,
) -> Result<Vec<ServiceDefinition>, YamlWorkflowError> {
let mut services = Vec::new();
for (name, yaml_svc) in yaml_services {
let readiness = yaml_svc.readiness.as_ref().map(|r| {
let check = if let Some(ref exec_cmd) = r.exec {
ReadinessCheck::Exec(exec_cmd.clone())
} else if let Some(tcp_port) = r.tcp {
ReadinessCheck::TcpSocket(tcp_port)
} else if let Some(ref http) = r.http {
ReadinessCheck::HttpGet {
port: http.port,
path: http.path.clone(),
}
} else {
// Default: TCP check on first port.
ReadinessCheck::TcpSocket(
yaml_svc.ports.first().copied().unwrap_or(0),
)
};
let interval_ms = r
.interval
.as_ref()
.and_then(|s| parse_duration_ms(s))
.unwrap_or(5000);
let timeout_ms = r
.timeout
.as_ref()
.and_then(|s| parse_duration_ms(s))
.unwrap_or(60000);
ReadinessProbe {
check,
interval_ms,
timeout_ms,
retries: r.retries.unwrap_or(12),
}
});
services.push(ServiceDefinition {
name: name.clone(),
image: yaml_svc.image.clone(),
ports: yaml_svc
.ports
.iter()
.map(|&p| ServicePort::tcp(p))
.collect(),
env: yaml_svc.env.clone(),
readiness,
command: yaml_svc.command.clone().unwrap_or_default(),
args: yaml_svc.args.clone().unwrap_or_default(),
memory: yaml_svc.memory.clone(),
cpu: yaml_svc.cpu.clone(),
});
}
Ok(services)
}
fn map_error_behavior(eb: &YamlErrorBehavior) -> Result<ErrorBehavior, YamlWorkflowError> { fn map_error_behavior(eb: &YamlErrorBehavior) -> Result<ErrorBehavior, YamlWorkflowError> {
match eb.behavior_type.as_str() { match eb.behavior_type.as_str() {
"retry" => { "retry" => {

View File

@@ -87,11 +87,69 @@ pub struct WorkflowSpec {
/// Typed output schema: { field_name: type_string }. /// Typed output schema: { field_name: type_string }.
#[serde(default)] #[serde(default)]
pub outputs: HashMap<String, String>, pub outputs: HashMap<String, String>,
/// Infrastructure services required by this workflow.
#[serde(default)]
pub services: HashMap<String, YamlService>,
/// Allow unknown top-level keys (e.g. `_templates`) for YAML anchors. /// Allow unknown top-level keys (e.g. `_templates`) for YAML anchors.
#[serde(flatten)] #[serde(flatten)]
pub _extra: HashMap<String, serde_yaml::Value>, pub _extra: HashMap<String, serde_yaml::Value>,
} }
/// A service definition in YAML format.
#[derive(Debug, Deserialize)]
pub struct YamlService {
pub image: String,
#[serde(default)]
pub ports: Vec<u16>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub readiness: Option<YamlReadiness>,
#[serde(default)]
pub memory: Option<String>,
#[serde(default)]
pub cpu: Option<String>,
#[serde(default)]
pub command: Option<Vec<String>>,
#[serde(default)]
pub args: Option<Vec<String>>,
}
/// Readiness probe configuration in YAML format.
#[derive(Debug, Deserialize)]
pub struct YamlReadiness {
/// Execute a command to check readiness.
#[serde(default)]
pub exec: Option<Vec<String>>,
/// Check if a TCP port is accepting connections.
#[serde(default)]
pub tcp: Option<u16>,
/// HTTP GET health check.
#[serde(default)]
pub http: Option<YamlHttpGet>,
/// Poll interval (e.g., "5s", "2s").
#[serde(default)]
pub interval: Option<String>,
/// Total timeout (e.g., "60s", "30s").
#[serde(default)]
pub timeout: Option<String>,
/// Max retries before giving up.
#[serde(default)]
pub retries: Option<u32>,
}
/// HTTP GET readiness check.
#[derive(Debug, Deserialize)]
pub struct YamlHttpGet {
pub port: u16,
#[serde(default = "default_health_path")]
pub path: String,
}
fn default_health_path() -> String {
"/".into()
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct YamlStep { pub struct YamlStep {
pub name: String, pub name: String,