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 for (key, value) in &spec.env {
82 let location = format!("stack.verify.tiers.{tier}.env.{key}");
83 let refs = interp::references(value, &location)?;
84 validate_references(def, &refs, &location)?;
85 }
86 }
87 }
88
89 Ok(())
90}
91
92fn validate_substrate_keys(
93 substrates: &BTreeMap<String, toml::Value>,
94 location: &str,
95 known_substrates: &[&str],
96) -> Result<(), DefError> {
97 for (key, value) in substrates {
98 if key == "depends_on" {
99 return Err(DefError::DependsOnRejected {
102 location: location.to_owned(),
103 });
104 }
105 if !known_substrates.contains(&key.as_str()) {
106 return Err(DefError::UnknownKey {
107 location: location.to_owned(),
108 key: key.clone(),
109 known_substrates: known_substrates.iter().map(|s| (*s).to_owned()).collect(),
110 });
111 }
112 if !value.is_table() {
113 return Err(DefError::SubstrateBlockInvalid {
114 location: format!("{location}.{key}"),
115 found: value.type_str().to_owned(),
116 });
117 }
118 }
119 Ok(())
120}
121
122fn validate_service_references(
123 def: &StackDef,
124 name: &str,
125 service: &Service,
126 known_substrates: &[&str],
127) -> Result<(), DefError> {
128 for key in &service.secrets {
131 if !def.secrets.required.contains(key) {
132 return Err(DefError::SecretNotRequired {
133 location: format!("services.{name}.secrets"),
134 key: key.clone(),
135 });
136 }
137 }
138 for (key, value) in &service.env {
139 let location = format!("services.{name}.env.{key}");
140 let refs = interp::references(value, &location)?;
141 validate_references(def, &refs, &location)?;
142 }
143 for substrate in known_substrates {
146 let overlay = service.substrate_env(name, substrate)?;
147 for (key, value) in &overlay {
148 let location = format!("services.{name}.{substrate}.env.{key}");
149 let refs = interp::references(value, &location)?;
150 validate_references(def, &refs, &location)?;
151 }
152 }
153 Ok(())
154}
155
156fn validate_integrations(def: &StackDef, known_substrates: &[&str]) -> Result<(), DefError> {
157 for (name, integration) in &def.integrations {
158 if !crate::types::dns_safe(name) {
159 return Err(DefError::NameInvalid {
160 kind: "integration",
161 name: name.clone(),
162 });
163 }
164 if integration.provider.is_empty() {
165 return Err(DefError::IntegrationInvalid {
166 integration: name.clone(),
167 detail: "provider is required".into(),
168 });
169 }
170 validate_integration_substrate_keys(name, integration, known_substrates)?;
171 validate_integration_string_refs(def, name, integration, known_substrates)?;
172 }
173 Ok(())
174}
175
176fn validate_integration_substrate_keys(
177 name: &str,
178 integration: &Integration,
179 known_substrates: &[&str],
180) -> Result<(), DefError> {
181 let substrates: std::collections::BTreeMap<String, toml::Value> = integration
182 .fields
183 .iter()
184 .filter(|(key, _)| known_substrates.contains(&key.as_str()))
185 .map(|(key, value)| (key.clone(), value.clone()))
186 .collect();
187 validate_substrate_keys(
188 &substrates,
189 &format!("integrations.{name}"),
190 known_substrates,
191 )
192}
193
194fn validate_integration_string_refs(
195 def: &StackDef,
196 name: &str,
197 integration: &Integration,
198 known_substrates: &[&str],
199) -> Result<(), DefError> {
200 for (key, value) in integration.config_fields(known_substrates) {
201 let Some(text) = value.as_str() else {
202 continue;
203 };
204 let location = format!("integrations.{name}.{key}");
205 let refs = interp::references(text, &location)?;
206 validate_references(def, &refs, &location)?;
207 }
208 for substrate in known_substrates {
209 let Some(block) = integration.host_block(substrate) else {
210 continue;
211 };
212 for (key, value) in block {
213 let Some(text) = value.as_str() else {
214 continue;
215 };
216 let location = format!("integrations.{name}.{substrate}.{key}");
217 let refs = interp::references(text, &location)?;
218 validate_references(def, &refs, &location)?;
219 }
220 }
221 Ok(())
222}
223
224fn validate_references(def: &StackDef, refs: &[Reference], location: &str) -> Result<(), DefError> {
225 for reference in refs {
226 match reference {
227 Reference::StackName | Reference::InstanceName => {}
228 Reference::ServiceOrigin(target) => {
229 if !def.services.contains_key(target) {
230 return Err(DefError::UndeclaredReference {
231 location: location.to_owned(),
232 kind: "service",
233 name: target.clone(),
234 });
235 }
236 }
237 Reference::DatastoreUrl(target) => {
238 if !def.legacy_datastores.contains(target) {
241 return Err(DefError::UndeclaredReference {
242 location: location.to_owned(),
243 kind: "datastore",
244 name: target.clone(),
245 });
246 }
247 }
248 Reference::Secret(key) => {
249 if !def.secrets.required.contains(key) {
250 return Err(DefError::SecretNotRequired {
251 location: location.to_owned(),
252 key: key.clone(),
253 });
254 }
255 }
256 Reference::IntegrationOutput { integration, .. } => {
257 if !def.integrations.contains_key(integration) {
258 return Err(DefError::UndeclaredReference {
259 location: location.to_owned(),
260 kind: "integration",
261 name: integration.clone(),
262 });
263 }
264 }
265 }
266 }
267 Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272 use crate::types::dns_safe;
273
274 #[test]
275 fn dns_safety() {
276 assert!(dns_safe("atto"));
277 assert!(dns_safe("a1-b2"));
278 assert!(!dns_safe(""));
279 assert!(!dns_safe("Atto"));
280 assert!(!dns_safe("1atto"));
281 assert!(!dns_safe("atto-"));
282 assert!(!dns_safe("at to"));
283 assert!(!dns_safe(&"a".repeat(64)));
284 }
285}