noetl_executor/
capabilities.rs1use anyhow::Result;
10
11use crate::playbook::{Playbook, RuntimeCapabilities};
12
13#[derive(Debug, Clone)]
21pub struct ValidationReport {
22 pub playbook_name: String,
23 pub errors: Vec<ValidationError>,
24 pub warnings: Vec<String>,
25}
26
27impl ValidationReport {
28 pub fn is_ok(&self) -> bool {
29 self.errors.is_empty()
30 }
31}
32
33#[derive(Debug, Clone)]
34pub enum ValidationError {
35 IncompatibleProfile { required: String },
37 MissingTool { tool: String, supported: Vec<String> },
39 MissingFeature { feature: String, supported: Vec<String> },
41}
42
43pub fn validate_capabilities(
48 playbook: &Playbook,
49 runtime: &RuntimeCapabilities,
50) -> Result<ValidationReport> {
51 let mut report = ValidationReport {
52 playbook_name: playbook.metadata.name.clone(),
53 errors: Vec::new(),
54 warnings: Vec::new(),
55 };
56
57 let executor = match &playbook.executor {
58 Some(e) => e,
59 None => return Ok(report),
61 };
62
63 match executor.profile.as_str() {
65 "distributed" => {
66 if runtime.runtime != "distributed" {
67 report.errors.push(ValidationError::IncompatibleProfile {
68 required: "distributed".to_string(),
69 });
70 }
71 }
72 "local" | "auto" | "" => {
73 }
75 other => {
76 report.warnings.push(format!(
77 "Unknown executor profile '{}'; proceeding with '{}' runtime",
78 other, runtime.runtime
79 ));
80 }
81 }
82
83 if executor.version != runtime.version && !executor.version.is_empty() {
85 report.warnings.push(format!(
86 "Playbook requires '{}', runtime provides '{}'. Some features may not work as expected.",
87 executor.version, runtime.version,
88 ));
89 }
90
91 if let Some(requires) = &executor.requires {
93 for tool in &requires.tools {
94 if !runtime.tools.contains(tool) {
95 report.errors.push(ValidationError::MissingTool {
96 tool: tool.clone(),
97 supported: runtime.tools.clone(),
98 });
99 }
100 }
101 for feature in &requires.features {
102 if !runtime.features.contains(feature) {
103 report.errors.push(ValidationError::MissingFeature {
104 feature: feature.clone(),
105 supported: runtime.features.clone(),
106 });
107 }
108 }
109 }
110
111 Ok(report)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::playbook::{Executor, ExecutorRequires, Metadata, Playbook};
118
119 fn pb(executor: Option<Executor>) -> Playbook {
120 Playbook {
121 api_version: "noetl.io/v2".to_string(),
122 kind: "Playbook".to_string(),
123 metadata: Metadata {
124 name: "test".to_string(),
125 path: None,
126 },
127 executor,
128 workload: None,
129 workflow: Vec::new(),
130 }
131 }
132
133 #[test]
134 fn no_executor_block_is_ok() {
135 let playbook = pb(None);
136 let runtime = RuntimeCapabilities::local();
137 let report = validate_capabilities(&playbook, &runtime).unwrap();
138 assert!(report.is_ok());
139 }
140
141 #[test]
142 fn distributed_profile_fails_against_local_runtime() {
143 let playbook = pb(Some(Executor {
144 profile: "distributed".to_string(),
145 version: "".to_string(),
146 requires: None,
147 spec: None,
148 }));
149 let runtime = RuntimeCapabilities::local();
150 let report = validate_capabilities(&playbook, &runtime).unwrap();
151 assert!(!report.is_ok());
152 assert!(matches!(
153 report.errors[0],
154 ValidationError::IncompatibleProfile { .. }
155 ));
156 }
157
158 #[test]
159 fn missing_tool_is_an_error() {
160 let playbook = pb(Some(Executor {
161 profile: "local".to_string(),
162 version: "noetl-runtime/1".to_string(),
163 requires: Some(ExecutorRequires {
164 tools: vec!["nonexistent".to_string()],
165 features: Vec::new(),
166 }),
167 spec: None,
168 }));
169 let runtime = RuntimeCapabilities::local();
170 let report = validate_capabilities(&playbook, &runtime).unwrap();
171 assert!(!report.is_ok());
172 assert!(matches!(report.errors[0], ValidationError::MissingTool { .. }));
173 }
174
175 #[test]
176 fn auto_profile_is_compatible_with_local() {
177 let playbook = pb(Some(Executor {
178 profile: "auto".to_string(),
179 version: "".to_string(),
180 requires: None,
181 spec: None,
182 }));
183 let runtime = RuntimeCapabilities::local();
184 let report = validate_capabilities(&playbook, &runtime).unwrap();
185 assert!(report.is_ok());
186 }
187}