1use std::collections::BTreeMap;
9
10use super::error::DefError;
11use super::interp::{self, Reference};
12use super::model::{Integration, Service, StackDef};
13
14impl StackDef {
15 pub fn validate_hosts(&self, known_substrates: &[&str]) -> Result<(), DefError> {
18 validate_definition(self, known_substrates)
19 }
20
21 pub fn validate_for_substrate(&self, substrate: &str) -> Result<(), DefError> {
24 for (name, service) in &self.services {
25 if !service.substrates.contains_key(substrate) {
26 return Err(DefError::SubstrateConfigMissing {
27 service: name.clone(),
28 substrate: substrate.to_owned(),
29 });
30 }
31 }
32 Ok(())
33 }
34}
35
36fn validate_definition(def: &StackDef, known_substrates: &[&str]) -> Result<(), DefError> {
37 if !crate::types::dns_safe(def.stack.name.as_str()) {
38 return Err(DefError::NameInvalid {
39 kind: "stack",
40 name: def.stack.name.as_str().to_owned(),
41 });
42 }
43 if def.services.is_empty() {
44 return Err(DefError::NoServices);
45 }
46
47 validate_substrate_keys(&def.stack.substrates, "stack", known_substrates)?;
48 validate_integrations(def, known_substrates)?;
49
50 let mut root_origins = Vec::new();
51 for (name, service) in &def.services {
52 if !crate::types::dns_safe(name) {
53 return Err(DefError::NameInvalid {
54 kind: "service",
55 name: name.clone(),
56 });
57 }
58 if service.root_origin {
59 root_origins.push(name.clone());
60 }
61 validate_substrate_keys(
62 &service.substrates,
63 &format!("services.{name}"),
64 known_substrates,
65 )?;
66 validate_service_references(def, name, service, known_substrates)?;
67 }
68 if root_origins.len() > 1 {
69 return Err(DefError::RootOriginConflict {
70 services: root_origins,
71 });
72 }
73
74 if let Some(verify) = &def.stack.verify {
75 for (key, value) in &verify.env {
76 let location = format!("stack.verify.env.{key}");
77 let refs = interp::references(value, &location)?;
78 validate_references(def, &refs, &location)?;
79 }
80 for (tier, spec) in &verify.tiers {
81 if !crate::types::dns_safe(tier) {
82 return Err(DefError::NameInvalid {
83 kind: "verify tier",
84 name: tier.clone(),
85 });
86 }
87 for (key, value) in &spec.env {
88 let location = format!("stack.verify.tiers.{tier}.env.{key}");
89 let refs = interp::references(value, &location)?;
90 validate_references(def, &refs, &location)?;
91 }
92 }
93 }
94
95 Ok(())
96}
97
98fn validate_substrate_keys(
99 substrates: &BTreeMap<String, toml::Value>,
100 location: &str,
101 known_substrates: &[&str],
102) -> Result<(), DefError> {
103 for (key, value) in substrates {
104 if key == "depends_on" {
105 return Err(DefError::DependsOnRejected {
108 location: location.to_owned(),
109 });
110 }
111 if !known_substrates.contains(&key.as_str()) {
112 return Err(DefError::UnknownKey {
113 location: location.to_owned(),
114 key: key.clone(),
115 known_substrates: known_substrates.iter().map(|s| (*s).to_owned()).collect(),
116 });
117 }
118 if !value.is_table() {
119 return Err(DefError::SubstrateBlockInvalid {
120 location: format!("{location}.{key}"),
121 found: value.type_str().to_owned(),
122 });
123 }
124 }
125 Ok(())
126}
127
128fn validate_service_references(
129 def: &StackDef,
130 name: &str,
131 service: &Service,
132 known_substrates: &[&str],
133) -> Result<(), DefError> {
134 for key in &service.secrets {
137 if !def.secrets.required.contains(key) {
138 return Err(DefError::SecretNotRequired {
139 location: format!("services.{name}.secrets"),
140 key: key.clone(),
141 });
142 }
143 }
144 for (key, value) in &service.env {
145 let location = format!("services.{name}.env.{key}");
146 let refs = interp::references(value, &location)?;
147 validate_references(def, &refs, &location)?;
148 }
149 for substrate in known_substrates {
152 let overlay = service.substrate_env(name, substrate)?;
153 for (key, value) in &overlay {
154 let location = format!("services.{name}.{substrate}.env.{key}");
155 let refs = interp::references(value, &location)?;
156 validate_references(def, &refs, &location)?;
157 }
158 }
159 Ok(())
160}
161
162fn validate_integrations(def: &StackDef, known_substrates: &[&str]) -> Result<(), DefError> {
163 for (name, integration) in &def.integrations {
164 if !crate::types::dns_safe(name) {
165 return Err(DefError::NameInvalid {
166 kind: "integration",
167 name: name.clone(),
168 });
169 }
170 if integration.provider.is_empty() {
171 return Err(DefError::IntegrationInvalid {
172 integration: name.clone(),
173 detail: "provider is required".into(),
174 });
175 }
176 validate_integration_substrate_keys(name, integration, known_substrates)?;
177 validate_integration_string_refs(def, name, integration, known_substrates)?;
178 }
179 Ok(())
180}
181
182fn validate_integration_substrate_keys(
183 name: &str,
184 integration: &Integration,
185 known_substrates: &[&str],
186) -> Result<(), DefError> {
187 let substrates: std::collections::BTreeMap<String, toml::Value> = integration
188 .fields
189 .iter()
190 .filter(|(key, _)| known_substrates.contains(&key.as_str()))
191 .map(|(key, value)| (key.clone(), value.clone()))
192 .collect();
193 validate_substrate_keys(
194 &substrates,
195 &format!("integrations.{name}"),
196 known_substrates,
197 )
198}
199
200fn validate_integration_string_refs(
201 def: &StackDef,
202 name: &str,
203 integration: &Integration,
204 known_substrates: &[&str],
205) -> Result<(), DefError> {
206 for (key, value) in integration.config_fields(known_substrates) {
207 let Some(text) = value.as_str() else {
208 continue;
209 };
210 let location = format!("integrations.{name}.{key}");
211 let refs = interp::references(text, &location)?;
212 validate_references(def, &refs, &location)?;
213 }
214 for substrate in known_substrates {
215 let Some(block) = integration.host_block(substrate) else {
216 continue;
217 };
218 for (key, value) in block {
219 let Some(text) = value.as_str() else {
220 continue;
221 };
222 let location = format!("integrations.{name}.{substrate}.{key}");
223 let refs = interp::references(text, &location)?;
224 validate_references(def, &refs, &location)?;
225 }
226 }
227 Ok(())
228}
229
230fn validate_references(def: &StackDef, refs: &[Reference], location: &str) -> Result<(), DefError> {
231 for reference in refs {
232 match reference {
233 Reference::StackName | Reference::InstanceName => {}
234 Reference::ServiceOrigin(target) => {
235 if !def.services.contains_key(target) {
236 return Err(DefError::UndeclaredReference {
237 location: location.to_owned(),
238 kind: "service",
239 name: target.clone(),
240 });
241 }
242 }
243 Reference::DatastoreUrl(target) => {
244 if !def.legacy_datastores.contains(target) {
247 return Err(DefError::UndeclaredReference {
248 location: location.to_owned(),
249 kind: "datastore",
250 name: target.clone(),
251 });
252 }
253 }
254 Reference::Secret(key) => {
255 if !def.secrets.required.contains(key) {
256 return Err(DefError::SecretNotRequired {
257 location: location.to_owned(),
258 key: key.clone(),
259 });
260 }
261 }
262 Reference::IntegrationOutput { integration, .. } => {
263 if !def.integrations.contains_key(integration) {
264 return Err(DefError::UndeclaredReference {
265 location: location.to_owned(),
266 kind: "integration",
267 name: integration.clone(),
268 });
269 }
270 }
271 }
272 }
273 Ok(())
274}
275
276#[cfg(test)]
277mod tests {
278 use crate::def::StackDef;
279 use crate::types::dns_safe;
280
281 #[test]
282 fn dns_safety() {
283 assert!(dns_safe("atto"));
284 assert!(dns_safe("a1-b2"));
285 assert!(!dns_safe(""));
286 assert!(!dns_safe("Atto"));
287 assert!(!dns_safe("1atto"));
288 assert!(!dns_safe("atto-"));
289 assert!(!dns_safe("at to"));
290 assert!(!dns_safe(&"a".repeat(64)));
291 }
292
293 #[test]
294 fn verify_tier_keys_must_be_dns_safe() {
295 let text = r#"
296[stack]
297name = "bad"
298
299[stack.verify.tiers."a);func"]
300run = "true"
301
302[services.web]
303source = { repo = "https://example.invalid/x", ref = "main" }
304health = { path = "/" }
305
306[services.web.local]
307run = "true"
308"#;
309 let def = StackDef::parse(text).expect("parse");
310 let err = def.validate_hosts(&["local"]).expect_err("tier name");
311 assert!(matches!(
312 err,
313 crate::def::DefError::NameInvalid {
314 kind: "verify tier",
315 ..
316 }
317 ));
318 }
319}