1use std::collections::BTreeMap;
57use std::path::{Path, PathBuf};
58
59use anyhow::Context as _;
60
61use crate::config::{
62 live_taint_keys, private_ipv4_from_url, MachineConfig, MirrorConfig, MirrorProviderSlot,
63 Provider, ServiceConfig,
64};
65use crate::paths::{machines_dir, services_dir};
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct AliasSource {
70 pub service: String,
72 pub component_id: String,
74 pub workload_toml: PathBuf,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct AliasCollision {
81 pub alias: String,
82 pub first: AliasSource,
83 pub second: AliasSource,
84}
85
86impl AliasCollision {
87 pub fn message(&self) -> String {
89 format!(
90 "alias {:?} declared in both {} (component {}) and {} (component {})\n\
91 \u{2192} rename the alias in one of these files:\n {}\n {}",
92 self.alias,
93 self.first.service,
94 self.first.component_id,
95 self.second.service,
96 self.second.component_id,
97 self.first.workload_toml.display(),
98 self.second.workload_toml.display(),
99 )
100 }
101}
102
103pub fn check_alias_collisions(workspace_root: &Path) -> anyhow::Result<Vec<AliasCollision>> {
108 let dir = services_dir(workspace_root);
109 if !dir.exists() {
110 return Ok(vec![]);
111 }
112
113 let mut seen: BTreeMap<String, AliasSource> = BTreeMap::new();
115 let mut collisions = Vec::new();
116
117 let mut entries: Vec<_> = std::fs::read_dir(&dir)
118 .with_context(|| format!("reading {}", dir.display()))?
119 .filter_map(|e| e.ok())
120 .filter(|e| e.path().is_dir())
121 .collect();
122 entries.sort_by_key(|e| e.file_name());
123
124 for entry in entries {
125 let svc_dir = entry.path();
126 let service_toml = svc_dir.join("service.toml");
127 if !service_toml.exists() {
128 continue;
129 }
130 let service = match ServiceConfig::load(&service_toml) {
131 Ok(s) => s,
132 Err(e) => {
133 tracing::warn!(
134 path = %service_toml.display(),
135 error = %e,
136 "skipping service with unparseable service.toml"
137 );
138 continue;
139 }
140 };
141
142 for component in &service.components {
143 if component.kind != "static-asset" {
144 continue;
145 }
146 let workload_dir = workspace_root.join(&component.path);
147 let workload_toml_path = workload_dir.join("workload.toml");
148 if !workload_toml_path.exists() {
149 continue;
150 }
151
152 let aliases = match load_static_asset_aliases(&workload_toml_path) {
153 Ok(a) => a,
154 Err(e) => {
155 tracing::warn!(
156 path = %workload_toml_path.display(),
157 error = %e,
158 "skipping workload.toml with parse error"
159 );
160 continue;
161 }
162 };
163
164 for alias_name in aliases.keys() {
165 let source = AliasSource {
166 service: service.name.clone(),
167 component_id: component.id.clone(),
168 workload_toml: workload_toml_path.clone(),
169 };
170 if let Some(first) = seen.get(alias_name) {
171 collisions.push(AliasCollision {
172 alias: alias_name.clone(),
173 first: first.clone(),
174 second: source,
175 });
176 } else {
177 seen.insert(alias_name.clone(), source);
178 }
179 }
180 }
181 }
182
183 Ok(collisions)
184}
185
186fn load_static_asset_aliases(path: &Path) -> anyhow::Result<BTreeMap<String, String>> {
190 let src =
191 std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
192 let workload: workload_spec::Workload =
193 toml::from_str(&src).with_context(|| format!("parsing {}", path.display()))?;
194 match workload {
195 workload_spec::Workload::StaticAsset(w) => Ok(w.aliases),
196 _ => Ok(BTreeMap::new()),
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct PortSource {
206 pub service: String,
208 pub env: String,
210 pub slot_role: String,
212 pub field: String,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct PortCollision {
223 pub port: u16,
224 pub first: PortSource,
225 pub second: PortSource,
226}
227
228impl PortCollision {
229 pub fn is_cross_service(&self) -> bool {
234 self.first.service != self.second.service
235 }
236
237 pub fn message(&self) -> String {
239 format!(
240 "host port {} is bound by both {}/{} (providers.{}.{}) and {}/{} (providers.{}.{})\n\
241 \u{2192} give one a distinct port — local mirrors share the operator's localhost, so \
242 two slots on the same port collide, and the local-static adopt probe may silently \
243 adopt the wrong service.",
244 self.port,
245 self.first.service,
246 self.first.env,
247 self.first.slot_role,
248 self.first.field,
249 self.second.service,
250 self.second.env,
251 self.second.slot_role,
252 self.second.field,
253 )
254 }
255}
256
257const PORT_FIELDS: &[&str] = &["port", "api_port", "console_port"];
259
260fn slot_binds_localhost(slot: &MirrorProviderSlot) -> bool {
264 matches!(
265 slot.inline_kind(),
266 Some(Provider::LocalStatic | Provider::MiniflareContainer | Provider::MinioContainer)
267 )
268}
269
270pub fn check_port_collisions(workspace_root: &Path) -> anyhow::Result<Vec<PortCollision>> {
279 let mut seen: BTreeMap<u16, PortSource> = BTreeMap::new();
281 let mut collisions = Vec::new();
282
283 for m in load_service_mirrors(workspace_root)? {
284 let mut roles: Vec<&String> = m.mirror.providers.keys().collect();
285 roles.sort();
286 for role in roles {
287 let slot = &m.mirror.providers[role];
288 if !slot_binds_localhost(slot) {
289 continue;
290 }
291 for field in PORT_FIELDS {
292 let Some(port) = crate::reconciler::slot_field_u16(slot.fields(), field) else {
293 continue;
294 };
295 let source = PortSource {
296 service: m.service.clone(),
297 env: m.env.clone(),
298 slot_role: role.clone(),
299 field: (*field).to_string(),
300 };
301 match seen.get(&port) {
302 Some(first) => collisions.push(PortCollision {
303 port,
304 first: first.clone(),
305 second: source,
306 }),
307 None => {
308 seen.insert(port, source);
309 }
310 }
311 }
312 }
313 }
314
315 Ok(collisions)
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum MachineLoadMode {
324 Tolerant,
328 Strict,
333}
334
335pub fn load_machine_tomls(
344 workspace_root: &Path,
345 mode: MachineLoadMode,
346) -> anyhow::Result<Vec<(PathBuf, MachineConfig)>> {
347 let dir = machines_dir(workspace_root);
348 if !dir.exists() {
349 return Ok(Vec::new());
350 }
351
352 let mut entries: Vec<_> = std::fs::read_dir(&dir)
353 .with_context(|| format!("reading {}", dir.display()))?
354 .filter_map(|e| e.ok())
355 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
356 .collect();
357 entries.sort_by_key(|e| e.file_name());
358
359 let mut out = Vec::with_capacity(entries.len());
360 for entry in entries {
361 let path = entry.path();
362 let src = match std::fs::read_to_string(&path) {
363 Ok(s) => s,
364 Err(e) if mode == MachineLoadMode::Tolerant => {
365 tracing::warn!(path = %path.display(), error = %e, "skipping unreadable machine toml");
366 continue;
367 }
368 Err(e) => return Err(e).with_context(|| format!("reading {}", path.display())),
369 };
370 let machine: MachineConfig = match toml::from_str(&src) {
371 Ok(m) => m,
372 Err(e) if mode == MachineLoadMode::Tolerant => {
373 tracing::warn!(path = %path.display(), error = %e, "skipping unparseable machine toml");
374 continue;
375 }
376 Err(e) => return Err(e).with_context(|| format!("parsing {}", path.display())),
377 };
378 out.push((path, machine));
379 }
380 Ok(out)
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
387pub struct InertTaint {
388 pub machine: String,
390 pub machine_toml: PathBuf,
392 pub key: String,
394}
395
396impl InertTaint {
397 pub fn message(&self) -> String {
398 format!(
399 "machine {:?} declares the taint {:?}, which no placement decision can read\n\
400 \u{2192} a taint fires in exactly two ways: as repulsion \
401 (`no-server` / `no-appliance` / `no-job`, an absolute block on that archetype), \
402 or as affinity (a key a workload names in `yah.placement.requires-taint`).\n\
403 \u{2192} legal keys today: {}\n\
404 \u{2192} if this is a *fact* about the node rather than a placement input, \
405 move it to `mesh_tags` or a comment; if it should really constrain placement, \
406 add it to cloud::config::AFFINITY_TAINT_KEYS together with the workload that \
407 requires it.\n {}",
408 self.machine,
409 self.key,
410 live_taint_keys().join(", "),
411 self.machine_toml.display(),
412 )
413 }
414}
415
416pub fn check_inert_taints(workspace_root: &Path) -> anyhow::Result<Vec<InertTaint>> {
435 let mut found = Vec::new();
436 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
437 for key in machine.inert_taints() {
438 found.push(InertTaint {
439 machine: machine.name.clone(),
440 machine_toml: path.clone(),
441 key: key.to_string(),
442 });
443 }
444 }
445 Ok(found)
446}
447
448const RETIRED_ARCH_TAG_PREFIX: &str = "tier:";
454const ARCH_TAG_PREFIX: &str = "arch:";
456
457#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct RetiredArchTag {
460 pub machine: String,
461 pub machine_toml: PathBuf,
462 pub tag: String,
464}
465
466impl RetiredArchTag {
467 pub fn replacement(&self) -> String {
469 format!(
470 "{ARCH_TAG_PREFIX}{}",
471 self.tag.trim_start_matches(RETIRED_ARCH_TAG_PREFIX)
472 )
473 }
474
475 pub fn message(&self) -> String {
476 format!(
477 "machine {:?} declares the retired mesh tag {:?} — rename it to {:?}\n\
478 \u{2192} `tier:x86` / `tier:arm` were renamed to `arch:x86` / `arch:arm` \
479 (R763): the value is a CPU architecture, not a tier, and `tier:` is \
480 reserved for the environment axis.\n\
481 \u{2192} this does not fail loudly on its own, which is why it is checked \
482 here: `qed::platform::build_worker_mesh_tags` now requests `arch:<arch>`, \
483 and placement is SUPERSET matching, so a node still carrying the old tag \
484 simply stops matching and the build reports 'no node' instead of \
485 'wrong tag'.\n {}",
486 self.machine,
487 self.tag,
488 self.replacement(),
489 self.machine_toml.display(),
490 )
491 }
492}
493
494pub fn check_retired_arch_tags(workspace_root: &Path) -> anyhow::Result<Vec<RetiredArchTag>> {
502 let mut found = Vec::new();
503 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
504 for tag in machine
505 .mesh_tags
506 .iter()
507 .filter(|t| t.starts_with(RETIRED_ARCH_TAG_PREFIX))
508 {
509 found.push(RetiredArchTag {
510 machine: machine.name.clone(),
511 machine_toml: path.clone(),
512 tag: tag.clone(),
513 });
514 }
515 }
516 Ok(found)
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
523pub struct UnroledSovereignMember {
524 pub machine: String,
525 pub machine_toml: PathBuf,
526 pub group: String,
529}
530
531impl UnroledSovereignMember {
532 pub fn message(&self) -> String {
533 format!(
534 "machine {:?} declares `sovereign_group = {:?}` but no `sovereign_role`\n\
535 \u{2192} membership and quorum eligibility are separate axes (R605-F12): a node can \
536 be inside a group's blast radius — its secrets, its upgrade cadence, its \
537 destruction — and still never hold a seat in its quorum.\n\
538 \u{2192} an absent role reads as `\"voter\"`, so this box is quorum-eligible today. \
539 That is the pre-R605-F12 meaning and is often right; the complaint is that nothing \
540 records whether anyone decided it.\n\
541 \u{2192} add `sovereign_role = \"voter\"` or `sovereign_role = \"non-voter\"` as a \
542 TOP-LEVEL key (below a `[table]` header TOML makes it a field of that table, and \
543 every consumer reads it as absent).\n {}",
544 self.machine,
545 self.group,
546 self.machine_toml.display(),
547 )
548 }
549}
550
551pub fn check_unroled_sovereign_members(
569 workspace_root: &Path,
570) -> anyhow::Result<Vec<UnroledSovereignMember>> {
571 let mut found = Vec::new();
572 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
573 if let (Some(group), None) = (&machine.sovereign_group, &machine.sovereign_role) {
574 found.push(UnroledSovereignMember {
575 machine: machine.name.clone(),
576 machine_toml: path.clone(),
577 group: group.clone(),
578 });
579 }
580 }
581 Ok(found)
582}
583
584#[derive(Debug, Clone, PartialEq, Eq)]
588pub struct LanDialTarget {
589 pub machine: String,
590 pub machine_toml: PathBuf,
591 pub url: String,
593 pub mesh_ipv4: Option<String>,
596}
597
598impl LanDialTarget {
599 pub fn message(&self) -> String {
600 let fix = match &self.mesh_ipv4 {
601 Some(ip) => format!(
602 "\u{2192} this box IS mesh-joined at {ip}: DELETE the `yubaba` line. \
603 `[registration].mesh_ipv4` composes with `[connect].yubaba_port` on its own, \
604 and `[connect].address` already records the LAN address as metadata."
605 ),
606 None => "\u{2192} this box has no `[registration].mesh_ipv4`: mesh-join it and record \
607 the tailnet address, or taint it out of placement. Until then it is \
608 unresolvable to automation and `MachineConfig::reach` refuses it by name."
609 .to_string(),
610 };
611 format!(
612 "machine {:?} declares `[connect].yubaba = {:?}` — a LAN address in the field every \
613 automated path dials\n\
614 \u{2192} the LAN address is an emergency break-glass route, never an official one \
615 (R605-T10, operator 2026-08-19). Automation ALWAYS assumes the caller is not on that \
616 LAN, and this camp genuinely is not — it sits on 192.168.22.0/22 with no route to \
617 192.168.10.0/24.\n\
618 \u{2192} it does not sit beside the mesh route, it OVERRODE it: before this check, a \
619 declared literal beat `[registration].mesh_ipv4` outright (R707-T6), so a healthy \
620 mesh-joined build worker was elected and then dialed at an address only its own \
621 building can reach.\n\
622 {fix}\n\
623 \u{2192} keeping the LAN address is fine and wanted — in `[connect].address` and \
624 `[connect].ssh`, which no resolver dials. A manual SSH session may use it once a \
625 human confirms they are on that LAN.\n {}",
626 self.machine,
627 self.url,
628 self.machine_toml.display(),
629 )
630 }
631}
632
633pub fn check_lan_dial_targets(workspace_root: &Path) -> anyhow::Result<Vec<LanDialTarget>> {
650 let mut found = Vec::new();
651 for (path, machine) in load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)? {
652 let Some(url) = machine.connect.as_ref().and_then(|c| c.yubaba.as_deref()) else {
653 continue;
654 };
655 if private_ipv4_from_url(url).is_none() {
656 continue;
657 }
658 found.push(LanDialTarget {
659 machine: machine.name.clone(),
660 machine_toml: path.clone(),
661 url: url.to_string(),
662 mesh_ipv4: machine.mesh_ipv4().map(str::to_string),
663 });
664 }
665 Ok(found)
666}
667
668#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct IngressFloatingIpProblem {
674 pub machine: String,
675 pub machine_toml: PathBuf,
676 pub kind: IngressFloatingIpProblemKind,
677}
678
679#[derive(Debug, Clone, PartialEq, Eq)]
681pub enum IngressFloatingIpProblemKind {
682 Blank,
684 NoAdapter { provider: String },
687 CohortDisagreement {
690 group: String,
691 this_ip: String,
692 other_machine: String,
693 other_ip: String,
694 },
695}
696
697impl IngressFloatingIpProblem {
698 pub fn message(&self) -> String {
699 let body = match &self.kind {
700 IngressFloatingIpProblemKind::Blank => {
701 "declares an empty `ingress_floating_ip`\n\
702 \u{2192} an empty string is not \"no floating IP\" — omit the key entirely for \
703 that, which is the normal case and a clean skip. A blank value reads as a \
704 declaration and resolves to nothing."
705 .to_string()
706 }
707 IngressFloatingIpProblemKind::NoAdapter { provider } => format!(
708 "declares an `ingress_floating_ip` but its provider is {provider:?}, which has \
709 no floating-IP adapter\n\
710 \u{2192} floating/reserved IPs are implemented for hetzner, ovh and vultr \
711 (cloud::provider::floating_ip's registry). Nothing in this codebase can move \
712 this IP, so the declaration is inert — and inert in the worst way, because it \
713 reads as a working failover path.\n\
714 \u{2192} if the box really does carry a public IP that never moves, that is \
715 what the `public-ip` taint plus `[connect].address` already say."
716 ),
717 IngressFloatingIpProblemKind::CohortDisagreement {
718 group,
719 this_ip,
720 other_machine,
721 other_ip,
722 } => format!(
723 "declares `ingress_floating_ip = {this_ip:?}` but {other_machine} in the same \
724 sovereign_group {group:?} declares {other_ip:?}\n\
725 \u{2192} the ingress floating IP is ONE resource that moves between the boxes \
726 of a cohort as ownership flips. Two ids means an ownership flip reassigns a \
727 different IP than the one currently serving traffic — the old IP stays pointed \
728 at the dead box and the new one was never in DNS.\n\
729 \u{2192} the symptom is a failover that reports success and serves nothing, \
730 which is why this is refused here rather than discovered during one."
731 ),
732 };
733 format!(
734 "machine {:?} {body}\n {}",
735 self.machine,
736 self.machine_toml.display(),
737 )
738 }
739}
740
741pub fn check_ingress_floating_ip(
758 workspace_root: &Path,
759) -> anyhow::Result<Vec<IngressFloatingIpProblem>> {
760 let machines = load_machine_tomls(workspace_root, MachineLoadMode::Tolerant)?;
761 let mut found = Vec::new();
762
763 let mut anchor: BTreeMap<String, (String, String)> = BTreeMap::new();
768
769 for (path, machine) in &machines {
770 let Some(ip) = machine.ingress_floating_ip.as_deref() else {
771 continue;
772 };
773 let mut push = |kind| {
774 found.push(IngressFloatingIpProblem {
775 machine: machine.name.clone(),
776 machine_toml: path.clone(),
777 kind,
778 })
779 };
780 if ip.trim().is_empty() {
781 push(IngressFloatingIpProblemKind::Blank);
782 continue;
783 }
784 if !crate::provider::provider_has_floating_ip_adapter(&machine.provider) {
785 push(IngressFloatingIpProblemKind::NoAdapter {
786 provider: machine.provider.clone(),
787 });
788 }
789 let Some(group) = machine.sovereign_group.as_deref() else {
790 continue;
791 };
792 match anchor.get(group) {
793 None => {
794 anchor.insert(group.to_string(), (machine.name.clone(), ip.to_string()));
795 }
796 Some((other_machine, other_ip)) if other_ip != ip => {
797 push(IngressFloatingIpProblemKind::CohortDisagreement {
798 group: group.to_string(),
799 this_ip: ip.to_string(),
800 other_machine: other_machine.clone(),
801 other_ip: other_ip.clone(),
802 });
803 }
804 Some(_) => {}
805 }
806 }
807 Ok(found)
808}
809
810#[derive(Debug, Clone)]
814pub struct LoadedMirror {
815 pub service: String,
817 pub env: String,
819 pub path: PathBuf,
821 pub mirror: MirrorConfig,
822}
823
824pub fn load_service_mirrors(workspace_root: &Path) -> anyhow::Result<Vec<LoadedMirror>> {
833 let dir = services_dir(workspace_root);
834 if !dir.exists() {
835 return Ok(vec![]);
836 }
837
838 let mut svc_entries: Vec<_> = std::fs::read_dir(&dir)
839 .with_context(|| format!("reading {}", dir.display()))?
840 .filter_map(|e| e.ok())
841 .filter(|e| e.path().is_dir())
842 .collect();
843 svc_entries.sort_by_key(|e| e.file_name());
844
845 let mut out = Vec::new();
846 for entry in svc_entries {
847 let svc_dir = entry.path();
848 let service_toml = svc_dir.join("service.toml");
849 if !service_toml.exists() {
850 continue;
851 }
852 let service = match ServiceConfig::load(&service_toml) {
853 Ok(s) => s,
854 Err(e) => {
855 tracing::warn!(
856 path = %service_toml.display(),
857 error = %e,
858 "skipping service with unparseable service.toml"
859 );
860 continue;
861 }
862 };
863
864 let mirrors_dir = svc_dir.join("mirrors");
865 if !mirrors_dir.exists() {
866 continue;
867 }
868 let mut mirror_entries: Vec<_> = std::fs::read_dir(&mirrors_dir)
869 .with_context(|| format!("reading {}", mirrors_dir.display()))?
870 .filter_map(|e| e.ok())
871 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
872 .collect();
873 mirror_entries.sort_by_key(|e| e.file_name());
874
875 for m in mirror_entries {
876 let path = m.path();
877 let mirror = match MirrorConfig::load(&path) {
878 Ok(mc) => mc,
879 Err(e) => {
880 tracing::warn!(
881 path = %path.display(),
882 error = %e,
883 "skipping mirror with parse error"
884 );
885 continue;
886 }
887 };
888 out.push(LoadedMirror {
889 service: service.name.clone(),
890 env: path
891 .file_stem()
892 .and_then(|s| s.to_str())
893 .unwrap_or_default()
894 .to_string(),
895 path,
896 mirror,
897 });
898 }
899 }
900 Ok(out)
901}
902
903#[derive(Debug, Clone, PartialEq, Eq)]
905pub enum IngressProblem {
906 Declaration {
909 service: String,
910 env: String,
911 mirror_toml: PathBuf,
912 detail: String,
913 },
914 Collation { detail: String },
917 Unplaced { label: String },
921}
922
923impl IngressProblem {
924 pub fn is_fatal(&self) -> bool {
927 !matches!(self, Self::Unplaced { .. })
928 }
929
930 pub fn message(&self) -> String {
932 match self {
933 Self::Declaration {
934 service,
935 env,
936 mirror_toml,
937 detail,
938 } => format!(
939 "{service}/{env}: ingress declaration does not plan — {detail}\n\u{2192} {}",
940 mirror_toml.display()
941 ),
942 Self::Collation { detail } => format!(
943 "ingress edges from two services collide on a shared node — {detail}\n\
944 \u{2192} the node's front door is COLLATED from every service that fronts \
945 through it (W305 F2), so this is invisible from either mirror alone."
946 ),
947 Self::Unplaced { label } => format!(
948 "{label}: declares a front door with no machine to run it on — neither the \
949 edge's `machines` nor the fronted slot's placement names a node, so it \
950 publishes nothing.\n\u{2192} add `machines = [...]` to the edge."
951 ),
952 }
953 }
954}
955
956#[derive(Debug, Clone, Default)]
958pub struct IngressReport {
959 pub collation: crate::reconciler::Collation,
961 pub problems: Vec<IngressProblem>,
963}
964
965pub fn collate_workspace_ingress(workspace_root: &Path) -> anyhow::Result<IngressReport> {
990 let machines: Vec<MachineConfig> =
999 load_machine_tomls(workspace_root, MachineLoadMode::Strict)?
1000 .into_iter()
1001 .map(|(_, m)| m)
1002 .collect();
1003
1004 let mesh_addrs = crate::reconciler::machine_mesh_addrs(&machines);
1008
1009 let mut planned = Vec::new();
1010 let mut problems = Vec::new();
1011
1012 for m in load_service_mirrors(workspace_root)? {
1013 let placements = match crate::reconciler::resolve_ingress_placements(&machines, &m.mirror) {
1014 Ok(p) => p,
1015 Err(e) => {
1016 problems.push(IngressProblem::Declaration {
1017 service: m.service.clone(),
1018 env: m.env.clone(),
1019 mirror_toml: m.path.clone(),
1020 detail: format!("{e:#}"),
1021 });
1022 continue;
1023 }
1024 };
1025 match crate::reconciler::plan_ingress(&m.mirror, &placements) {
1026 Ok(plans) => planned.extend(plans.into_iter().map(|mut plan| {
1027 plan.resolve_upstreams_from_config(&mesh_addrs);
1028 crate::reconciler::PlannedEdge {
1029 service: m.service.clone(),
1030 env: m.env.clone(),
1031 plan,
1032 }
1033 })),
1034 Err(e) => problems.push(IngressProblem::Declaration {
1035 service: m.service.clone(),
1036 env: m.env.clone(),
1037 mirror_toml: m.path.clone(),
1038 detail: format!("{e:#}"),
1039 }),
1040 }
1041 }
1042
1043 let collation = match crate::reconciler::collate_front_doors(&planned) {
1044 Ok(c) => c,
1045 Err(e) => {
1046 problems.push(IngressProblem::Collation {
1047 detail: format!("{e:#}"),
1048 });
1049 Default::default()
1050 }
1051 };
1052 for label in &collation.unplaced {
1053 problems.push(IngressProblem::Unplaced {
1054 label: label.clone(),
1055 });
1056 }
1057
1058 Ok(IngressReport {
1059 collation,
1060 problems,
1061 })
1062}
1063
1064#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use tempfile::tempdir;
1070
1071 fn write_service(workspace: &Path, svc_name: &str, component_path: &str) {
1072 let svc_dir = workspace.join(".yah/services").join(svc_name);
1073 std::fs::create_dir_all(&svc_dir).unwrap();
1074 let toml = format!(
1075 "schema_version = 1\nname = \"{svc_name}\"\ndomain = \"{svc_name}.example.com\"\n\
1076 [[components]]\nid = \"models\"\nkind = \"static-asset\"\n\
1077 path = \"{component_path}\"\nrole = \"static\"\n"
1078 );
1079 std::fs::write(svc_dir.join("service.toml"), toml).unwrap();
1080 }
1081
1082 fn write_workload_with_aliases(dir: &Path, aliases: &[(&str, &str)]) {
1083 std::fs::create_dir_all(dir).unwrap();
1084 let alias_lines: String = aliases
1085 .iter()
1086 .map(|(k, v)| format!("\"{k}\" = \"{v}\"\n"))
1087 .collect();
1088 let content = format!(
1089 "kind = \"static-asset\"\nschema_version = \"V1\"\n\
1090 [aliases]\n{alias_lines}"
1091 );
1092 std::fs::write(dir.join("workload.toml"), content).unwrap();
1093 }
1094
1095 #[test]
1096 fn cloud_validate_clean_workspace_returns_empty() {
1097 let dir = tempdir().unwrap();
1098 let root = dir.path();
1099
1100 write_service(root, "svc-a", "svc-a/models");
1101 write_workload_with_aliases(
1102 &root.join("svc-a/models"),
1103 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1104 );
1105
1106 let collisions = check_alias_collisions(root).unwrap();
1107 assert!(
1108 collisions.is_empty(),
1109 "expected no collisions: {collisions:?}"
1110 );
1111 }
1112
1113 #[test]
1114 fn cloud_validate_rejects_alias_collision() {
1115 let dir = tempdir().unwrap();
1116 let root = dir.path();
1117
1118 write_service(root, "svc-a", "svc-a/models");
1119 write_workload_with_aliases(
1120 &root.join("svc-a/models"),
1121 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1122 );
1123
1124 write_service(root, "svc-b", "svc-b/models");
1125 write_workload_with_aliases(
1126 &root.join("svc-b/models"),
1127 &[("whisper-default-ggml", "svc-b/whisper/model.bin")],
1128 );
1129
1130 let collisions = check_alias_collisions(root).unwrap();
1131 assert_eq!(
1132 collisions.len(),
1133 1,
1134 "expected one collision: {collisions:?}"
1135 );
1136 let c = &collisions[0];
1137 assert_eq!(c.alias, "whisper-default-ggml");
1138 assert_eq!(c.first.service, "svc-a");
1139 assert_eq!(c.second.service, "svc-b");
1140
1141 let msg = c.message();
1142 assert!(msg.contains("whisper-default-ggml"), "message: {msg}");
1143 assert!(msg.contains("svc-a"), "message: {msg}");
1144 assert!(msg.contains("svc-b"), "message: {msg}");
1145 }
1146
1147 #[test]
1148 fn cloud_validate_distinct_aliases_no_collision() {
1149 let dir = tempdir().unwrap();
1150 let root = dir.path();
1151
1152 write_service(root, "svc-a", "svc-a/models");
1153 write_workload_with_aliases(
1154 &root.join("svc-a/models"),
1155 &[
1156 ("whisper-default-ggml", "svc-a/model.bin"),
1157 ("whisper-default", "svc-a/model.bin"),
1158 ],
1159 );
1160
1161 write_service(root, "svc-b", "svc-b/models");
1162 write_workload_with_aliases(
1163 &root.join("svc-b/models"),
1164 &[("whisper-default-coreml", "svc-b/model.tar.gz")],
1165 );
1166
1167 let collisions = check_alias_collisions(root).unwrap();
1168 assert!(collisions.is_empty());
1169 }
1170
1171 #[test]
1172 fn cloud_validate_multiple_collisions_all_reported() {
1173 let dir = tempdir().unwrap();
1174 let root = dir.path();
1175
1176 write_service(root, "svc-a", "svc-a/models");
1177 write_workload_with_aliases(
1178 &root.join("svc-a/models"),
1179 &[
1180 ("alias-one", "svc-a/one.bin"),
1181 ("alias-two", "svc-a/two.bin"),
1182 ],
1183 );
1184
1185 write_service(root, "svc-b", "svc-b/models");
1186 write_workload_with_aliases(
1187 &root.join("svc-b/models"),
1188 &[
1189 ("alias-one", "svc-b/one.bin"),
1190 ("alias-two", "svc-b/two.bin"),
1191 ],
1192 );
1193
1194 let collisions = check_alias_collisions(root).unwrap();
1195 assert_eq!(collisions.len(), 2);
1196 let names: Vec<_> = collisions.iter().map(|c| c.alias.as_str()).collect();
1197 assert!(names.contains(&"alias-one"));
1198 assert!(names.contains(&"alias-two"));
1199 }
1200
1201 #[test]
1202 fn cloud_validate_missing_services_dir_is_not_error() {
1203 let dir = tempdir().unwrap();
1204 let collisions = check_alias_collisions(dir.path()).unwrap();
1205 assert!(collisions.is_empty());
1206 }
1207
1208 #[test]
1209 fn cloud_validate_non_static_asset_workloads_ignored() {
1210 let dir = tempdir().unwrap();
1211 let root = dir.path();
1212
1213 write_service(root, "svc-a", "svc-a/api");
1214 let workload_dir = root.join("svc-a/api");
1216 std::fs::create_dir_all(&workload_dir).unwrap();
1217 std::fs::write(
1220 workload_dir.join("workload.toml"),
1221 "schema_version = \"V1\"\nname = \"api\"\nkind = \"mesofact-static\"\n\
1222 bundle_dir = \"dist\"\n",
1223 )
1224 .unwrap();
1225
1226 let collisions = check_alias_collisions(root).unwrap();
1227 assert!(collisions.is_empty());
1228 }
1229
1230 fn write_mirror(workspace: &Path, svc: &str, env: &str, body: &str) {
1233 let dir = workspace.join(".yah/services").join(svc).join("mirrors");
1234 std::fs::create_dir_all(&dir).unwrap();
1235 std::fs::write(dir.join(format!("{env}.toml")), body).unwrap();
1236 }
1237
1238 fn local_static_mirror(port: u16) -> String {
1239 format!(
1240 "schema_version = 1\nshape = \"local\"\n\
1241 [providers.static]\nkind = \"local-static\"\nport = {port}\n"
1242 )
1243 }
1244
1245 #[test]
1246 fn port_collision_across_services_and_envs_is_flagged() {
1247 let dir = tempdir().unwrap();
1248 let root = dir.path();
1249 write_service(root, "scrabcake", "scrabcake/site");
1250 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4322));
1251 write_service(root, "yah-marketing", "yah-marketing/site");
1252 write_mirror(
1253 root,
1254 "yah-marketing",
1255 "pond",
1256 "schema_version = 1\nshape = \"local\"\n\
1257 [providers.static]\nkind = \"miniflare-container\"\nport = 4322\n",
1258 );
1259
1260 let cols = check_port_collisions(root).unwrap();
1261 assert_eq!(cols.len(), 1, "{cols:?}");
1262 assert_eq!(cols[0].port, 4322);
1263 assert_eq!(cols[0].first.service, "scrabcake");
1265 assert_eq!(cols[0].second.service, "yah-marketing");
1266 assert!(cols[0].is_cross_service(), "different services collide");
1267 let msg = cols[0].message();
1268 assert!(msg.contains("4322"), "{msg}");
1269 assert!(msg.contains("scrabcake"), "{msg}");
1270 assert!(msg.contains("yah-marketing"), "{msg}");
1271 }
1272
1273 #[test]
1274 fn distinct_ports_no_collision() {
1275 let dir = tempdir().unwrap();
1276 let root = dir.path();
1277 write_service(root, "a", "a/site");
1278 write_mirror(root, "a", "dev", &local_static_mirror(4322));
1279 write_service(root, "b", "b/site");
1280 write_mirror(root, "b", "dev", &local_static_mirror(4323));
1281 assert!(check_port_collisions(root).unwrap().is_empty());
1282 }
1283
1284 #[test]
1285 fn same_service_two_envs_reusing_a_port_is_flagged() {
1286 let dir = tempdir().unwrap();
1288 let root = dir.path();
1289 write_service(root, "scrabcake", "scrabcake/site");
1290 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4352));
1291 write_mirror(root, "scrabcake", "cloud", &local_static_mirror(4352));
1292 let cols = check_port_collisions(root).unwrap();
1293 assert_eq!(cols.len(), 1, "{cols:?}");
1294 assert_eq!(cols[0].port, 4352);
1295 assert_eq!(cols[0].first.env, "cloud");
1297 assert_eq!(cols[0].second.env, "dev");
1298 assert!(
1299 !cols[0].is_cross_service(),
1300 "same service across envs is NOT cross-service"
1301 );
1302 }
1303
1304 #[test]
1305 fn reference_slots_do_not_bind_localhost_and_are_ignored() {
1306 let dir = tempdir().unwrap();
1309 let root = dir.path();
1310 let ref_slot = "schema_version = 1\nshape = \"local\"\n\
1311 [providers.static]\nuse = \"cloudflare\"\nport = 8080\n";
1312 write_service(root, "a", "a/site");
1313 write_mirror(root, "a", "cloud", ref_slot);
1314 write_service(root, "b", "b/site");
1315 write_mirror(root, "b", "cloud", ref_slot);
1316 assert!(check_port_collisions(root).unwrap().is_empty());
1317 }
1318
1319 #[test]
1320 fn minio_api_and_console_ports_collide_across_ponds() {
1321 let dir = tempdir().unwrap();
1323 let root = dir.path();
1324 let minio = "schema_version = 1\nshape = \"local\"\n\
1325 [providers.object_store]\nkind = \"minio-container\"\napi_port = 9000\nconsole_port = 9001\n";
1326 write_service(root, "a", "a/site");
1327 write_mirror(root, "a", "pond", minio);
1328 write_service(root, "b", "b/site");
1329 write_mirror(root, "b", "pond", minio);
1330 let cols = check_port_collisions(root).unwrap();
1331 assert_eq!(cols.len(), 2, "{cols:?}");
1333 let ports: Vec<u16> = cols.iter().map(|c| c.port).collect();
1334 assert!(ports.contains(&9000));
1335 assert!(ports.contains(&9001));
1336 }
1337
1338 #[test]
1339 fn missing_services_dir_is_not_error_for_ports() {
1340 let dir = tempdir().unwrap();
1341 assert!(check_port_collisions(dir.path()).unwrap().is_empty());
1342 }
1343
1344 fn assert_machine_toml_parses(path: &Path) {
1351 let src = std::fs::read_to_string(path).unwrap();
1352 toml::from_str::<MachineConfig>(&src)
1353 .unwrap_or_else(|e| panic!("fixture {} does not parse as MachineConfig: {e}\n{src}", path.display()));
1354 }
1355
1356 fn write_machine_tags(workspace: &Path, name: &str, mesh_tags: &[&str]) {
1357 let dir = workspace.join(".yah/infra/machines");
1358 std::fs::create_dir_all(&dir).unwrap();
1359 let list: Vec<String> = mesh_tags.iter().map(|t| format!("\"{t}\"")).collect();
1360 let path = dir.join(format!("{name}.toml"));
1361 std::fs::write(
1362 &path,
1363 format!(
1364 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = [{}]\n",
1365 list.join(", ")
1366 ),
1367 )
1368 .unwrap();
1369 assert_machine_toml_parses(&path);
1370 }
1371
1372 #[test]
1373 fn the_current_arch_tag_is_clean() {
1374 let dir = tempdir().unwrap();
1375 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "arch:x86", "os:linux"]);
1376 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1377 }
1378
1379 #[test]
1380 fn a_stale_tier_arch_tag_is_flagged_with_its_replacement() {
1381 let dir = tempdir().unwrap();
1382 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "tier:arm", "os:linux"]);
1383 let found = check_retired_arch_tags(dir.path()).unwrap();
1384 assert_eq!(found.len(), 1, "{found:?}");
1385 assert_eq!(found[0].tag, "tier:arm");
1386 assert_eq!(found[0].replacement(), "arch:arm");
1387 let msg = found[0].message();
1388 assert!(msg.contains("arch:arm"), "{msg}");
1391 assert!(msg.contains("n1"), "{msg}");
1392 }
1393
1394 #[test]
1398 fn a_stale_tag_is_not_caught_by_the_inert_taint_lint() {
1399 let dir = tempdir().unwrap();
1400 write_machine_tags(dir.path(), "n1", &["tier:x86"]);
1401 assert!(
1402 check_inert_taints(dir.path()).unwrap().is_empty(),
1403 "mesh tags are not taints — this needs its own check"
1404 );
1405 assert_eq!(check_retired_arch_tags(dir.path()).unwrap().len(), 1);
1406 }
1407
1408 #[test]
1409 fn missing_machines_dir_is_not_error_for_arch_tags() {
1410 let dir = tempdir().unwrap();
1411 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1412 }
1413
1414 fn write_sovereign_machine(workspace: &Path, name: &str, stamp: &str) {
1421 let dir = workspace.join(".yah/infra/machines");
1422 std::fs::create_dir_all(&dir).unwrap();
1423 let path = dir.join(format!("{name}.toml"));
1424 std::fs::write(
1425 &path,
1426 format!("name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n{stamp}\n"),
1427 )
1428 .unwrap();
1429 assert_machine_toml_parses(&path);
1430 }
1431
1432 fn write_fip_machine(workspace: &Path, name: &str, provider: &str, stamp: &str) {
1437 let dir = workspace.join(".yah/infra/machines");
1438 std::fs::create_dir_all(&dir).unwrap();
1439 let path = dir.join(format!("{name}.toml"));
1440 std::fs::write(
1441 &path,
1442 format!("name = \"{name}\"\nprovider = \"{provider}\"\nmesh_tags = []\n{stamp}\n"),
1443 )
1444 .unwrap();
1445 assert_machine_toml_parses(&path);
1446 }
1447
1448 #[test]
1452 fn a_machine_with_no_ingress_floating_ip_is_never_flagged() {
1453 let dir = tempdir().unwrap();
1454 write_fip_machine(dir.path(), "us-west-002", "static", "");
1455 write_fip_machine(dir.path(), "n1", "digitalocean", "");
1456 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1457 }
1458
1459 #[test]
1460 fn a_valid_declaration_on_an_adapter_backed_provider_is_clean() {
1461 let dir = tempdir().unwrap();
1462 write_fip_machine(
1463 dir.path(),
1464 "us-west-001",
1465 "hetzner",
1466 "ingress_floating_ip = \"42\"",
1467 );
1468 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1469 }
1470
1471 #[test]
1473 fn a_provider_with_no_floating_ip_adapter_is_flagged_with_what_is_supported() {
1474 let dir = tempdir().unwrap();
1475 write_fip_machine(
1476 dir.path(),
1477 "us-east-001",
1478 "static",
1479 "ingress_floating_ip = \"51.81.85.200\"",
1480 );
1481 let found = check_ingress_floating_ip(dir.path()).unwrap();
1482 assert_eq!(found.len(), 1, "{found:?}");
1483 assert_eq!(
1484 found[0].kind,
1485 IngressFloatingIpProblemKind::NoAdapter {
1486 provider: "static".into()
1487 }
1488 );
1489 let msg = found[0].message();
1490 assert!(msg.contains("us-east-001"), "{msg}");
1491 assert!(msg.contains("hetzner"), "the message must name what IS supported: {msg}");
1492 assert!(msg.ends_with("us-east-001.toml"), "{msg}");
1493 }
1494
1495 #[test]
1498 fn a_blank_declaration_is_flagged_rather_than_read_as_absent() {
1499 let dir = tempdir().unwrap();
1500 write_fip_machine(
1501 dir.path(),
1502 "us-west-001",
1503 "hetzner",
1504 "ingress_floating_ip = \" \"",
1505 );
1506 let found = check_ingress_floating_ip(dir.path()).unwrap();
1507 assert_eq!(found.len(), 1, "{found:?}");
1508 assert_eq!(found[0].kind, IngressFloatingIpProblemKind::Blank);
1509 }
1510
1511 #[test]
1515 fn two_ids_in_one_sovereign_group_are_refused_and_name_both_machines() {
1516 let dir = tempdir().unwrap();
1517 write_fip_machine(
1518 dir.path(),
1519 "us-east-001",
1520 "hetzner",
1521 "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1522 );
1523 write_fip_machine(
1524 dir.path(),
1525 "us-west-001",
1526 "hetzner",
1527 "sovereign_group = \"prod\"\ningress_floating_ip = \"77\"",
1528 );
1529 let found = check_ingress_floating_ip(dir.path()).unwrap();
1530 assert_eq!(found.len(), 1, "{found:?}");
1531 let msg = found[0].message();
1532 assert!(msg.contains("us-west-001") && msg.contains("us-east-001"), "{msg}");
1533 assert!(msg.contains("42") && msg.contains("77"), "{msg}");
1534 }
1535
1536 #[test]
1537 fn one_id_across_a_whole_cohort_is_clean() {
1538 let dir = tempdir().unwrap();
1539 for name in ["us-east-001", "us-west-001", "us-south-001"] {
1540 write_fip_machine(
1541 dir.path(),
1542 name,
1543 "hetzner",
1544 "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1545 );
1546 }
1547 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1548 }
1549
1550 #[test]
1553 fn machines_in_no_group_may_hold_different_ips() {
1554 let dir = tempdir().unwrap();
1555 write_fip_machine(
1556 dir.path(),
1557 "us-east-001",
1558 "hetzner",
1559 "ingress_floating_ip = \"42\"",
1560 );
1561 write_fip_machine(
1562 dir.path(),
1563 "us-west-001",
1564 "vultr",
1565 "ingress_floating_ip = \"a-uuid\"",
1566 );
1567 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1568 }
1569
1570 #[test]
1571 fn missing_machines_dir_is_not_error_for_ingress_floating_ip() {
1572 let dir = tempdir().unwrap();
1573 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1574 }
1575
1576 #[test]
1577 fn a_group_with_no_role_is_reported_with_the_declaring_file() {
1578 let dir = tempdir().unwrap();
1579 let root = dir.path();
1580 write_sovereign_machine(root, "us-west-001", "sovereign_group = \"prod\"");
1581 let found = check_unroled_sovereign_members(root).unwrap();
1582 assert_eq!(found.len(), 1, "{found:?}");
1583 assert_eq!(found[0].machine, "us-west-001");
1584 assert_eq!(found[0].group, "prod");
1585 assert!(found[0].machine_toml.ends_with("us-west-001.toml"));
1586 let msg = found[0].message();
1587 assert!(msg.contains("voter") && msg.contains("non-voter"), "{msg}");
1590 assert!(msg.contains("TOP-LEVEL"), "{msg}");
1591 }
1592
1593 #[test]
1594 fn either_stated_role_is_clean() {
1595 let dir = tempdir().unwrap();
1596 let root = dir.path();
1597 write_sovereign_machine(
1598 root,
1599 "us-west-001",
1600 "sovereign_group = \"prod\"\nsovereign_role = \"voter\"",
1601 );
1602 write_sovereign_machine(
1603 root,
1604 "us-west-003",
1605 "sovereign_group = \"prod\"\nsovereign_role = \"non-voter\"",
1606 );
1607 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1608 }
1609
1610 #[test]
1614 fn a_machine_in_no_group_is_not_asked_for_a_role() {
1615 let dir = tempdir().unwrap();
1616 let root = dir.path();
1617 write_sovereign_machine(root, "us-west-002", "taints = [\"no-appliance\"]");
1618 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1619 }
1620
1621 #[test]
1622 fn unroled_findings_are_ordered_by_file_so_output_is_stable() {
1623 let dir = tempdir().unwrap();
1624 let root = dir.path();
1625 write_sovereign_machine(root, "b-node", "sovereign_group = \"dev\"");
1626 write_sovereign_machine(root, "a-node", "sovereign_group = \"prod\"");
1627 let found = check_unroled_sovereign_members(root).unwrap();
1628 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1629 assert_eq!(names, vec!["a-node", "b-node"]);
1630 }
1631
1632 #[test]
1633 fn missing_machines_dir_is_not_error_for_unroled_members() {
1634 let dir = tempdir().unwrap();
1635 assert!(check_unroled_sovereign_members(dir.path())
1636 .unwrap()
1637 .is_empty());
1638 }
1639
1640 fn write_reach_machine(workspace: &Path, name: &str, connect: &str, registration: &str) {
1645 let dir = workspace.join(".yah/infra/machines");
1646 std::fs::create_dir_all(&dir).unwrap();
1647 let path = dir.join(format!("{name}.toml"));
1648 let reg = if registration.is_empty() {
1649 String::new()
1650 } else {
1651 format!("\n[registration]\n{registration}\n")
1652 };
1653 std::fs::write(
1654 &path,
1655 format!(
1656 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n\n\
1657 [connect]\n{connect}\n{reg}"
1658 ),
1659 )
1660 .unwrap();
1661 assert_machine_toml_parses(&path);
1662 }
1663
1664 #[test]
1665 fn a_lan_literal_in_the_dialed_field_is_reported_with_its_file() {
1666 let dir = tempdir().unwrap();
1667 let root = dir.path();
1668 write_reach_machine(
1670 root,
1671 "us-west-011",
1672 "address = \"192.168.10.11\"\nssh = \"yah@192.168.10.11\"\n\
1673 yubaba = \"http://192.168.10.11:7443\"",
1674 "",
1675 );
1676 let found = check_lan_dial_targets(root).unwrap();
1677 assert_eq!(found.len(), 1);
1678 assert_eq!(found[0].machine, "us-west-011");
1679 assert_eq!(found[0].url, "http://192.168.10.11:7443");
1680 assert_eq!(found[0].mesh_ipv4, None);
1681 let msg = found[0].message();
1682 assert!(msg.contains("mesh-join it"), "{msg}");
1683 assert!(msg.contains("us-west-011.toml"), "{msg}");
1684 }
1685
1686 #[test]
1689 fn a_mesh_joined_lan_declarer_is_told_to_delete_the_line() {
1690 let dir = tempdir().unwrap();
1691 let root = dir.path();
1692 write_reach_machine(
1693 root,
1694 "us-west-014",
1695 "address = \"192.168.10.14\"\nssh = \"yah@192.168.10.14\"\n\
1696 yubaba = \"http://192.168.10.14:7443\"",
1697 "mesh_ipv4 = \"100.64.0.6\"",
1698 );
1699 let found = check_lan_dial_targets(root).unwrap();
1700 assert_eq!(found.len(), 1);
1701 assert_eq!(found[0].mesh_ipv4.as_deref(), Some("100.64.0.6"));
1702 let msg = found[0].message();
1703 assert!(msg.contains("DELETE the `yubaba` line"), "{msg}");
1704 assert!(msg.contains("100.64.0.6"), "{msg}");
1705 }
1706
1707 #[test]
1711 fn mesh_loopback_and_metadata_only_lan_addresses_are_clean() {
1712 let dir = tempdir().unwrap();
1713 let root = dir.path();
1714 write_reach_machine(
1715 root,
1716 "meshed",
1717 "address = \"192.168.10.15\"\nssh = \"yah@192.168.10.15\"",
1718 "mesh_ipv4 = \"100.64.0.7\"",
1719 );
1720 write_reach_machine(
1721 root,
1722 "tunnelled",
1723 "address = \"192.168.10.16\"\nssh = \"yah@192.168.10.16\"\n\
1724 yubaba = \"http://127.0.0.1:7443\"",
1725 "",
1726 );
1727 write_reach_machine(
1728 root,
1729 "public",
1730 "address = \"45.32.194.254\"\nssh = \"debian@45.32.194.254\"\n\
1731 yubaba = \"http://45.32.194.254:7443\"",
1732 "",
1733 );
1734 assert!(check_lan_dial_targets(root).unwrap().is_empty());
1735 }
1736
1737 #[test]
1738 fn missing_machines_dir_is_not_error_for_lan_dial_targets() {
1739 let dir = tempdir().unwrap();
1740 assert!(check_lan_dial_targets(dir.path()).unwrap().is_empty());
1741 }
1742
1743 fn write_machine(workspace: &Path, name: &str, taints: &[&str]) {
1746 let dir = workspace.join(".yah/infra/machines");
1747 std::fs::create_dir_all(&dir).unwrap();
1748 let list: Vec<String> = taints.iter().map(|t| format!("\"{t}\"")).collect();
1749 let path = dir.join(format!("{name}.toml"));
1750 std::fs::write(
1751 &path,
1752 format!(
1753 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\ntaints = [{}]\n",
1754 list.join(", ")
1755 ),
1756 )
1757 .unwrap();
1758 assert_machine_toml_parses(&path);
1759 }
1760
1761 #[test]
1762 fn archetype_repel_keys_and_affinity_keys_are_clean() {
1763 let dir = tempdir().unwrap();
1764 let root = dir.path();
1765 write_machine(root, "worker", &["no-server", "no-appliance", "no-job"]);
1766 write_machine(root, "edge", &["public-ip"]);
1767 write_machine(root, "plain", &[]);
1768 assert!(check_inert_taints(root).unwrap().is_empty());
1769 }
1770
1771 #[test]
1772 fn a_free_form_taint_is_reported_with_the_declaring_file() {
1773 let dir = tempdir().unwrap();
1774 let root = dir.path();
1775 write_machine(root, "us-west-011", &["qa"]);
1777 let found = check_inert_taints(root).unwrap();
1778 assert_eq!(found.len(), 1, "{found:?}");
1779 assert_eq!(found[0].machine, "us-west-011");
1780 assert_eq!(found[0].key, "qa");
1781 assert!(found[0].machine_toml.ends_with("us-west-011.toml"));
1782 let msg = found[0].message();
1783 assert!(msg.contains("no-appliance"), "{msg}");
1786 assert!(msg.contains("public-ip"), "{msg}");
1787 assert!(msg.contains("mesh_tags"), "{msg}");
1788 }
1789
1790 #[test]
1791 fn no_voter_is_inert_because_voter_is_not_an_archetype() {
1792 let dir = tempdir().unwrap();
1795 let root = dir.path();
1796 write_machine(root, "us-west-015", &["no-server", "no-appliance", "no-voter"]);
1797 let found = check_inert_taints(root).unwrap();
1798 assert_eq!(found.len(), 1, "{found:?}");
1799 assert_eq!(found[0].key, "no-voter");
1800 }
1801
1802 #[test]
1803 fn findings_are_ordered_by_file_so_output_is_stable() {
1804 let dir = tempdir().unwrap();
1805 let root = dir.path();
1806 write_machine(root, "b-node", &["qa"]);
1807 write_machine(root, "a-node", &["staging"]);
1808 let found = check_inert_taints(root).unwrap();
1809 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1810 assert_eq!(names, vec!["a-node", "b-node"]);
1811 }
1812
1813 #[test]
1814 fn missing_machines_dir_is_not_error() {
1815 let dir = tempdir().unwrap();
1816 assert!(check_inert_taints(dir.path()).unwrap().is_empty());
1817 }
1818
1819 #[test]
1820 fn an_unparseable_machine_toml_is_skipped_not_fatal() {
1821 let dir = tempdir().unwrap();
1822 let root = dir.path();
1823 let mdir = root.join(".yah/infra/machines");
1824 std::fs::create_dir_all(&mdir).unwrap();
1825 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1826 write_machine(root, "good", &["qa"]);
1827 let found = check_inert_taints(root).unwrap();
1830 assert_eq!(found.len(), 1);
1831 assert_eq!(found[0].machine, "good");
1832 }
1833
1834 #[test]
1837 fn tolerant_mode_skips_an_unparseable_toml_and_still_pairs_the_path() {
1838 let dir = tempdir().unwrap();
1839 let root = dir.path();
1840 let mdir = root.join(".yah/infra/machines");
1841 std::fs::create_dir_all(&mdir).unwrap();
1842 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1843 write_machine(root, "good", &["qa"]);
1844
1845 let loaded = load_machine_tomls(root, MachineLoadMode::Tolerant).unwrap();
1846 assert_eq!(loaded.len(), 1, "{loaded:?}");
1847 assert_eq!(loaded[0].1.name, "good");
1848 assert!(loaded[0].0.ends_with("good.toml"));
1849 }
1850
1851 #[test]
1852 fn strict_mode_fails_the_whole_load_on_one_unparseable_toml() {
1853 let dir = tempdir().unwrap();
1856 let root = dir.path();
1857 let mdir = root.join(".yah/infra/machines");
1858 std::fs::create_dir_all(&mdir).unwrap();
1859 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1860 write_machine(root, "good", &["qa"]);
1861
1862 let err = load_machine_tomls(root, MachineLoadMode::Strict).unwrap_err();
1863 assert!(format!("{err:#}").contains("broken.toml"), "{err:#}");
1864 }
1865
1866 fn fronted_mirror(provider: &str, machines: &[&str], hostname: &str, port: u16) -> String {
1870 let list = machines
1871 .iter()
1872 .map(|m| format!("\"{m}\""))
1873 .collect::<Vec<_>>()
1874 .join(", ");
1875 format!(
1876 "schema_version = 1\nshape = \"single-machine\"\n\
1877 ingress = \"{provider}\"\ningress_machines = [{list}]\n\
1878 [providers.compute]\nuse = \"hetzner\"\nzone = \"{hostname}\"\n\
1879 port = {port}\nupstream_host = \"100.64.0.5\"\n"
1880 )
1881 }
1882
1883 #[test]
1884 fn two_services_fronting_one_node_collate_into_one_front_door() {
1885 let dir = tempdir().unwrap();
1886 let root = dir.path();
1887 write_service(root, "yah-marketing", "yah-marketing/site");
1888 write_mirror(
1889 root,
1890 "yah-marketing",
1891 "cloud",
1892 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1893 );
1894 write_service(root, "yah-issues", "yah-issues/site");
1895 write_mirror(
1896 root,
1897 "yah-issues",
1898 "cloud",
1899 &fronted_mirror("passway", &["us-east-001"], "issues.yah.dev", 8731),
1900 );
1901
1902 let report = collate_workspace_ingress(root).unwrap();
1903 assert!(report.problems.is_empty(), "{:?}", report.problems);
1904 assert_eq!(report.collation.front_doors.len(), 1);
1905 let door = &report.collation.front_doors[0];
1906 assert_eq!(door.machine, "us-east-001");
1907 assert_eq!(
1908 door.passway_upstreams().unwrap(),
1909 vec!["issues.yah.dev=100.64.0.5:8731", "yah.dev=100.64.0.5:8080"]
1910 );
1911 }
1912
1913 #[test]
1914 fn a_cross_service_hostname_clash_is_reported_with_both_declarations() {
1915 let dir = tempdir().unwrap();
1919 let root = dir.path();
1920 write_service(root, "svc-a", "svc-a/site");
1921 write_mirror(
1922 root,
1923 "svc-a",
1924 "cloud",
1925 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
1926 );
1927 write_service(root, "svc-b", "svc-b/site");
1928 write_mirror(
1929 root,
1930 "svc-b",
1931 "cloud",
1932 &fronted_mirror("cloudflare-tunnel", &["us-west-001"], "yah.dev", 8080),
1933 );
1934
1935 let report = collate_workspace_ingress(root).unwrap();
1936 let fatal: Vec<String> = report
1937 .problems
1938 .iter()
1939 .filter(|p| p.is_fatal())
1940 .map(|p| p.message())
1941 .collect();
1942 assert_eq!(fatal.len(), 1, "{fatal:?}");
1943 assert!(fatal[0].contains("svc-a/cloud"), "{}", fatal[0]);
1944 assert!(fatal[0].contains("svc-b/cloud"), "{}", fatal[0]);
1945 }
1946
1947 #[test]
1948 fn one_mirrors_broken_declaration_does_not_hide_the_rest() {
1949 let dir = tempdir().unwrap();
1953 let root = dir.path();
1954 write_service(root, "svc-broken", "svc-broken/site");
1955 write_mirror(
1956 root,
1957 "svc-broken",
1958 "cloud",
1959 "schema_version = 1\nshape = \"single-machine\"\n\
1960 ingress_machines = [\"us-east-001\"]\n\
1961 [providers.compute]\nuse = \"hetzner\"\nzone = \"a.yah.dev\"\nport = 8080\n",
1962 );
1963 write_service(root, "svc-ok", "svc-ok/site");
1964 write_mirror(
1965 root,
1966 "svc-ok",
1967 "cloud",
1968 &fronted_mirror("passway", &["us-east-001"], "b.yah.dev", 8080),
1969 );
1970
1971 let report = collate_workspace_ingress(root).unwrap();
1972 assert_eq!(report.problems.len(), 1);
1973 assert!(
1974 report.problems[0].message().contains("svc-broken/cloud"),
1975 "{}",
1976 report.problems[0].message()
1977 );
1978 assert_eq!(report.collation.front_doors.len(), 1, "svc-ok still collates");
1979 }
1980
1981 #[test]
1982 fn a_mirror_with_no_ingress_contributes_nothing_and_is_not_a_finding() {
1983 let dir = tempdir().unwrap();
1984 let root = dir.path();
1985 write_service(root, "svc", "svc/site");
1986 write_mirror(root, "svc", "dev", &local_static_mirror(4322));
1987 let report = collate_workspace_ingress(root).unwrap();
1988 assert!(report.problems.is_empty());
1989 assert!(report.collation.front_doors.is_empty());
1990 }
1991}