1use std::collections::BTreeMap;
54use std::path::{Path, PathBuf};
55
56use anyhow::Context as _;
57
58use crate::config::{
59 live_taint_keys, private_ipv4_from_url, MachineConfig, MirrorConfig, MirrorProviderSlot,
60 Provider, ServiceConfig,
61};
62use crate::paths::{machines_dir, services_dir};
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct AliasSource {
67 pub service: String,
69 pub component_id: String,
71 pub workload_toml: PathBuf,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct AliasCollision {
78 pub alias: String,
79 pub first: AliasSource,
80 pub second: AliasSource,
81}
82
83impl AliasCollision {
84 pub fn message(&self) -> String {
86 format!(
87 "alias {:?} declared in both {} (component {}) and {} (component {})\n\
88 \u{2192} rename the alias in one of these files:\n {}\n {}",
89 self.alias,
90 self.first.service,
91 self.first.component_id,
92 self.second.service,
93 self.second.component_id,
94 self.first.workload_toml.display(),
95 self.second.workload_toml.display(),
96 )
97 }
98}
99
100pub fn check_alias_collisions(workspace_root: &Path) -> anyhow::Result<Vec<AliasCollision>> {
105 let dir = services_dir(workspace_root);
106 if !dir.exists() {
107 return Ok(vec![]);
108 }
109
110 let mut seen: BTreeMap<String, AliasSource> = BTreeMap::new();
112 let mut collisions = Vec::new();
113
114 let mut entries: Vec<_> = std::fs::read_dir(&dir)
115 .with_context(|| format!("reading {}", dir.display()))?
116 .filter_map(|e| e.ok())
117 .filter(|e| e.path().is_dir())
118 .collect();
119 entries.sort_by_key(|e| e.file_name());
120
121 for entry in entries {
122 let svc_dir = entry.path();
123 let service_toml = svc_dir.join("service.toml");
124 if !service_toml.exists() {
125 continue;
126 }
127 let service = match ServiceConfig::load(&service_toml) {
128 Ok(s) => s,
129 Err(e) => {
130 tracing::warn!(
131 path = %service_toml.display(),
132 error = %e,
133 "skipping service with unparseable service.toml"
134 );
135 continue;
136 }
137 };
138
139 for component in &service.components {
140 if component.kind != "static-asset" {
141 continue;
142 }
143 let workload_dir = workspace_root.join(&component.path);
144 let workload_toml_path = workload_dir.join("workload.toml");
145 if !workload_toml_path.exists() {
146 continue;
147 }
148
149 let aliases = match load_static_asset_aliases(&workload_toml_path) {
150 Ok(a) => a,
151 Err(e) => {
152 tracing::warn!(
153 path = %workload_toml_path.display(),
154 error = %e,
155 "skipping workload.toml with parse error"
156 );
157 continue;
158 }
159 };
160
161 for alias_name in aliases.keys() {
162 let source = AliasSource {
163 service: service.name.clone(),
164 component_id: component.id.clone(),
165 workload_toml: workload_toml_path.clone(),
166 };
167 if let Some(first) = seen.get(alias_name) {
168 collisions.push(AliasCollision {
169 alias: alias_name.clone(),
170 first: first.clone(),
171 second: source,
172 });
173 } else {
174 seen.insert(alias_name.clone(), source);
175 }
176 }
177 }
178 }
179
180 Ok(collisions)
181}
182
183fn load_static_asset_aliases(path: &Path) -> anyhow::Result<BTreeMap<String, String>> {
187 let src =
188 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
189 let workload: workload_spec::Workload =
190 toml::from_str(&src).with_context(|| format!("parsing {}", path.display()))?;
191 match workload {
192 workload_spec::Workload::StaticAsset(w) => Ok(w.aliases),
193 _ => Ok(BTreeMap::new()),
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct PortSource {
203 pub service: String,
205 pub env: String,
207 pub slot_role: String,
209 pub field: String,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct PortCollision {
220 pub port: u16,
221 pub first: PortSource,
222 pub second: PortSource,
223}
224
225impl PortCollision {
226 pub fn is_cross_service(&self) -> bool {
231 self.first.service != self.second.service
232 }
233
234 pub fn message(&self) -> String {
236 format!(
237 "host port {} is bound by both {}/{} (providers.{}.{}) and {}/{} (providers.{}.{})\n\
238 \u{2192} give one a distinct port — local mirrors share the operator's localhost, so \
239 two slots on the same port collide, and the local-static adopt probe may silently \
240 adopt the wrong service.",
241 self.port,
242 self.first.service,
243 self.first.env,
244 self.first.slot_role,
245 self.first.field,
246 self.second.service,
247 self.second.env,
248 self.second.slot_role,
249 self.second.field,
250 )
251 }
252}
253
254const PORT_FIELDS: &[&str] = &["port", "api_port", "console_port"];
256
257fn slot_binds_localhost(slot: &MirrorProviderSlot) -> bool {
261 matches!(
262 slot.inline_kind(),
263 Some(Provider::LocalStatic | Provider::MiniflareContainer | Provider::MinioContainer)
264 )
265}
266
267pub fn check_port_collisions(workspace_root: &Path) -> anyhow::Result<Vec<PortCollision>> {
276 let mut seen: BTreeMap<u16, PortSource> = BTreeMap::new();
278 let mut collisions = Vec::new();
279
280 for m in load_service_mirrors(workspace_root)? {
281 let mut roles: Vec<&String> = m.mirror.providers.keys().collect();
282 roles.sort();
283 for role in roles {
284 let slot = &m.mirror.providers[role];
285 if !slot_binds_localhost(slot) {
286 continue;
287 }
288 for field in PORT_FIELDS {
289 let Some(port) = crate::reconciler::slot_field_u16(slot.fields(), field) else {
290 continue;
291 };
292 let source = PortSource {
293 service: m.service.clone(),
294 env: m.env.clone(),
295 slot_role: role.clone(),
296 field: (*field).to_string(),
297 };
298 match seen.get(&port) {
299 Some(first) => collisions.push(PortCollision {
300 port,
301 first: first.clone(),
302 second: source,
303 }),
304 None => {
305 seen.insert(port, source);
306 }
307 }
308 }
309 }
310 }
311
312 Ok(collisions)
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum MachineLoadMode {
321 Tolerant,
325 Strict,
330}
331
332pub fn load_machine_tomls(
341 workspace_root: &Path,
342 mode: MachineLoadMode,
343) -> anyhow::Result<Vec<(PathBuf, MachineConfig)>> {
344 let dir = machines_dir(workspace_root);
345 if !dir.exists() {
346 return Ok(Vec::new());
347 }
348
349 let mut entries: Vec<_> = std::fs::read_dir(&dir)
350 .with_context(|| format!("reading {}", dir.display()))?
351 .filter_map(|e| e.ok())
352 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
353 .collect();
354 entries.sort_by_key(|e| e.file_name());
355
356 let mut out = Vec::with_capacity(entries.len());
357 for entry in entries {
358 let path = entry.path();
359 let src = match std::fs::read_to_string(&path) {
360 Ok(s) => s,
361 Err(e) if mode == MachineLoadMode::Tolerant => {
362 tracing::warn!(path = %path.display(), error = %e, "skipping unreadable machine toml");
363 continue;
364 }
365 Err(e) => return Err(e).with_context(|| format!("reading {}", path.display())),
366 };
367 let machine: MachineConfig = match toml::from_str(&src) {
368 Ok(m) => m,
369 Err(e) if mode == MachineLoadMode::Tolerant => {
370 tracing::warn!(path = %path.display(), error = %e, "skipping unparseable machine toml");
371 continue;
372 }
373 Err(e) => return Err(e).with_context(|| format!("parsing {}", path.display())),
374 };
375 out.push((path, machine));
376 }
377 Ok(out)
378}
379
380#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct InertTaint {
385 pub machine: String,
387 pub machine_toml: PathBuf,
389 pub key: String,
391}
392
393impl InertTaint {
394 pub fn message(&self) -> String {
395 format!(
396 "machine {:?} declares the taint {:?}, which no placement decision can read\n\
397 \u{2192} a taint fires in exactly two ways: as repulsion \
398 (`no-server` / `no-appliance` / `no-job`, an absolute block on that archetype), \
399 or as affinity (a key a workload names in `yah.placement.requires-taint`).\n\
400 \u{2192} legal keys today: {}\n\
401 \u{2192} if this is a *fact* about the node rather than a placement input, \
402 move it to `mesh_tags` or a comment; if it should really constrain placement, \
403 add it to cloud::config::AFFINITY_TAINT_KEYS together with the workload that \
404 requires it.\n {}",
405 self.machine,
406 self.key,
407 live_taint_keys().join(", "),
408 self.machine_toml.display(),
409 )
410 }
411}
412
413pub fn check_inert_taints(workspace_root: &Path) -> anyhow::Result<Vec<InertTaint>> {
432 let mut found = Vec::new();
433 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
434 for key in machine.inert_taints() {
435 found.push(InertTaint {
436 machine: machine.name.clone(),
437 machine_toml: path.clone(),
438 key: key.to_string(),
439 });
440 }
441 }
442 Ok(found)
443}
444
445const RETIRED_ARCH_TAG_PREFIX: &str = "tier:";
451const ARCH_TAG_PREFIX: &str = "arch:";
453
454#[derive(Debug, Clone, PartialEq, Eq)]
456pub struct RetiredArchTag {
457 pub machine: String,
458 pub machine_toml: PathBuf,
459 pub tag: String,
461}
462
463impl RetiredArchTag {
464 pub fn replacement(&self) -> String {
466 format!(
467 "{ARCH_TAG_PREFIX}{}",
468 self.tag.trim_start_matches(RETIRED_ARCH_TAG_PREFIX)
469 )
470 }
471
472 pub fn message(&self) -> String {
473 format!(
474 "machine {:?} declares the retired mesh tag {:?} — rename it to {:?}\n\
475 \u{2192} `tier:x86` / `tier:arm` were renamed to `arch:x86` / `arch:arm` \
476 (R763): the value is a CPU architecture, not a tier, and `tier:` is \
477 reserved for the environment axis.\n\
478 \u{2192} this does not fail loudly on its own, which is why it is checked \
479 here: `qed::platform::build_worker_mesh_tags` now requests `arch:<arch>`, \
480 and placement is SUPERSET matching, so a node still carrying the old tag \
481 simply stops matching and the build reports 'no node' instead of \
482 'wrong tag'.\n {}",
483 self.machine,
484 self.tag,
485 self.replacement(),
486 self.machine_toml.display(),
487 )
488 }
489}
490
491pub fn check_retired_arch_tags(workspace_root: &Path) -> anyhow::Result<Vec<RetiredArchTag>> {
499 let mut found = Vec::new();
500 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
501 for tag in machine
502 .mesh_tags
503 .iter()
504 .filter(|t| t.starts_with(RETIRED_ARCH_TAG_PREFIX))
505 {
506 found.push(RetiredArchTag {
507 machine: machine.name.clone(),
508 machine_toml: path.clone(),
509 tag: tag.clone(),
510 });
511 }
512 }
513 Ok(found)
514}
515
516#[derive(Debug, Clone, PartialEq, Eq)]
520pub struct UnroledSovereignMember {
521 pub machine: String,
522 pub machine_toml: PathBuf,
523 pub group: String,
526}
527
528impl UnroledSovereignMember {
529 pub fn message(&self) -> String {
530 format!(
531 "machine {:?} declares `sovereign_group = {:?}` but no `sovereign_role`\n\
532 \u{2192} membership and quorum eligibility are separate axes (R605-F12): a node can \
533 be inside a group's blast radius — its secrets, its upgrade cadence, its \
534 destruction — and still never hold a seat in its quorum.\n\
535 \u{2192} an absent role reads as `\"voter\"`, so this box is quorum-eligible today. \
536 That is the pre-R605-F12 meaning and is often right; the complaint is that nothing \
537 records whether anyone decided it.\n\
538 \u{2192} add `sovereign_role = \"voter\"` or `sovereign_role = \"non-voter\"` as a \
539 TOP-LEVEL key (below a `[table]` header TOML makes it a field of that table, and \
540 every consumer reads it as absent).\n {}",
541 self.machine,
542 self.group,
543 self.machine_toml.display(),
544 )
545 }
546}
547
548pub fn check_unroled_sovereign_members(
566 workspace_root: &Path,
567) -> anyhow::Result<Vec<UnroledSovereignMember>> {
568 let mut found = Vec::new();
569 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
570 if let (Some(group), None) = (&machine.sovereign_group, &machine.sovereign_role) {
571 found.push(UnroledSovereignMember {
572 machine: machine.name.clone(),
573 machine_toml: path.clone(),
574 group: group.clone(),
575 });
576 }
577 }
578 Ok(found)
579}
580
581#[derive(Debug, Clone, PartialEq, Eq)]
585pub struct LanDialTarget {
586 pub machine: String,
587 pub machine_toml: PathBuf,
588 pub url: String,
590 pub mesh_ipv4: Option<String>,
593}
594
595impl LanDialTarget {
596 pub fn message(&self) -> String {
597 let fix = match &self.mesh_ipv4 {
598 Some(ip) => format!(
599 "\u{2192} this box IS mesh-joined at {ip}: DELETE the `yubaba` line. \
600 `[registration].mesh_ipv4` composes with `[connect].yubaba_port` on its own, \
601 and `[connect].address` already records the LAN address as metadata."
602 ),
603 None => "\u{2192} this box has no `[registration].mesh_ipv4`: mesh-join it and record \
604 the tailnet address, or taint it out of placement. Until then it is \
605 unresolvable to automation and `MachineConfig::reach` refuses it by name."
606 .to_string(),
607 };
608 format!(
609 "machine {:?} declares `[connect].yubaba = {:?}` — a LAN address in the field every \
610 automated path dials\n\
611 \u{2192} the LAN address is an emergency break-glass route, never an official one \
612 (R605-T10, operator 2026-08-19). Automation ALWAYS assumes the caller is not on that \
613 LAN, and this camp genuinely is not — it sits on 192.168.22.0/22 with no route to \
614 192.168.10.0/24.\n\
615 \u{2192} it does not sit beside the mesh route, it OVERRODE it: before this check, a \
616 declared literal beat `[registration].mesh_ipv4` outright (R707-T6), so a healthy \
617 mesh-joined build worker was elected and then dialed at an address only its own \
618 building can reach.\n\
619 {fix}\n\
620 \u{2192} keeping the LAN address is fine and wanted — in `[connect].address` and \
621 `[connect].ssh`, which no resolver dials. A manual SSH session may use it once a \
622 human confirms they are on that LAN.\n {}",
623 self.machine,
624 self.url,
625 self.machine_toml.display(),
626 )
627 }
628}
629
630pub fn check_lan_dial_targets(workspace_root: &Path) -> anyhow::Result<Vec<LanDialTarget>> {
647 let mut found = Vec::new();
648 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
649 let Some(url) = machine.connect.as_ref().and_then(|c| c.yubaba.as_deref()) else {
650 continue;
651 };
652 if private_ipv4_from_url(url).is_none() {
653 continue;
654 }
655 found.push(LanDialTarget {
656 machine: machine.name.clone(),
657 machine_toml: path.clone(),
658 url: url.to_string(),
659 mesh_ipv4: machine.mesh_ipv4().map(str::to_string),
660 });
661 }
662 Ok(found)
663}
664
665#[derive(Debug, Clone)]
669pub struct LoadedMirror {
670 pub service: String,
672 pub env: String,
674 pub path: PathBuf,
676 pub mirror: MirrorConfig,
677}
678
679pub fn load_service_mirrors(workspace_root: &Path) -> anyhow::Result<Vec<LoadedMirror>> {
688 let dir = services_dir(workspace_root);
689 if !dir.exists() {
690 return Ok(vec![]);
691 }
692
693 let mut svc_entries: Vec<_> = std::fs::read_dir(&dir)
694 .with_context(|| format!("reading {}", dir.display()))?
695 .filter_map(|e| e.ok())
696 .filter(|e| e.path().is_dir())
697 .collect();
698 svc_entries.sort_by_key(|e| e.file_name());
699
700 let mut out = Vec::new();
701 for entry in svc_entries {
702 let svc_dir = entry.path();
703 let service_toml = svc_dir.join("service.toml");
704 if !service_toml.exists() {
705 continue;
706 }
707 let service = match ServiceConfig::load(&service_toml) {
708 Ok(s) => s,
709 Err(e) => {
710 tracing::warn!(
711 path = %service_toml.display(),
712 error = %e,
713 "skipping service with unparseable service.toml"
714 );
715 continue;
716 }
717 };
718
719 let mirrors_dir = svc_dir.join("mirrors");
720 if !mirrors_dir.exists() {
721 continue;
722 }
723 let mut mirror_entries: Vec<_> = std::fs::read_dir(&mirrors_dir)
724 .with_context(|| format!("reading {}", mirrors_dir.display()))?
725 .filter_map(|e| e.ok())
726 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
727 .collect();
728 mirror_entries.sort_by_key(|e| e.file_name());
729
730 for m in mirror_entries {
731 let path = m.path();
732 let mirror = match MirrorConfig::load(&path) {
733 Ok(mc) => mc,
734 Err(e) => {
735 tracing::warn!(
736 path = %path.display(),
737 error = %e,
738 "skipping mirror with parse error"
739 );
740 continue;
741 }
742 };
743 out.push(LoadedMirror {
744 service: service.name.clone(),
745 env: path
746 .file_stem()
747 .and_then(|s| s.to_str())
748 .unwrap_or_default()
749 .to_string(),
750 path,
751 mirror,
752 });
753 }
754 }
755 Ok(out)
756}
757
758#[derive(Debug, Clone, PartialEq, Eq)]
760pub enum IngressProblem {
761 Declaration {
764 service: String,
765 env: String,
766 mirror_toml: PathBuf,
767 detail: String,
768 },
769 Collation { detail: String },
772 Unplaced { label: String },
776}
777
778impl IngressProblem {
779 pub fn is_fatal(&self) -> bool {
782 !matches!(self, Self::Unplaced { .. })
783 }
784
785 pub fn message(&self) -> String {
787 match self {
788 Self::Declaration {
789 service,
790 env,
791 mirror_toml,
792 detail,
793 } => format!(
794 "{service}/{env}: ingress declaration does not plan — {detail}\n\u{2192} {}",
795 mirror_toml.display()
796 ),
797 Self::Collation { detail } => format!(
798 "ingress edges from two services collide on a shared node — {detail}\n\
799 \u{2192} the node's front door is COLLATED from every service that fronts \
800 through it (W305 F2), so this is invisible from either mirror alone."
801 ),
802 Self::Unplaced { label } => format!(
803 "{label}: declares a front door with no machine to run it on — neither the \
804 edge's `machines` nor the fronted slot's placement names a node, so it \
805 publishes nothing.\n\u{2192} add `machines = [...]` to the edge."
806 ),
807 }
808 }
809}
810
811#[derive(Debug, Clone, Default)]
813pub struct IngressReport {
814 pub collation: crate::reconciler::Collation,
816 pub problems: Vec<IngressProblem>,
818}
819
820pub fn collate_workspace_ingress(workspace_root: &Path) -> anyhow::Result<IngressReport> {
834 let machines: Vec<MachineConfig> =
843 load_machine_tomls(workspace_root, MachineLoadMode::Strict)?
844 .into_iter()
845 .map(|(_, m)| m)
846 .collect();
847
848 let mut planned = Vec::new();
849 let mut problems = Vec::new();
850
851 for m in load_service_mirrors(workspace_root)? {
852 let placements = match crate::reconciler::resolve_ingress_placements(&machines, &m.mirror) {
853 Ok(p) => p,
854 Err(e) => {
855 problems.push(IngressProblem::Declaration {
856 service: m.service.clone(),
857 env: m.env.clone(),
858 mirror_toml: m.path.clone(),
859 detail: format!("{e:#}"),
860 });
861 continue;
862 }
863 };
864 match crate::reconciler::plan_ingress(&m.mirror, &placements) {
865 Ok(plans) => planned.extend(plans.into_iter().map(|plan| {
866 crate::reconciler::PlannedEdge {
867 service: m.service.clone(),
868 env: m.env.clone(),
869 plan,
870 }
871 })),
872 Err(e) => problems.push(IngressProblem::Declaration {
873 service: m.service.clone(),
874 env: m.env.clone(),
875 mirror_toml: m.path.clone(),
876 detail: format!("{e:#}"),
877 }),
878 }
879 }
880
881 let collation = match crate::reconciler::collate_front_doors(&planned) {
882 Ok(c) => c,
883 Err(e) => {
884 problems.push(IngressProblem::Collation {
885 detail: format!("{e:#}"),
886 });
887 Default::default()
888 }
889 };
890 for label in &collation.unplaced {
891 problems.push(IngressProblem::Unplaced {
892 label: label.clone(),
893 });
894 }
895
896 Ok(IngressReport {
897 collation,
898 problems,
899 })
900}
901
902#[cfg(test)]
905mod tests {
906 use super::*;
907 use tempfile::tempdir;
908
909 fn write_service(workspace: &Path, svc_name: &str, component_path: &str) {
910 let svc_dir = workspace.join(".yah/services").join(svc_name);
911 std::fs::create_dir_all(&svc_dir).unwrap();
912 let toml = format!(
913 "schema_version = 1\nname = \"{svc_name}\"\ndomain = \"{svc_name}.example.com\"\n\
914 [[components]]\nid = \"models\"\nkind = \"static-asset\"\n\
915 path = \"{component_path}\"\nrole = \"static\"\n"
916 );
917 std::fs::write(svc_dir.join("service.toml"), toml).unwrap();
918 }
919
920 fn write_workload_with_aliases(dir: &Path, aliases: &[(&str, &str)]) {
921 std::fs::create_dir_all(dir).unwrap();
922 let alias_lines: String = aliases
923 .iter()
924 .map(|(k, v)| format!("\"{k}\" = \"{v}\"\n"))
925 .collect();
926 let content = format!(
927 "kind = \"static-asset\"\nschema_version = \"V1\"\n\
928 [aliases]\n{alias_lines}"
929 );
930 std::fs::write(dir.join("workload.toml"), content).unwrap();
931 }
932
933 #[test]
934 fn cloud_validate_clean_workspace_returns_empty() {
935 let dir = tempdir().unwrap();
936 let root = dir.path();
937
938 write_service(root, "svc-a", "svc-a/models");
939 write_workload_with_aliases(
940 &root.join("svc-a/models"),
941 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
942 );
943
944 let collisions = check_alias_collisions(root).unwrap();
945 assert!(
946 collisions.is_empty(),
947 "expected no collisions: {collisions:?}"
948 );
949 }
950
951 #[test]
952 fn cloud_validate_rejects_alias_collision() {
953 let dir = tempdir().unwrap();
954 let root = dir.path();
955
956 write_service(root, "svc-a", "svc-a/models");
957 write_workload_with_aliases(
958 &root.join("svc-a/models"),
959 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
960 );
961
962 write_service(root, "svc-b", "svc-b/models");
963 write_workload_with_aliases(
964 &root.join("svc-b/models"),
965 &[("whisper-default-ggml", "svc-b/whisper/model.bin")],
966 );
967
968 let collisions = check_alias_collisions(root).unwrap();
969 assert_eq!(
970 collisions.len(),
971 1,
972 "expected one collision: {collisions:?}"
973 );
974 let c = &collisions[0];
975 assert_eq!(c.alias, "whisper-default-ggml");
976 assert_eq!(c.first.service, "svc-a");
977 assert_eq!(c.second.service, "svc-b");
978
979 let msg = c.message();
980 assert!(msg.contains("whisper-default-ggml"), "message: {msg}");
981 assert!(msg.contains("svc-a"), "message: {msg}");
982 assert!(msg.contains("svc-b"), "message: {msg}");
983 }
984
985 #[test]
986 fn cloud_validate_distinct_aliases_no_collision() {
987 let dir = tempdir().unwrap();
988 let root = dir.path();
989
990 write_service(root, "svc-a", "svc-a/models");
991 write_workload_with_aliases(
992 &root.join("svc-a/models"),
993 &[
994 ("whisper-default-ggml", "svc-a/model.bin"),
995 ("whisper-default", "svc-a/model.bin"),
996 ],
997 );
998
999 write_service(root, "svc-b", "svc-b/models");
1000 write_workload_with_aliases(
1001 &root.join("svc-b/models"),
1002 &[("whisper-default-coreml", "svc-b/model.tar.gz")],
1003 );
1004
1005 let collisions = check_alias_collisions(root).unwrap();
1006 assert!(collisions.is_empty());
1007 }
1008
1009 #[test]
1010 fn cloud_validate_multiple_collisions_all_reported() {
1011 let dir = tempdir().unwrap();
1012 let root = dir.path();
1013
1014 write_service(root, "svc-a", "svc-a/models");
1015 write_workload_with_aliases(
1016 &root.join("svc-a/models"),
1017 &[
1018 ("alias-one", "svc-a/one.bin"),
1019 ("alias-two", "svc-a/two.bin"),
1020 ],
1021 );
1022
1023 write_service(root, "svc-b", "svc-b/models");
1024 write_workload_with_aliases(
1025 &root.join("svc-b/models"),
1026 &[
1027 ("alias-one", "svc-b/one.bin"),
1028 ("alias-two", "svc-b/two.bin"),
1029 ],
1030 );
1031
1032 let collisions = check_alias_collisions(root).unwrap();
1033 assert_eq!(collisions.len(), 2);
1034 let names: Vec<_> = collisions.iter().map(|c| c.alias.as_str()).collect();
1035 assert!(names.contains(&"alias-one"));
1036 assert!(names.contains(&"alias-two"));
1037 }
1038
1039 #[test]
1040 fn cloud_validate_missing_services_dir_is_not_error() {
1041 let dir = tempdir().unwrap();
1042 let collisions = check_alias_collisions(dir.path()).unwrap();
1043 assert!(collisions.is_empty());
1044 }
1045
1046 #[test]
1047 fn cloud_validate_non_static_asset_workloads_ignored() {
1048 let dir = tempdir().unwrap();
1049 let root = dir.path();
1050
1051 write_service(root, "svc-a", "svc-a/api");
1052 let workload_dir = root.join("svc-a/api");
1054 std::fs::create_dir_all(&workload_dir).unwrap();
1055 std::fs::write(
1058 workload_dir.join("workload.toml"),
1059 "schema_version = \"V1\"\nname = \"api\"\nkind = \"mesofact-static\"\n\
1060 bundle_dir = \"dist\"\n",
1061 )
1062 .unwrap();
1063
1064 let collisions = check_alias_collisions(root).unwrap();
1065 assert!(collisions.is_empty());
1066 }
1067
1068 fn write_mirror(workspace: &Path, svc: &str, env: &str, body: &str) {
1071 let dir = workspace.join(".yah/services").join(svc).join("mirrors");
1072 std::fs::create_dir_all(&dir).unwrap();
1073 std::fs::write(dir.join(format!("{env}.toml")), body).unwrap();
1074 }
1075
1076 fn local_static_mirror(port: u16) -> String {
1077 format!(
1078 "schema_version = 1\nshape = \"local\"\n\
1079 [providers.static]\nkind = \"local-static\"\nport = {port}\n"
1080 )
1081 }
1082
1083 #[test]
1084 fn port_collision_across_services_and_envs_is_flagged() {
1085 let dir = tempdir().unwrap();
1086 let root = dir.path();
1087 write_service(root, "scrabcake", "scrabcake/site");
1088 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4322));
1089 write_service(root, "yah-marketing", "yah-marketing/site");
1090 write_mirror(
1091 root,
1092 "yah-marketing",
1093 "pond",
1094 "schema_version = 1\nshape = \"local\"\n\
1095 [providers.static]\nkind = \"miniflare-container\"\nport = 4322\n",
1096 );
1097
1098 let cols = check_port_collisions(root).unwrap();
1099 assert_eq!(cols.len(), 1, "{cols:?}");
1100 assert_eq!(cols[0].port, 4322);
1101 assert_eq!(cols[0].first.service, "scrabcake");
1103 assert_eq!(cols[0].second.service, "yah-marketing");
1104 assert!(cols[0].is_cross_service(), "different services collide");
1105 let msg = cols[0].message();
1106 assert!(msg.contains("4322"), "{msg}");
1107 assert!(msg.contains("scrabcake"), "{msg}");
1108 assert!(msg.contains("yah-marketing"), "{msg}");
1109 }
1110
1111 #[test]
1112 fn distinct_ports_no_collision() {
1113 let dir = tempdir().unwrap();
1114 let root = dir.path();
1115 write_service(root, "a", "a/site");
1116 write_mirror(root, "a", "dev", &local_static_mirror(4322));
1117 write_service(root, "b", "b/site");
1118 write_mirror(root, "b", "dev", &local_static_mirror(4323));
1119 assert!(check_port_collisions(root).unwrap().is_empty());
1120 }
1121
1122 #[test]
1123 fn same_service_two_envs_reusing_a_port_is_flagged() {
1124 let dir = tempdir().unwrap();
1126 let root = dir.path();
1127 write_service(root, "scrabcake", "scrabcake/site");
1128 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4352));
1129 write_mirror(root, "scrabcake", "cloud", &local_static_mirror(4352));
1130 let cols = check_port_collisions(root).unwrap();
1131 assert_eq!(cols.len(), 1, "{cols:?}");
1132 assert_eq!(cols[0].port, 4352);
1133 assert_eq!(cols[0].first.env, "cloud");
1135 assert_eq!(cols[0].second.env, "dev");
1136 assert!(
1137 !cols[0].is_cross_service(),
1138 "same service across envs is NOT cross-service"
1139 );
1140 }
1141
1142 #[test]
1143 fn reference_slots_do_not_bind_localhost_and_are_ignored() {
1144 let dir = tempdir().unwrap();
1147 let root = dir.path();
1148 let ref_slot = "schema_version = 1\nshape = \"local\"\n\
1149 [providers.static]\nuse = \"cloudflare\"\nport = 8080\n";
1150 write_service(root, "a", "a/site");
1151 write_mirror(root, "a", "cloud", ref_slot);
1152 write_service(root, "b", "b/site");
1153 write_mirror(root, "b", "cloud", ref_slot);
1154 assert!(check_port_collisions(root).unwrap().is_empty());
1155 }
1156
1157 #[test]
1158 fn minio_api_and_console_ports_collide_across_ponds() {
1159 let dir = tempdir().unwrap();
1161 let root = dir.path();
1162 let minio = "schema_version = 1\nshape = \"local\"\n\
1163 [providers.object_store]\nkind = \"minio-container\"\napi_port = 9000\nconsole_port = 9001\n";
1164 write_service(root, "a", "a/site");
1165 write_mirror(root, "a", "pond", minio);
1166 write_service(root, "b", "b/site");
1167 write_mirror(root, "b", "pond", minio);
1168 let cols = check_port_collisions(root).unwrap();
1169 assert_eq!(cols.len(), 2, "{cols:?}");
1171 let ports: Vec<u16> = cols.iter().map(|c| c.port).collect();
1172 assert!(ports.contains(&9000));
1173 assert!(ports.contains(&9001));
1174 }
1175
1176 #[test]
1177 fn missing_services_dir_is_not_error_for_ports() {
1178 let dir = tempdir().unwrap();
1179 assert!(check_port_collisions(dir.path()).unwrap().is_empty());
1180 }
1181
1182 fn assert_machine_toml_parses(path: &Path) {
1189 let src = std::fs::read_to_string(path).unwrap();
1190 toml::from_str::<MachineConfig>(&src)
1191 .unwrap_or_else(|e| panic!("fixture {} does not parse as MachineConfig: {e}\n{src}", path.display()));
1192 }
1193
1194 fn write_machine_tags(workspace: &Path, name: &str, mesh_tags: &[&str]) {
1195 let dir = workspace.join(".yah/infra/machines");
1196 std::fs::create_dir_all(&dir).unwrap();
1197 let list: Vec<String> = mesh_tags.iter().map(|t| format!("\"{t}\"")).collect();
1198 let path = dir.join(format!("{name}.toml"));
1199 std::fs::write(
1200 &path,
1201 format!(
1202 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = [{}]\n",
1203 list.join(", ")
1204 ),
1205 )
1206 .unwrap();
1207 assert_machine_toml_parses(&path);
1208 }
1209
1210 #[test]
1211 fn the_current_arch_tag_is_clean() {
1212 let dir = tempdir().unwrap();
1213 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "arch:x86", "os:linux"]);
1214 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1215 }
1216
1217 #[test]
1218 fn a_stale_tier_arch_tag_is_flagged_with_its_replacement() {
1219 let dir = tempdir().unwrap();
1220 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "tier:arm", "os:linux"]);
1221 let found = check_retired_arch_tags(dir.path()).unwrap();
1222 assert_eq!(found.len(), 1, "{found:?}");
1223 assert_eq!(found[0].tag, "tier:arm");
1224 assert_eq!(found[0].replacement(), "arch:arm");
1225 let msg = found[0].message();
1226 assert!(msg.contains("arch:arm"), "{msg}");
1229 assert!(msg.contains("n1"), "{msg}");
1230 }
1231
1232 #[test]
1236 fn a_stale_tag_is_not_caught_by_the_inert_taint_lint() {
1237 let dir = tempdir().unwrap();
1238 write_machine_tags(dir.path(), "n1", &["tier:x86"]);
1239 assert!(
1240 check_inert_taints(dir.path()).unwrap().is_empty(),
1241 "mesh tags are not taints — this needs its own check"
1242 );
1243 assert_eq!(check_retired_arch_tags(dir.path()).unwrap().len(), 1);
1244 }
1245
1246 #[test]
1247 fn missing_machines_dir_is_not_error_for_arch_tags() {
1248 let dir = tempdir().unwrap();
1249 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1250 }
1251
1252 fn write_sovereign_machine(workspace: &Path, name: &str, stamp: &str) {
1259 let dir = workspace.join(".yah/infra/machines");
1260 std::fs::create_dir_all(&dir).unwrap();
1261 let path = dir.join(format!("{name}.toml"));
1262 std::fs::write(
1263 &path,
1264 format!("name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n{stamp}\n"),
1265 )
1266 .unwrap();
1267 assert_machine_toml_parses(&path);
1268 }
1269
1270 #[test]
1271 fn a_group_with_no_role_is_reported_with_the_declaring_file() {
1272 let dir = tempdir().unwrap();
1273 let root = dir.path();
1274 write_sovereign_machine(root, "us-west-001", "sovereign_group = \"prod\"");
1275 let found = check_unroled_sovereign_members(root).unwrap();
1276 assert_eq!(found.len(), 1, "{found:?}");
1277 assert_eq!(found[0].machine, "us-west-001");
1278 assert_eq!(found[0].group, "prod");
1279 assert!(found[0].machine_toml.ends_with("us-west-001.toml"));
1280 let msg = found[0].message();
1281 assert!(msg.contains("voter") && msg.contains("non-voter"), "{msg}");
1284 assert!(msg.contains("TOP-LEVEL"), "{msg}");
1285 }
1286
1287 #[test]
1288 fn either_stated_role_is_clean() {
1289 let dir = tempdir().unwrap();
1290 let root = dir.path();
1291 write_sovereign_machine(
1292 root,
1293 "us-west-001",
1294 "sovereign_group = \"prod\"\nsovereign_role = \"voter\"",
1295 );
1296 write_sovereign_machine(
1297 root,
1298 "us-west-003",
1299 "sovereign_group = \"prod\"\nsovereign_role = \"non-voter\"",
1300 );
1301 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1302 }
1303
1304 #[test]
1308 fn a_machine_in_no_group_is_not_asked_for_a_role() {
1309 let dir = tempdir().unwrap();
1310 let root = dir.path();
1311 write_sovereign_machine(root, "us-west-002", "taints = [\"no-appliance\"]");
1312 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1313 }
1314
1315 #[test]
1316 fn unroled_findings_are_ordered_by_file_so_output_is_stable() {
1317 let dir = tempdir().unwrap();
1318 let root = dir.path();
1319 write_sovereign_machine(root, "b-node", "sovereign_group = \"dev\"");
1320 write_sovereign_machine(root, "a-node", "sovereign_group = \"prod\"");
1321 let found = check_unroled_sovereign_members(root).unwrap();
1322 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1323 assert_eq!(names, vec!["a-node", "b-node"]);
1324 }
1325
1326 #[test]
1327 fn missing_machines_dir_is_not_error_for_unroled_members() {
1328 let dir = tempdir().unwrap();
1329 assert!(check_unroled_sovereign_members(dir.path())
1330 .unwrap()
1331 .is_empty());
1332 }
1333
1334 fn write_reach_machine(workspace: &Path, name: &str, connect: &str, registration: &str) {
1339 let dir = workspace.join(".yah/infra/machines");
1340 std::fs::create_dir_all(&dir).unwrap();
1341 let path = dir.join(format!("{name}.toml"));
1342 let reg = if registration.is_empty() {
1343 String::new()
1344 } else {
1345 format!("\n[registration]\n{registration}\n")
1346 };
1347 std::fs::write(
1348 &path,
1349 format!(
1350 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n\n\
1351 [connect]\n{connect}\n{reg}"
1352 ),
1353 )
1354 .unwrap();
1355 assert_machine_toml_parses(&path);
1356 }
1357
1358 #[test]
1359 fn a_lan_literal_in_the_dialed_field_is_reported_with_its_file() {
1360 let dir = tempdir().unwrap();
1361 let root = dir.path();
1362 write_reach_machine(
1364 root,
1365 "us-west-011",
1366 "address = \"192.168.10.11\"\nssh = \"yah@192.168.10.11\"\n\
1367 yubaba = \"http://192.168.10.11:7443\"",
1368 "",
1369 );
1370 let found = check_lan_dial_targets(root).unwrap();
1371 assert_eq!(found.len(), 1);
1372 assert_eq!(found[0].machine, "us-west-011");
1373 assert_eq!(found[0].url, "http://192.168.10.11:7443");
1374 assert_eq!(found[0].mesh_ipv4, None);
1375 let msg = found[0].message();
1376 assert!(msg.contains("mesh-join it"), "{msg}");
1377 assert!(msg.contains("us-west-011.toml"), "{msg}");
1378 }
1379
1380 #[test]
1383 fn a_mesh_joined_lan_declarer_is_told_to_delete_the_line() {
1384 let dir = tempdir().unwrap();
1385 let root = dir.path();
1386 write_reach_machine(
1387 root,
1388 "us-west-014",
1389 "address = \"192.168.10.14\"\nssh = \"yah@192.168.10.14\"\n\
1390 yubaba = \"http://192.168.10.14:7443\"",
1391 "mesh_ipv4 = \"100.64.0.6\"",
1392 );
1393 let found = check_lan_dial_targets(root).unwrap();
1394 assert_eq!(found.len(), 1);
1395 assert_eq!(found[0].mesh_ipv4.as_deref(), Some("100.64.0.6"));
1396 let msg = found[0].message();
1397 assert!(msg.contains("DELETE the `yubaba` line"), "{msg}");
1398 assert!(msg.contains("100.64.0.6"), "{msg}");
1399 }
1400
1401 #[test]
1405 fn mesh_loopback_and_metadata_only_lan_addresses_are_clean() {
1406 let dir = tempdir().unwrap();
1407 let root = dir.path();
1408 write_reach_machine(
1409 root,
1410 "meshed",
1411 "address = \"192.168.10.15\"\nssh = \"yah@192.168.10.15\"",
1412 "mesh_ipv4 = \"100.64.0.7\"",
1413 );
1414 write_reach_machine(
1415 root,
1416 "tunnelled",
1417 "address = \"192.168.10.16\"\nssh = \"yah@192.168.10.16\"\n\
1418 yubaba = \"http://127.0.0.1:7443\"",
1419 "",
1420 );
1421 write_reach_machine(
1422 root,
1423 "public",
1424 "address = \"45.32.194.254\"\nssh = \"debian@45.32.194.254\"\n\
1425 yubaba = \"http://45.32.194.254:7443\"",
1426 "",
1427 );
1428 assert!(check_lan_dial_targets(root).unwrap().is_empty());
1429 }
1430
1431 #[test]
1432 fn missing_machines_dir_is_not_error_for_lan_dial_targets() {
1433 let dir = tempdir().unwrap();
1434 assert!(check_lan_dial_targets(dir.path()).unwrap().is_empty());
1435 }
1436
1437 fn write_machine(workspace: &Path, name: &str, taints: &[&str]) {
1440 let dir = workspace.join(".yah/infra/machines");
1441 std::fs::create_dir_all(&dir).unwrap();
1442 let list: Vec<String> = taints.iter().map(|t| format!("\"{t}\"")).collect();
1443 let path = dir.join(format!("{name}.toml"));
1444 std::fs::write(
1445 &path,
1446 format!(
1447 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\ntaints = [{}]\n",
1448 list.join(", ")
1449 ),
1450 )
1451 .unwrap();
1452 assert_machine_toml_parses(&path);
1453 }
1454
1455 #[test]
1456 fn archetype_repel_keys_and_affinity_keys_are_clean() {
1457 let dir = tempdir().unwrap();
1458 let root = dir.path();
1459 write_machine(root, "worker", &["no-server", "no-appliance", "no-job"]);
1460 write_machine(root, "edge", &["public-ip"]);
1461 write_machine(root, "plain", &[]);
1462 assert!(check_inert_taints(root).unwrap().is_empty());
1463 }
1464
1465 #[test]
1466 fn a_free_form_taint_is_reported_with_the_declaring_file() {
1467 let dir = tempdir().unwrap();
1468 let root = dir.path();
1469 write_machine(root, "us-west-011", &["qa"]);
1471 let found = check_inert_taints(root).unwrap();
1472 assert_eq!(found.len(), 1, "{found:?}");
1473 assert_eq!(found[0].machine, "us-west-011");
1474 assert_eq!(found[0].key, "qa");
1475 assert!(found[0].machine_toml.ends_with("us-west-011.toml"));
1476 let msg = found[0].message();
1477 assert!(msg.contains("no-appliance"), "{msg}");
1480 assert!(msg.contains("public-ip"), "{msg}");
1481 assert!(msg.contains("mesh_tags"), "{msg}");
1482 }
1483
1484 #[test]
1485 fn no_voter_is_inert_because_voter_is_not_an_archetype() {
1486 let dir = tempdir().unwrap();
1489 let root = dir.path();
1490 write_machine(root, "us-west-015", &["no-server", "no-appliance", "no-voter"]);
1491 let found = check_inert_taints(root).unwrap();
1492 assert_eq!(found.len(), 1, "{found:?}");
1493 assert_eq!(found[0].key, "no-voter");
1494 }
1495
1496 #[test]
1497 fn findings_are_ordered_by_file_so_output_is_stable() {
1498 let dir = tempdir().unwrap();
1499 let root = dir.path();
1500 write_machine(root, "b-node", &["qa"]);
1501 write_machine(root, "a-node", &["staging"]);
1502 let found = check_inert_taints(root).unwrap();
1503 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1504 assert_eq!(names, vec!["a-node", "b-node"]);
1505 }
1506
1507 #[test]
1508 fn missing_machines_dir_is_not_error() {
1509 let dir = tempdir().unwrap();
1510 assert!(check_inert_taints(dir.path()).unwrap().is_empty());
1511 }
1512
1513 #[test]
1514 fn an_unparseable_machine_toml_is_skipped_not_fatal() {
1515 let dir = tempdir().unwrap();
1516 let root = dir.path();
1517 let mdir = root.join(".yah/infra/machines");
1518 std::fs::create_dir_all(&mdir).unwrap();
1519 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1520 write_machine(root, "good", &["qa"]);
1521 let found = check_inert_taints(root).unwrap();
1524 assert_eq!(found.len(), 1);
1525 assert_eq!(found[0].machine, "good");
1526 }
1527
1528 #[test]
1531 fn tolerant_mode_skips_an_unparseable_toml_and_still_pairs_the_path() {
1532 let dir = tempdir().unwrap();
1533 let root = dir.path();
1534 let mdir = root.join(".yah/infra/machines");
1535 std::fs::create_dir_all(&mdir).unwrap();
1536 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1537 write_machine(root, "good", &["qa"]);
1538
1539 let loaded = load_machine_tomls(root, MachineLoadMode::Tolerant).unwrap();
1540 assert_eq!(loaded.len(), 1, "{loaded:?}");
1541 assert_eq!(loaded[0].1.name, "good");
1542 assert!(loaded[0].0.ends_with("good.toml"));
1543 }
1544
1545 #[test]
1546 fn strict_mode_fails_the_whole_load_on_one_unparseable_toml() {
1547 let dir = tempdir().unwrap();
1550 let root = dir.path();
1551 let mdir = root.join(".yah/infra/machines");
1552 std::fs::create_dir_all(&mdir).unwrap();
1553 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1554 write_machine(root, "good", &["qa"]);
1555
1556 let err = load_machine_tomls(root, MachineLoadMode::Strict).unwrap_err();
1557 assert!(format!("{err:#}").contains("broken.toml"), "{err:#}");
1558 }
1559
1560 fn fronted_mirror(provider: &str, machines: &[&str], hostname: &str, port: u16) -> String {
1564 let list = machines
1565 .iter()
1566 .map(|m| format!("\"{m}\""))
1567 .collect::<Vec<_>>()
1568 .join(", ");
1569 format!(
1570 "schema_version = 1\nshape = \"single-machine\"\n\
1571 ingress = \"{provider}\"\ningress_machines = [{list}]\n\
1572 [providers.compute]\nuse = \"hetzner\"\nzone = \"{hostname}\"\n\
1573 port = {port}\nupstream_host = \"100.64.0.5\"\n"
1574 )
1575 }
1576
1577 #[test]
1578 fn two_services_fronting_one_node_collate_into_one_front_door() {
1579 let dir = tempdir().unwrap();
1580 let root = dir.path();
1581 write_service(root, "yah-marketing", "yah-marketing/site");
1582 write_mirror(
1583 root,
1584 "yah-marketing",
1585 "cloud",
1586 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1587 );
1588 write_service(root, "yah-issues", "yah-issues/site");
1589 write_mirror(
1590 root,
1591 "yah-issues",
1592 "cloud",
1593 &fronted_mirror("passway", &["us-east-001"], "issues.yah.dev", 8731),
1594 );
1595
1596 let report = collate_workspace_ingress(root).unwrap();
1597 assert!(report.problems.is_empty(), "{:?}", report.problems);
1598 assert_eq!(report.collation.front_doors.len(), 1);
1599 let door = &report.collation.front_doors[0];
1600 assert_eq!(door.machine, "us-east-001");
1601 assert_eq!(
1602 door.passway_upstreams().unwrap(),
1603 vec!["issues.yah.dev=100.64.0.5:8731", "yah.dev=100.64.0.5:8080"]
1604 );
1605 }
1606
1607 #[test]
1608 fn a_cross_service_hostname_clash_is_reported_with_both_declarations() {
1609 let dir = tempdir().unwrap();
1613 let root = dir.path();
1614 write_service(root, "svc-a", "svc-a/site");
1615 write_mirror(
1616 root,
1617 "svc-a",
1618 "cloud",
1619 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1620 );
1621 write_service(root, "svc-b", "svc-b/site");
1622 write_mirror(
1623 root,
1624 "svc-b",
1625 "cloud",
1626 &fronted_mirror("cloudflare-tunnel", &["us-west-001"], "yah.dev", 8080),
1627 );
1628
1629 let report = collate_workspace_ingress(root).unwrap();
1630 let fatal: Vec<String> = report
1631 .problems
1632 .iter()
1633 .filter(|p| p.is_fatal())
1634 .map(|p| p.message())
1635 .collect();
1636 assert_eq!(fatal.len(), 1, "{fatal:?}");
1637 assert!(fatal[0].contains("svc-a/cloud"), "{}", fatal[0]);
1638 assert!(fatal[0].contains("svc-b/cloud"), "{}", fatal[0]);
1639 }
1640
1641 #[test]
1642 fn one_mirrors_broken_declaration_does_not_hide_the_rest() {
1643 let dir = tempdir().unwrap();
1647 let root = dir.path();
1648 write_service(root, "svc-broken", "svc-broken/site");
1649 write_mirror(
1650 root,
1651 "svc-broken",
1652 "cloud",
1653 "schema_version = 1\nshape = \"single-machine\"\n\
1654 ingress_machines = [\"us-east-001\"]\n\
1655 [providers.compute]\nuse = \"hetzner\"\nzone = \"a.yah.dev\"\nport = 8080\n",
1656 );
1657 write_service(root, "svc-ok", "svc-ok/site");
1658 write_mirror(
1659 root,
1660 "svc-ok",
1661 "cloud",
1662 &fronted_mirror("passway", &["us-east-001"], "b.yah.dev", 8080),
1663 );
1664
1665 let report = collate_workspace_ingress(root).unwrap();
1666 assert_eq!(report.problems.len(), 1);
1667 assert!(
1668 report.problems[0].message().contains("svc-broken/cloud"),
1669 "{}",
1670 report.problems[0].message()
1671 );
1672 assert_eq!(report.collation.front_doors.len(), 1, "svc-ok still collates");
1673 }
1674
1675 #[test]
1676 fn a_mirror_with_no_ingress_contributes_nothing_and_is_not_a_finding() {
1677 let dir = tempdir().unwrap();
1678 let root = dir.path();
1679 write_service(root, "svc", "svc/site");
1680 write_mirror(root, "svc", "dev", &local_static_mirror(4322));
1681 let report = collate_workspace_ingress(root).unwrap();
1682 assert!(report.problems.is_empty());
1683 assert!(report.collation.front_doors.is_empty());
1684 }
1685}