1use std::{collections::HashSet, path::PathBuf};
4
5use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};
6use anyhow::Context;
7
8pub fn normalize_manifest(manifest: &mut AppManifest, profile: Option<&str>) -> anyhow::Result<()> {
13 normalize_trigger_ids(manifest);
14 normalize_inline_components(manifest);
15 apply_profile_overrides(manifest, profile);
16 normalize_dependency_inherit_configuration(manifest)?;
17 normalize_dependency_component_refs(manifest)?;
18 Ok(())
19}
20
21fn normalize_inline_components(manifest: &mut AppManifest) {
22 let components = &mut manifest.components;
24
25 for trigger in manifest.triggers.values_mut().flatten() {
26 let trigger_id = &trigger.id;
27
28 let component_specs = trigger
29 .component
30 .iter_mut()
31 .chain(
32 trigger
33 .components
34 .values_mut()
35 .flat_map(|specs| specs.0.iter_mut()),
36 )
37 .collect::<Vec<_>>();
38 let multiple_components = component_specs.len() > 1;
39
40 let mut counter = 1;
41 for spec in component_specs {
42 if !matches!(spec, ComponentSpec::Inline(_)) {
43 continue;
44 };
45
46 let inline_id = {
47 let mut id = KebabId::try_from(format!("{trigger_id}-component"));
49 if multiple_components
51 || id.is_err()
52 || components.contains_key(id.as_ref().unwrap())
53 {
54 id = Ok(loop {
55 let id = KebabId::try_from(format!("inline-component{counter}")).unwrap();
56 if !components.contains_key(&id) {
57 break id;
58 }
59 counter += 1;
60 });
61 }
62 id.unwrap()
63 };
64
65 let inline_spec = std::mem::replace(spec, ComponentSpec::Reference(inline_id.clone()));
67 let ComponentSpec::Inline(component) = inline_spec else {
68 unreachable!();
69 };
70 components.insert(inline_id.clone(), *component);
72 }
73 }
74}
75
76fn normalize_trigger_ids(manifest: &mut AppManifest) {
77 let mut trigger_ids = manifest
78 .triggers
79 .values()
80 .flatten()
81 .cloned()
82 .map(|t| t.id)
83 .collect::<HashSet<_>>();
84 for (trigger_type, triggers) in &mut manifest.triggers {
85 let mut counter = 1;
86 for trigger in triggers {
87 if !trigger.id.is_empty() {
88 continue;
89 }
90 if let Some(ComponentSpec::Reference(component_id)) = &trigger.component {
92 let candidate_id = format!("{component_id}-{trigger_type}-trigger");
93 if !trigger_ids.contains(&candidate_id) {
94 trigger.id.clone_from(&candidate_id);
95 trigger_ids.insert(candidate_id);
96 continue;
97 }
98 }
99 trigger.id = loop {
101 let id = format!("{trigger_type}-trigger{counter}");
102 if !trigger_ids.contains(&id) {
103 trigger_ids.insert(id.clone());
104 break id;
105 }
106 counter += 1;
107 }
108 }
109 }
110}
111
112fn apply_profile_overrides(manifest: &mut AppManifest, profile: Option<&str>) {
113 let Some(profile) = profile else {
114 return;
115 };
116
117 for (_, component) in &mut manifest.components {
118 let Some(overrides) = component.profile.get(profile) else {
119 continue;
120 };
121
122 if let Some(profile_build) = overrides.build.as_ref() {
123 match component.build.as_mut() {
124 None => {
125 component.build = Some(crate::schema::v2::ComponentBuildConfig {
126 command: profile_build.command.clone(),
127 workdir: None,
128 watch: vec![],
129 })
130 }
131 Some(build) => {
132 build.command = profile_build.command.clone();
133 }
134 }
135 }
136
137 if let Some(source) = overrides.source.as_ref() {
138 component.source = source.clone();
139 }
140
141 component.environment.extend(overrides.environment.clone());
142
143 component
144 .dependencies
145 .inner
146 .extend(overrides.dependencies.inner.clone());
147 }
148}
149
150use crate::schema::v2::{Component, ComponentDependency, ComponentSource, InheritConfiguration};
151
152fn normalize_dependency_inherit_configuration(manifest: &mut AppManifest) -> anyhow::Result<()> {
156 for (component_id, component) in &mut manifest.components {
157 let component_level = component.dependencies_inherit_configuration;
158
159 let has_per_dep = component
160 .dependencies
161 .inner
162 .values()
163 .any(|dep| dep.inherit_configuration().is_some());
164
165 if component_level.is_some() && has_per_dep {
166 anyhow::bail!(
167 "Component `{component_id}` specifies both `dependencies_inherit_configuration` \
168 and per-dependency `inherit_configuration`. These are mutually exclusive; \
169 use one or the other."
170 );
171 }
172
173 if component_level == Some(true) {
174 let inherit = InheritConfiguration::All(true);
175 for dep in component.dependencies.inner.values_mut() {
176 dep.set_inherit_configuration(inherit.clone());
177 }
178 component.dependencies_inherit_configuration = None;
179 }
180 }
181
182 Ok(())
183}
184
185fn normalize_dependency_component_refs(manifest: &mut AppManifest) -> anyhow::Result<()> {
186 let components = manifest.components.clone();
190
191 for (depender_id, component) in &mut manifest.components {
192 for dependency in component.dependencies.inner.values_mut() {
193 if let ComponentDependency::AppComponent {
194 component: depended_on_id,
195 export,
196 inherit_configuration,
197 } = dependency
198 {
199 let depended_on = components
200 .get(depended_on_id)
201 .with_context(|| format!("dependency ID {depended_on_id} does not exist"))?;
202 ensure_is_acceptable_dependency(depended_on, depended_on_id, depender_id)?;
203 *dependency = component_source_to_dependency(
204 &depended_on.source,
205 export.clone(),
206 inherit_configuration.clone(),
207 );
208 }
209 }
210 }
211
212 Ok(())
213}
214
215fn component_source_to_dependency(
216 source: &ComponentSource,
217 export: Option<String>,
218 inherit_configuration: Option<InheritConfiguration>,
219) -> ComponentDependency {
220 match source {
221 ComponentSource::Local(path) => ComponentDependency::Local {
222 path: PathBuf::from(path),
223 export,
224 inherit_configuration,
225 },
226 ComponentSource::Remote { url, digest } => ComponentDependency::HTTP {
227 url: url.clone(),
228 digest: digest.clone(),
229 export,
230 inherit_configuration,
231 },
232 ComponentSource::Registry {
233 registry,
234 package,
235 version,
236 } => ComponentDependency::Package {
237 version: version.clone(),
238 registry: registry.as_ref().map(|r| r.to_string()),
239 package: Some(package.to_string()),
240 export,
241 inherit_configuration,
242 },
243 }
244}
245
246fn ensure_is_acceptable_dependency(
250 component: &Component,
251 depended_on_id: &KebabId,
252 depender_id: &KebabId,
253) -> anyhow::Result<()> {
254 let mut surprises = vec![];
255
256 #[allow(deprecated)]
260 let Component {
261 source: _,
262 description: _,
263 variables,
264 environment,
265 files,
266 exclude_files: _,
267 allowed_http_hosts,
268 allowed_outbound_hosts,
269 key_value_stores,
270 sqlite_databases,
271 ai_models,
272 targets: _,
273 build: _,
274 tool: _,
275 dependencies_inherit_configuration: _,
276 dependencies,
277 profile: _,
278 } = component;
279
280 if !ai_models.is_empty() {
281 surprises.push("ai_models");
282 }
283 if !allowed_http_hosts.is_empty() {
284 surprises.push("allowed_http_hosts");
285 }
286 if !allowed_outbound_hosts.is_empty() {
287 surprises.push("allowed_outbound_hosts");
288 }
289 if !dependencies.inner.is_empty() {
290 surprises.push("dependencies");
291 }
292 if !environment.is_empty() {
293 surprises.push("environment");
294 }
295 if !files.is_empty() {
296 surprises.push("files");
297 }
298 if !key_value_stores.is_empty() {
299 surprises.push("key_value_stores");
300 }
301 if !sqlite_databases.is_empty() {
302 surprises.push("sqlite_databases");
303 }
304 if !variables.is_empty() {
305 surprises.push("variables");
306 }
307
308 if surprises.is_empty() {
309 Ok(())
310 } else {
311 anyhow::bail!(
312 "Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {}",
313 surprises.join(", ")
314 );
315 }
316}
317
318#[cfg(test)]
319mod test {
320 use super::*;
321
322 use crate::schema::v2::InheritConfiguration;
323 use serde::Deserialize;
324 use toml::toml;
325
326 fn package_name(name: &str) -> spin_serde::DependencyName {
327 let dpn = spin_serde::DependencyPackageName::try_from(name.to_string()).unwrap();
328 spin_serde::DependencyName::Package(dpn)
329 }
330
331 #[test]
332 fn can_resolve_dependency_on_file_source() {
333 let mut manifest = AppManifest::deserialize(toml! {
334 spin_manifest_version = 2
335
336 [application]
337 name = "dummy"
338
339 [[trigger.dummy]]
340 component = "a"
341
342 [component.a]
343 source = "a.wasm"
344 [component.a.dependencies]
345 "b:b" = { component = "b" }
346
347 [component.b]
348 source = "b.wasm"
349 })
350 .unwrap();
351
352 normalize_manifest(&mut manifest, None).unwrap();
353
354 let dep = manifest
355 .components
356 .get("a")
357 .unwrap()
358 .dependencies
359 .inner
360 .get(&package_name("b:b"))
361 .unwrap();
362
363 let ComponentDependency::Local {
364 path,
365 export,
366 inherit_configuration,
367 } = dep
368 else {
369 panic!("should have normalised to local dep");
370 };
371
372 assert_eq!(&PathBuf::from("b.wasm"), path);
373 assert_eq!(&None, export);
374 assert!(inherit_configuration.is_none());
375 }
376
377 #[test]
378 fn can_resolve_dependency_on_http_source() {
379 let mut manifest = AppManifest::deserialize(toml! {
380 spin_manifest_version = 2
381
382 [application]
383 name = "dummy"
384
385 [[trigger.dummy]]
386 component = "a"
387
388 [component.a]
389 source = "a.wasm"
390 [component.a.dependencies]
391 "b:b" = { component = "b", export = "c:d/e" }
392
393 [component.b]
394 source = { url = "http://example.com/b.wasm", digest = "12345" }
395 })
396 .unwrap();
397
398 normalize_manifest(&mut manifest, None).unwrap();
399
400 let dep = manifest
401 .components
402 .get("a")
403 .unwrap()
404 .dependencies
405 .inner
406 .get(&package_name("b:b"))
407 .unwrap();
408
409 let ComponentDependency::HTTP {
410 url,
411 digest,
412 export,
413 inherit_configuration,
414 } = dep
415 else {
416 panic!("should have normalised to HTTP dep");
417 };
418
419 assert_eq!("http://example.com/b.wasm", url);
420 assert_eq!("12345", digest);
421 assert_eq!("c:d/e", export.as_ref().unwrap());
422 assert!(inherit_configuration.is_none());
423 }
424
425 #[test]
426 fn can_resolve_dependency_on_package() {
427 let mut manifest = AppManifest::deserialize(toml! {
428 spin_manifest_version = 2
429
430 [application]
431 name = "dummy"
432
433 [[trigger.dummy]]
434 component = "a"
435
436 [component.a]
437 source = "a.wasm"
438 [component.a.dependencies]
439 "b:b" = { component = "b" }
440
441 [component.b]
442 source = { package = "bb:bb", version = "1.2.3", registry = "reginalds-registry.reg" }
443 })
444 .unwrap();
445
446 normalize_manifest(&mut manifest, None).unwrap();
447
448 let dep = manifest
449 .components
450 .get("a")
451 .unwrap()
452 .dependencies
453 .inner
454 .get(&package_name("b:b"))
455 .unwrap();
456
457 let ComponentDependency::Package {
458 version,
459 registry,
460 package,
461 export,
462 inherit_configuration,
463 } = dep
464 else {
465 panic!("should have normalised to package dep");
466 };
467
468 assert_eq!("1.2.3", version);
469 assert_eq!("reginalds-registry.reg", registry.as_ref().unwrap());
470 assert_eq!("bb:bb", package.as_ref().unwrap());
471 assert_eq!(&None, export);
472 assert!(inherit_configuration.is_none());
473 }
474
475 #[test]
476 fn can_resolve_dependency_with_inherit() {
477 let mut manifest = AppManifest::deserialize(toml! {
478 spin_manifest_version = 2
479
480 [application]
481 name = "dummy"
482
483 [[trigger.dummy]]
484 component = "a"
485
486 [component.a]
487 source = "a.wasm"
488 [component.a.dependencies]
489 "b:b" = { component = "b", inherit_configuration = true }
490
491 [component.b]
492 source = "b.wasm"
493 })
494 .unwrap();
495
496 normalize_manifest(&mut manifest, None).unwrap();
497
498 let dep = manifest
499 .components
500 .get("a")
501 .unwrap()
502 .dependencies
503 .inner
504 .get(&package_name("b:b"))
505 .unwrap();
506
507 let ComponentDependency::Local {
508 path,
509 export,
510 inherit_configuration,
511 } = dep
512 else {
513 panic!("should have normalised to local dep");
514 };
515
516 assert_eq!(&PathBuf::from("b.wasm"), path);
517 assert_eq!(&None, export);
518 assert!(matches!(
519 inherit_configuration,
520 Some(InheritConfiguration::All(true))
521 ));
522 }
523
524 #[test]
525 fn can_resolve_dependency_with_inherit_some() {
526 let mut manifest = AppManifest::deserialize(toml! {
527 spin_manifest_version = 2
528
529 [application]
530 name = "dummy"
531
532 [[trigger.dummy]]
533 component = "a"
534
535 [component.a]
536 source = "a.wasm"
537 [component.a.dependencies]
538 "b:b" = { component = "b", inherit_configuration = ["ai_models", "allowed_outbound_hosts"] }
539
540 [component.b]
541 source = "b.wasm"
542 })
543 .unwrap();
544
545 normalize_manifest(&mut manifest, None).unwrap();
546
547 let dep = manifest
548 .components
549 .get("a")
550 .unwrap()
551 .dependencies
552 .inner
553 .get(&package_name("b:b"))
554 .unwrap();
555
556 let ComponentDependency::Local {
557 path,
558 export,
559 inherit_configuration,
560 } = dep
561 else {
562 panic!("should have normalised to local dep");
563 };
564
565 assert_eq!(&PathBuf::from("b.wasm"), path);
566 assert_eq!(&None, export);
567 let Some(InheritConfiguration::Some(keys)) = inherit_configuration else {
568 panic!("should have inherit_configuration = Some([...])");
569 };
570 assert_eq!(
571 &vec![
572 "ai_models".to_string(),
573 "allowed_outbound_hosts".to_string()
574 ],
575 keys
576 );
577 }
578}