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
318pub fn load_camp_local_machine_tomls(
346 workspace_root: &Path,
347) -> anyhow::Result<Vec<(PathBuf, MachineConfig)>> {
348 let dir = machines_dir(workspace_root);
349 if !dir.exists() {
350 return Ok(Vec::new());
351 }
352
353 let mut entries: Vec<_> = std::fs::read_dir(&dir)
354 .with_context(|| format!("reading {}", dir.display()))?
355 .filter_map(|e| e.ok())
356 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
357 .collect();
358 entries.sort_by_key(|e| e.file_name());
359
360 let mut out = Vec::with_capacity(entries.len());
361 for entry in entries {
362 let path = entry.path();
363 let src = match std::fs::read_to_string(&path) {
364 Ok(s) => s,
365 Err(e) => {
366 tracing::warn!(path = %path.display(), error = %e, "skipping unreadable machine toml");
367 continue;
368 }
369 };
370 let machine: MachineConfig = match toml::from_str(&src) {
371 Ok(m) => m,
372 Err(e) => {
373 tracing::warn!(path = %path.display(), error = %e, "skipping unparseable machine toml");
374 continue;
375 }
376 };
377 out.push((path, machine));
378 }
379 Ok(out)
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct InertTaint {
387 pub machine: String,
389 pub machine_toml: PathBuf,
391 pub key: String,
393}
394
395impl InertTaint {
396 pub fn message(&self) -> String {
397 format!(
398 "machine {:?} declares the taint {:?}, which no placement decision can read\n\
399 \u{2192} a taint fires in exactly two ways: as repulsion \
400 (`no-server` / `no-appliance` / `no-job`, an absolute block on that archetype), \
401 or as affinity (a key a workload names in `yah.placement.requires-taint`).\n\
402 \u{2192} legal keys today: {}\n\
403 \u{2192} if this is a *fact* about the node rather than a placement input, \
404 move it to `mesh_tags` or a comment; if it should really constrain placement, \
405 add it to cloud::config::AFFINITY_TAINT_KEYS together with the workload that \
406 requires it.\n {}",
407 self.machine,
408 self.key,
409 live_taint_keys().join(", "),
410 self.machine_toml.display(),
411 )
412 }
413}
414
415pub fn check_inert_taints(workspace_root: &Path) -> anyhow::Result<Vec<InertTaint>> {
434 let mut found = Vec::new();
435 for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
436 for key in machine.inert_taints() {
437 found.push(InertTaint {
438 machine: machine.name.clone(),
439 machine_toml: path.clone(),
440 key: key.to_string(),
441 });
442 }
443 }
444 Ok(found)
445}
446
447const RETIRED_ARCH_TAG_PREFIX: &str = "tier:";
453const ARCH_TAG_PREFIX: &str = "arch:";
455
456#[derive(Debug, Clone, PartialEq, Eq)]
458pub struct RetiredArchTag {
459 pub machine: String,
460 pub machine_toml: PathBuf,
461 pub tag: String,
463}
464
465impl RetiredArchTag {
466 pub fn replacement(&self) -> String {
468 format!(
469 "{ARCH_TAG_PREFIX}{}",
470 self.tag.trim_start_matches(RETIRED_ARCH_TAG_PREFIX)
471 )
472 }
473
474 pub fn message(&self) -> String {
475 format!(
476 "machine {:?} declares the retired mesh tag {:?} — rename it to {:?}\n\
477 \u{2192} `tier:x86` / `tier:arm` were renamed to `arch:x86` / `arch:arm` \
478 (R763): the value is a CPU architecture, not a tier, and `tier:` is \
479 reserved for the environment axis.\n\
480 \u{2192} this does not fail loudly on its own, which is why it is checked \
481 here: `qed::platform::build_worker_mesh_tags` now requests `arch:<arch>`, \
482 and placement is SUPERSET matching, so a node still carrying the old tag \
483 simply stops matching and the build reports 'no node' instead of \
484 'wrong tag'.\n {}",
485 self.machine,
486 self.tag,
487 self.replacement(),
488 self.machine_toml.display(),
489 )
490 }
491}
492
493pub fn check_retired_arch_tags(workspace_root: &Path) -> anyhow::Result<Vec<RetiredArchTag>> {
501 let mut found = Vec::new();
502 for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
503 for tag in machine
504 .mesh_tags
505 .iter()
506 .filter(|t| t.starts_with(RETIRED_ARCH_TAG_PREFIX))
507 {
508 found.push(RetiredArchTag {
509 machine: machine.name.clone(),
510 machine_toml: path.clone(),
511 tag: tag.clone(),
512 });
513 }
514 }
515 Ok(found)
516}
517
518#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct UnroledSovereignMember {
523 pub machine: String,
524 pub machine_toml: PathBuf,
525 pub group: String,
528}
529
530impl UnroledSovereignMember {
531 pub fn message(&self) -> String {
532 format!(
533 "machine {:?} declares `sovereign_group = {:?}` but no `sovereign_role`\n\
534 \u{2192} membership and quorum eligibility are separate axes (R605-F12): a node can \
535 be inside a group's blast radius — its secrets, its upgrade cadence, its \
536 destruction — and still never hold a seat in its quorum.\n\
537 \u{2192} an absent role reads as `\"voter\"`, so this box is quorum-eligible today. \
538 That is the pre-R605-F12 meaning and is often right; the complaint is that nothing \
539 records whether anyone decided it.\n\
540 \u{2192} add `sovereign_role = \"voter\"` or `sovereign_role = \"non-voter\"` as a \
541 TOP-LEVEL key (below a `[table]` header TOML makes it a field of that table, and \
542 every consumer reads it as absent).\n {}",
543 self.machine,
544 self.group,
545 self.machine_toml.display(),
546 )
547 }
548}
549
550pub fn check_unroled_sovereign_members(
568 workspace_root: &Path,
569) -> anyhow::Result<Vec<UnroledSovereignMember>> {
570 let mut found = Vec::new();
571 for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
572 if let (Some(group), None) = (&machine.sovereign_group, &machine.sovereign_role) {
573 found.push(UnroledSovereignMember {
574 machine: machine.name.clone(),
575 machine_toml: path.clone(),
576 group: group.clone(),
577 });
578 }
579 }
580 Ok(found)
581}
582
583#[derive(Debug, Clone, PartialEq, Eq)]
587pub struct LanDialTarget {
588 pub machine: String,
589 pub machine_toml: PathBuf,
590 pub url: String,
592 pub mesh_ipv4: Option<String>,
595}
596
597impl LanDialTarget {
598 pub fn message(&self) -> String {
599 let fix = match &self.mesh_ipv4 {
600 Some(ip) => format!(
601 "\u{2192} this box IS mesh-joined at {ip}: DELETE the `yubaba` line. \
602 `[registration].mesh_ipv4` composes with `[connect].yubaba_port` on its own, \
603 and `[connect].address` already records the LAN address as metadata."
604 ),
605 None => "\u{2192} this box has no `[registration].mesh_ipv4`: mesh-join it and record \
606 the tailnet address, or taint it out of placement. Until then it is \
607 unresolvable to automation and `MachineConfig::reach` refuses it by name."
608 .to_string(),
609 };
610 format!(
611 "machine {:?} declares `[connect].yubaba = {:?}` — a LAN address in the field every \
612 automated path dials\n\
613 \u{2192} the LAN address is an emergency break-glass route, never an official one \
614 (R605-T10, operator 2026-08-19). Automation ALWAYS assumes the caller is not on that \
615 LAN, and this camp genuinely is not — it sits on 192.168.22.0/22 with no route to \
616 192.168.10.0/24.\n\
617 \u{2192} it does not sit beside the mesh route, it OVERRODE it: before this check, a \
618 declared literal beat `[registration].mesh_ipv4` outright (R707-T6), so a healthy \
619 mesh-joined build worker was elected and then dialed at an address only its own \
620 building can reach.\n\
621 {fix}\n\
622 \u{2192} keeping the LAN address is fine and wanted — in `[connect].address` and \
623 `[connect].ssh`, which no resolver dials. A manual SSH session may use it once a \
624 human confirms they are on that LAN.\n {}",
625 self.machine,
626 self.url,
627 self.machine_toml.display(),
628 )
629 }
630}
631
632pub fn check_lan_dial_targets(workspace_root: &Path) -> anyhow::Result<Vec<LanDialTarget>> {
649 let mut found = Vec::new();
650 for (path, machine) in load_camp_local_machine_tomls(workspace_root)? {
651 let Some(url) = machine.connect.as_ref().and_then(|c| c.yubaba.as_deref()) else {
652 continue;
653 };
654 if private_ipv4_from_url(url).is_none() {
655 continue;
656 }
657 found.push(LanDialTarget {
658 machine: machine.name.clone(),
659 machine_toml: path.clone(),
660 url: url.to_string(),
661 mesh_ipv4: machine.mesh_ipv4().map(str::to_string),
662 });
663 }
664 Ok(found)
665}
666
667#[derive(Debug, Clone, PartialEq, Eq)]
672pub struct IngressFloatingIpProblem {
673 pub machine: String,
674 pub machine_toml: PathBuf,
675 pub kind: IngressFloatingIpProblemKind,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq)]
680pub enum IngressFloatingIpProblemKind {
681 Blank,
683 NoAdapter { provider: String },
686 CohortDisagreement {
689 group: String,
690 this_ip: String,
691 other_machine: String,
692 other_ip: String,
693 },
694}
695
696impl IngressFloatingIpProblem {
697 pub fn message(&self) -> String {
698 let body = match &self.kind {
699 IngressFloatingIpProblemKind::Blank => {
700 "declares an empty `ingress_floating_ip`\n\
701 \u{2192} an empty string is not \"no floating IP\" — omit the key entirely for \
702 that, which is the normal case and a clean skip. A blank value reads as a \
703 declaration and resolves to nothing."
704 .to_string()
705 }
706 IngressFloatingIpProblemKind::NoAdapter { provider } => format!(
707 "declares an `ingress_floating_ip` but its provider is {provider:?}, which has \
708 no floating-IP adapter\n\
709 \u{2192} floating/reserved IPs are implemented for hetzner, ovh and vultr \
710 (cloud::provider::floating_ip's registry). Nothing in this codebase can move \
711 this IP, so the declaration is inert — and inert in the worst way, because it \
712 reads as a working failover path.\n\
713 \u{2192} if the box really does carry a public IP that never moves, that is \
714 what the `public-ip` taint plus `[connect].address` already say."
715 ),
716 IngressFloatingIpProblemKind::CohortDisagreement {
717 group,
718 this_ip,
719 other_machine,
720 other_ip,
721 } => format!(
722 "declares `ingress_floating_ip = {this_ip:?}` but {other_machine} in the same \
723 sovereign_group {group:?} declares {other_ip:?}\n\
724 \u{2192} the ingress floating IP is ONE resource that moves between the boxes \
725 of a cohort as ownership flips. Two ids means an ownership flip reassigns a \
726 different IP than the one currently serving traffic — the old IP stays pointed \
727 at the dead box and the new one was never in DNS.\n\
728 \u{2192} the symptom is a failover that reports success and serves nothing, \
729 which is why this is refused here rather than discovered during one."
730 ),
731 };
732 format!(
733 "machine {:?} {body}\n {}",
734 self.machine,
735 self.machine_toml.display(),
736 )
737 }
738}
739
740pub fn check_ingress_floating_ip(
757 workspace_root: &Path,
758) -> anyhow::Result<Vec<IngressFloatingIpProblem>> {
759 let machines = load_camp_local_machine_tomls(workspace_root)?;
760 let mut found = Vec::new();
761
762 let mut anchor: BTreeMap<String, (String, String)> = BTreeMap::new();
767
768 for (path, machine) in &machines {
769 let Some(ip) = machine.ingress_floating_ip.as_deref() else {
770 continue;
771 };
772 let mut push = |kind| {
773 found.push(IngressFloatingIpProblem {
774 machine: machine.name.clone(),
775 machine_toml: path.clone(),
776 kind,
777 })
778 };
779 if ip.trim().is_empty() {
780 push(IngressFloatingIpProblemKind::Blank);
781 continue;
782 }
783 if !crate::provider::provider_has_floating_ip_adapter(&machine.provider) {
784 push(IngressFloatingIpProblemKind::NoAdapter {
785 provider: machine.provider.clone(),
786 });
787 }
788 let Some(group) = machine.sovereign_group.as_deref() else {
789 continue;
790 };
791 match anchor.get(group) {
792 None => {
793 anchor.insert(group.to_string(), (machine.name.clone(), ip.to_string()));
794 }
795 Some((other_machine, other_ip)) if other_ip != ip => {
796 push(IngressFloatingIpProblemKind::CohortDisagreement {
797 group: group.to_string(),
798 this_ip: ip.to_string(),
799 other_machine: other_machine.clone(),
800 other_ip: other_ip.clone(),
801 });
802 }
803 Some(_) => {}
804 }
805 }
806 Ok(found)
807}
808
809#[derive(Debug, Clone)]
813pub struct LoadedMirror {
814 pub service: String,
816 pub env: String,
818 pub path: PathBuf,
820 pub mirror: MirrorConfig,
821}
822
823pub fn load_service_mirrors(workspace_root: &Path) -> anyhow::Result<Vec<LoadedMirror>> {
832 let dir = services_dir(workspace_root);
833 if !dir.exists() {
834 return Ok(vec![]);
835 }
836
837 let mut svc_entries: Vec<_> = std::fs::read_dir(&dir)
838 .with_context(|| format!("reading {}", dir.display()))?
839 .filter_map(|e| e.ok())
840 .filter(|e| e.path().is_dir())
841 .collect();
842 svc_entries.sort_by_key(|e| e.file_name());
843
844 let mut out = Vec::new();
845 for entry in svc_entries {
846 let svc_dir = entry.path();
847 let service_toml = svc_dir.join("service.toml");
848 if !service_toml.exists() {
849 continue;
850 }
851 let service = match ServiceConfig::load(&service_toml) {
852 Ok(s) => s,
853 Err(e) => {
854 tracing::warn!(
855 path = %service_toml.display(),
856 error = %e,
857 "skipping service with unparseable service.toml"
858 );
859 continue;
860 }
861 };
862
863 let mirrors_dir = svc_dir.join("mirrors");
864 if !mirrors_dir.exists() {
865 continue;
866 }
867 let mut mirror_entries: Vec<_> = std::fs::read_dir(&mirrors_dir)
868 .with_context(|| format!("reading {}", mirrors_dir.display()))?
869 .filter_map(|e| e.ok())
870 .filter(|e| e.path().extension().map_or(false, |x| x == "toml"))
871 .collect();
872 mirror_entries.sort_by_key(|e| e.file_name());
873
874 for m in mirror_entries {
875 let path = m.path();
876 let mirror = match MirrorConfig::load(&path) {
877 Ok(mc) => mc,
878 Err(e) => {
879 tracing::warn!(
880 path = %path.display(),
881 error = %e,
882 "skipping mirror with parse error"
883 );
884 continue;
885 }
886 };
887 out.push(LoadedMirror {
888 service: service.name.clone(),
889 env: path
890 .file_stem()
891 .and_then(|s| s.to_str())
892 .unwrap_or_default()
893 .to_string(),
894 path,
895 mirror,
896 });
897 }
898 }
899 Ok(out)
900}
901
902#[derive(Debug, Clone, PartialEq, Eq)]
904pub enum IngressProblem {
905 Declaration {
908 service: String,
909 env: String,
910 mirror_toml: PathBuf,
911 detail: String,
912 },
913 Collation { detail: String },
916 Unplaced { label: String },
920}
921
922impl IngressProblem {
923 pub fn is_fatal(&self) -> bool {
926 !matches!(self, Self::Unplaced { .. })
927 }
928
929 pub fn message(&self) -> String {
931 match self {
932 Self::Declaration {
933 service,
934 env,
935 mirror_toml,
936 detail,
937 } => format!(
938 "{service}/{env}: ingress declaration does not plan — {detail}\n\u{2192} {}",
939 mirror_toml.display()
940 ),
941 Self::Collation { detail } => format!(
942 "ingress edges from two services collide on a shared node — {detail}\n\
943 \u{2192} the node's front door is COLLATED from every service that fronts \
944 through it (W305 F2), so this is invisible from either mirror alone."
945 ),
946 Self::Unplaced { label } => format!(
947 "{label}: declares a front door with no machine to run it on — neither the \
948 edge's `machines` nor the fronted slot's placement names a node, so it \
949 publishes nothing.\n\u{2192} add `machines = [...]` to the edge."
950 ),
951 }
952 }
953}
954
955#[derive(Debug, Clone, Default)]
957pub struct IngressReport {
958 pub collation: crate::reconciler::Collation,
960 pub problems: Vec<IngressProblem>,
962}
963
964pub fn collate_workspace_ingress(workspace_root: &Path) -> anyhow::Result<IngressReport> {
989 let machines: Vec<MachineConfig> =
1003 crate::config::resolve_fleet_inventory(workspace_root)?.machines;
1004
1005 let mesh_addrs = crate::reconciler::machine_mesh_addrs(&machines);
1009
1010 let mut planned = Vec::new();
1011 let mut problems = Vec::new();
1012
1013 for m in load_service_mirrors(workspace_root)? {
1014 let placements = match crate::reconciler::resolve_ingress_placements(&machines, &m.mirror) {
1015 Ok(p) => p,
1016 Err(e) => {
1017 problems.push(IngressProblem::Declaration {
1018 service: m.service.clone(),
1019 env: m.env.clone(),
1020 mirror_toml: m.path.clone(),
1021 detail: format!("{e:#}"),
1022 });
1023 continue;
1024 }
1025 };
1026 match crate::reconciler::plan_ingress(&m.mirror, &placements) {
1027 Ok(plans) => planned.extend(plans.into_iter().map(|mut plan| {
1028 plan.resolve_upstreams_from_config(&mesh_addrs);
1029 crate::reconciler::PlannedEdge {
1030 service: m.service.clone(),
1031 env: m.env.clone(),
1032 plan,
1033 }
1034 })),
1035 Err(e) => problems.push(IngressProblem::Declaration {
1036 service: m.service.clone(),
1037 env: m.env.clone(),
1038 mirror_toml: m.path.clone(),
1039 detail: format!("{e:#}"),
1040 }),
1041 }
1042 }
1043
1044 let collation = match crate::reconciler::collate_front_doors(&planned) {
1045 Ok(c) => c,
1046 Err(e) => {
1047 problems.push(IngressProblem::Collation {
1048 detail: format!("{e:#}"),
1049 });
1050 Default::default()
1051 }
1052 };
1053 for label in &collation.unplaced {
1054 problems.push(IngressProblem::Unplaced {
1055 label: label.clone(),
1056 });
1057 }
1058
1059 Ok(IngressReport {
1060 collation,
1061 problems,
1062 })
1063}
1064
1065#[cfg(test)]
1068mod tests {
1069 use super::*;
1070 use tempfile::tempdir;
1071
1072 fn write_service(workspace: &Path, svc_name: &str, component_path: &str) {
1073 let svc_dir = workspace.join(".yah/services").join(svc_name);
1074 std::fs::create_dir_all(&svc_dir).unwrap();
1075 let toml = format!(
1076 "schema_version = 1\nname = \"{svc_name}\"\ndomain = \"{svc_name}.example.com\"\n\
1077 [[components]]\nid = \"models\"\nkind = \"static-asset\"\n\
1078 path = \"{component_path}\"\nrole = \"static\"\n"
1079 );
1080 std::fs::write(svc_dir.join("service.toml"), toml).unwrap();
1081 }
1082
1083 fn write_workload_with_aliases(dir: &Path, aliases: &[(&str, &str)]) {
1084 std::fs::create_dir_all(dir).unwrap();
1085 let alias_lines: String = aliases
1086 .iter()
1087 .map(|(k, v)| format!("\"{k}\" = \"{v}\"\n"))
1088 .collect();
1089 let content = format!(
1090 "kind = \"static-asset\"\nschema_version = \"V1\"\n\
1091 [aliases]\n{alias_lines}"
1092 );
1093 std::fs::write(dir.join("workload.toml"), content).unwrap();
1094 }
1095
1096 #[test]
1097 fn cloud_validate_clean_workspace_returns_empty() {
1098 let dir = tempdir().unwrap();
1099 let root = dir.path();
1100
1101 write_service(root, "svc-a", "svc-a/models");
1102 write_workload_with_aliases(
1103 &root.join("svc-a/models"),
1104 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1105 );
1106
1107 let collisions = check_alias_collisions(root).unwrap();
1108 assert!(
1109 collisions.is_empty(),
1110 "expected no collisions: {collisions:?}"
1111 );
1112 }
1113
1114 #[test]
1115 fn cloud_validate_rejects_alias_collision() {
1116 let dir = tempdir().unwrap();
1117 let root = dir.path();
1118
1119 write_service(root, "svc-a", "svc-a/models");
1120 write_workload_with_aliases(
1121 &root.join("svc-a/models"),
1122 &[("whisper-default-ggml", "svc-a/whisper/model.bin")],
1123 );
1124
1125 write_service(root, "svc-b", "svc-b/models");
1126 write_workload_with_aliases(
1127 &root.join("svc-b/models"),
1128 &[("whisper-default-ggml", "svc-b/whisper/model.bin")],
1129 );
1130
1131 let collisions = check_alias_collisions(root).unwrap();
1132 assert_eq!(
1133 collisions.len(),
1134 1,
1135 "expected one collision: {collisions:?}"
1136 );
1137 let c = &collisions[0];
1138 assert_eq!(c.alias, "whisper-default-ggml");
1139 assert_eq!(c.first.service, "svc-a");
1140 assert_eq!(c.second.service, "svc-b");
1141
1142 let msg = c.message();
1143 assert!(msg.contains("whisper-default-ggml"), "message: {msg}");
1144 assert!(msg.contains("svc-a"), "message: {msg}");
1145 assert!(msg.contains("svc-b"), "message: {msg}");
1146 }
1147
1148 #[test]
1149 fn cloud_validate_distinct_aliases_no_collision() {
1150 let dir = tempdir().unwrap();
1151 let root = dir.path();
1152
1153 write_service(root, "svc-a", "svc-a/models");
1154 write_workload_with_aliases(
1155 &root.join("svc-a/models"),
1156 &[
1157 ("whisper-default-ggml", "svc-a/model.bin"),
1158 ("whisper-default", "svc-a/model.bin"),
1159 ],
1160 );
1161
1162 write_service(root, "svc-b", "svc-b/models");
1163 write_workload_with_aliases(
1164 &root.join("svc-b/models"),
1165 &[("whisper-default-coreml", "svc-b/model.tar.gz")],
1166 );
1167
1168 let collisions = check_alias_collisions(root).unwrap();
1169 assert!(collisions.is_empty());
1170 }
1171
1172 #[test]
1173 fn cloud_validate_multiple_collisions_all_reported() {
1174 let dir = tempdir().unwrap();
1175 let root = dir.path();
1176
1177 write_service(root, "svc-a", "svc-a/models");
1178 write_workload_with_aliases(
1179 &root.join("svc-a/models"),
1180 &[
1181 ("alias-one", "svc-a/one.bin"),
1182 ("alias-two", "svc-a/two.bin"),
1183 ],
1184 );
1185
1186 write_service(root, "svc-b", "svc-b/models");
1187 write_workload_with_aliases(
1188 &root.join("svc-b/models"),
1189 &[
1190 ("alias-one", "svc-b/one.bin"),
1191 ("alias-two", "svc-b/two.bin"),
1192 ],
1193 );
1194
1195 let collisions = check_alias_collisions(root).unwrap();
1196 assert_eq!(collisions.len(), 2);
1197 let names: Vec<_> = collisions.iter().map(|c| c.alias.as_str()).collect();
1198 assert!(names.contains(&"alias-one"));
1199 assert!(names.contains(&"alias-two"));
1200 }
1201
1202 #[test]
1203 fn cloud_validate_missing_services_dir_is_not_error() {
1204 let dir = tempdir().unwrap();
1205 let collisions = check_alias_collisions(dir.path()).unwrap();
1206 assert!(collisions.is_empty());
1207 }
1208
1209 #[test]
1210 fn cloud_validate_non_static_asset_workloads_ignored() {
1211 let dir = tempdir().unwrap();
1212 let root = dir.path();
1213
1214 write_service(root, "svc-a", "svc-a/api");
1215 let workload_dir = root.join("svc-a/api");
1217 std::fs::create_dir_all(&workload_dir).unwrap();
1218 std::fs::write(
1221 workload_dir.join("workload.toml"),
1222 "schema_version = \"V1\"\nname = \"api\"\nkind = \"mesofact-static\"\n\
1223 bundle_dir = \"dist\"\n",
1224 )
1225 .unwrap();
1226
1227 let collisions = check_alias_collisions(root).unwrap();
1228 assert!(collisions.is_empty());
1229 }
1230
1231 fn write_mirror(workspace: &Path, svc: &str, env: &str, body: &str) {
1234 let dir = workspace.join(".yah/services").join(svc).join("mirrors");
1235 std::fs::create_dir_all(&dir).unwrap();
1236 std::fs::write(dir.join(format!("{env}.toml")), body).unwrap();
1237 }
1238
1239 fn local_static_mirror(port: u16) -> String {
1240 format!(
1241 "schema_version = 1\nshape = \"local\"\n\
1242 [providers.static]\nkind = \"local-static\"\nport = {port}\n"
1243 )
1244 }
1245
1246 #[test]
1247 fn port_collision_across_services_and_envs_is_flagged() {
1248 let dir = tempdir().unwrap();
1249 let root = dir.path();
1250 write_service(root, "scrabcake", "scrabcake/site");
1251 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4322));
1252 write_service(root, "yah-marketing", "yah-marketing/site");
1253 write_mirror(
1254 root,
1255 "yah-marketing",
1256 "pond",
1257 "schema_version = 1\nshape = \"local\"\n\
1258 [providers.static]\nkind = \"miniflare-container\"\nport = 4322\n",
1259 );
1260
1261 let cols = check_port_collisions(root).unwrap();
1262 assert_eq!(cols.len(), 1, "{cols:?}");
1263 assert_eq!(cols[0].port, 4322);
1264 assert_eq!(cols[0].first.service, "scrabcake");
1266 assert_eq!(cols[0].second.service, "yah-marketing");
1267 assert!(cols[0].is_cross_service(), "different services collide");
1268 let msg = cols[0].message();
1269 assert!(msg.contains("4322"), "{msg}");
1270 assert!(msg.contains("scrabcake"), "{msg}");
1271 assert!(msg.contains("yah-marketing"), "{msg}");
1272 }
1273
1274 #[test]
1275 fn distinct_ports_no_collision() {
1276 let dir = tempdir().unwrap();
1277 let root = dir.path();
1278 write_service(root, "a", "a/site");
1279 write_mirror(root, "a", "dev", &local_static_mirror(4322));
1280 write_service(root, "b", "b/site");
1281 write_mirror(root, "b", "dev", &local_static_mirror(4323));
1282 assert!(check_port_collisions(root).unwrap().is_empty());
1283 }
1284
1285 #[test]
1286 fn same_service_two_envs_reusing_a_port_is_flagged() {
1287 let dir = tempdir().unwrap();
1289 let root = dir.path();
1290 write_service(root, "scrabcake", "scrabcake/site");
1291 write_mirror(root, "scrabcake", "dev", &local_static_mirror(4352));
1292 write_mirror(root, "scrabcake", "cloud", &local_static_mirror(4352));
1293 let cols = check_port_collisions(root).unwrap();
1294 assert_eq!(cols.len(), 1, "{cols:?}");
1295 assert_eq!(cols[0].port, 4352);
1296 assert_eq!(cols[0].first.env, "cloud");
1298 assert_eq!(cols[0].second.env, "dev");
1299 assert!(
1300 !cols[0].is_cross_service(),
1301 "same service across envs is NOT cross-service"
1302 );
1303 }
1304
1305 #[test]
1306 fn reference_slots_do_not_bind_localhost_and_are_ignored() {
1307 let dir = tempdir().unwrap();
1310 let root = dir.path();
1311 let ref_slot = "schema_version = 1\nshape = \"local\"\n\
1312 [providers.static]\nuse = \"cloudflare\"\nport = 8080\n";
1313 write_service(root, "a", "a/site");
1314 write_mirror(root, "a", "cloud", ref_slot);
1315 write_service(root, "b", "b/site");
1316 write_mirror(root, "b", "cloud", ref_slot);
1317 assert!(check_port_collisions(root).unwrap().is_empty());
1318 }
1319
1320 #[test]
1321 fn minio_api_and_console_ports_collide_across_ponds() {
1322 let dir = tempdir().unwrap();
1324 let root = dir.path();
1325 let minio = "schema_version = 1\nshape = \"local\"\n\
1326 [providers.object_store]\nkind = \"minio-container\"\napi_port = 9000\nconsole_port = 9001\n";
1327 write_service(root, "a", "a/site");
1328 write_mirror(root, "a", "pond", minio);
1329 write_service(root, "b", "b/site");
1330 write_mirror(root, "b", "pond", minio);
1331 let cols = check_port_collisions(root).unwrap();
1332 assert_eq!(cols.len(), 2, "{cols:?}");
1334 let ports: Vec<u16> = cols.iter().map(|c| c.port).collect();
1335 assert!(ports.contains(&9000));
1336 assert!(ports.contains(&9001));
1337 }
1338
1339 #[test]
1340 fn missing_services_dir_is_not_error_for_ports() {
1341 let dir = tempdir().unwrap();
1342 assert!(check_port_collisions(dir.path()).unwrap().is_empty());
1343 }
1344
1345 fn assert_machine_toml_parses(path: &Path) {
1353 let src = std::fs::read_to_string(path).unwrap();
1354 toml::from_str::<MachineConfig>(&src)
1355 .unwrap_or_else(|e| panic!("fixture {} does not parse as MachineConfig: {e}\n{src}", path.display()));
1356 }
1357
1358 fn write_machine_tags(workspace: &Path, name: &str, mesh_tags: &[&str]) {
1359 let dir = workspace.join(".yah/infra/machines");
1360 std::fs::create_dir_all(&dir).unwrap();
1361 let list: Vec<String> = mesh_tags.iter().map(|t| format!("\"{t}\"")).collect();
1362 let path = dir.join(format!("{name}.toml"));
1363 std::fs::write(
1364 &path,
1365 format!(
1366 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = [{}]\n",
1367 list.join(", ")
1368 ),
1369 )
1370 .unwrap();
1371 assert_machine_toml_parses(&path);
1372 }
1373
1374 #[test]
1375 fn the_current_arch_tag_is_clean() {
1376 let dir = tempdir().unwrap();
1377 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "arch:x86", "os:linux"]);
1378 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1379 }
1380
1381 #[test]
1382 fn a_stale_tier_arch_tag_is_flagged_with_its_replacement() {
1383 let dir = tempdir().unwrap();
1384 write_machine_tags(dir.path(), "n1", &["tag:build-worker", "tier:arm", "os:linux"]);
1385 let found = check_retired_arch_tags(dir.path()).unwrap();
1386 assert_eq!(found.len(), 1, "{found:?}");
1387 assert_eq!(found[0].tag, "tier:arm");
1388 assert_eq!(found[0].replacement(), "arch:arm");
1389 let msg = found[0].message();
1390 assert!(msg.contains("arch:arm"), "{msg}");
1393 assert!(msg.contains("n1"), "{msg}");
1394 }
1395
1396 #[test]
1400 fn a_stale_tag_is_not_caught_by_the_inert_taint_lint() {
1401 let dir = tempdir().unwrap();
1402 write_machine_tags(dir.path(), "n1", &["tier:x86"]);
1403 assert!(
1404 check_inert_taints(dir.path()).unwrap().is_empty(),
1405 "mesh tags are not taints — this needs its own check"
1406 );
1407 assert_eq!(check_retired_arch_tags(dir.path()).unwrap().len(), 1);
1408 }
1409
1410 #[test]
1411 fn missing_machines_dir_is_not_error_for_arch_tags() {
1412 let dir = tempdir().unwrap();
1413 assert!(check_retired_arch_tags(dir.path()).unwrap().is_empty());
1414 }
1415
1416 fn write_sovereign_machine(workspace: &Path, name: &str, stamp: &str) {
1423 let dir = workspace.join(".yah/infra/machines");
1424 std::fs::create_dir_all(&dir).unwrap();
1425 let path = dir.join(format!("{name}.toml"));
1426 std::fs::write(
1427 &path,
1428 format!("name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n{stamp}\n"),
1429 )
1430 .unwrap();
1431 assert_machine_toml_parses(&path);
1432 }
1433
1434 fn write_fip_machine(workspace: &Path, name: &str, provider: &str, stamp: &str) {
1439 let dir = workspace.join(".yah/infra/machines");
1440 std::fs::create_dir_all(&dir).unwrap();
1441 let path = dir.join(format!("{name}.toml"));
1442 std::fs::write(
1443 &path,
1444 format!("name = \"{name}\"\nprovider = \"{provider}\"\nmesh_tags = []\n{stamp}\n"),
1445 )
1446 .unwrap();
1447 assert_machine_toml_parses(&path);
1448 }
1449
1450 #[test]
1454 fn a_machine_with_no_ingress_floating_ip_is_never_flagged() {
1455 let dir = tempdir().unwrap();
1456 write_fip_machine(dir.path(), "us-west-002", "static", "");
1457 write_fip_machine(dir.path(), "n1", "digitalocean", "");
1458 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1459 }
1460
1461 #[test]
1462 fn a_valid_declaration_on_an_adapter_backed_provider_is_clean() {
1463 let dir = tempdir().unwrap();
1464 write_fip_machine(
1465 dir.path(),
1466 "us-west-001",
1467 "hetzner",
1468 "ingress_floating_ip = \"42\"",
1469 );
1470 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1471 }
1472
1473 #[test]
1475 fn a_provider_with_no_floating_ip_adapter_is_flagged_with_what_is_supported() {
1476 let dir = tempdir().unwrap();
1477 write_fip_machine(
1478 dir.path(),
1479 "us-east-001",
1480 "static",
1481 "ingress_floating_ip = \"51.81.85.200\"",
1482 );
1483 let found = check_ingress_floating_ip(dir.path()).unwrap();
1484 assert_eq!(found.len(), 1, "{found:?}");
1485 assert_eq!(
1486 found[0].kind,
1487 IngressFloatingIpProblemKind::NoAdapter {
1488 provider: "static".into()
1489 }
1490 );
1491 let msg = found[0].message();
1492 assert!(msg.contains("us-east-001"), "{msg}");
1493 assert!(msg.contains("hetzner"), "the message must name what IS supported: {msg}");
1494 assert!(msg.ends_with("us-east-001.toml"), "{msg}");
1495 }
1496
1497 #[test]
1500 fn a_blank_declaration_is_flagged_rather_than_read_as_absent() {
1501 let dir = tempdir().unwrap();
1502 write_fip_machine(
1503 dir.path(),
1504 "us-west-001",
1505 "hetzner",
1506 "ingress_floating_ip = \" \"",
1507 );
1508 let found = check_ingress_floating_ip(dir.path()).unwrap();
1509 assert_eq!(found.len(), 1, "{found:?}");
1510 assert_eq!(found[0].kind, IngressFloatingIpProblemKind::Blank);
1511 }
1512
1513 #[test]
1517 fn two_ids_in_one_sovereign_group_are_refused_and_name_both_machines() {
1518 let dir = tempdir().unwrap();
1519 write_fip_machine(
1520 dir.path(),
1521 "us-east-001",
1522 "hetzner",
1523 "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1524 );
1525 write_fip_machine(
1526 dir.path(),
1527 "us-west-001",
1528 "hetzner",
1529 "sovereign_group = \"prod\"\ningress_floating_ip = \"77\"",
1530 );
1531 let found = check_ingress_floating_ip(dir.path()).unwrap();
1532 assert_eq!(found.len(), 1, "{found:?}");
1533 let msg = found[0].message();
1534 assert!(msg.contains("us-west-001") && msg.contains("us-east-001"), "{msg}");
1535 assert!(msg.contains("42") && msg.contains("77"), "{msg}");
1536 }
1537
1538 #[test]
1539 fn one_id_across_a_whole_cohort_is_clean() {
1540 let dir = tempdir().unwrap();
1541 for name in ["us-east-001", "us-west-001", "us-south-001"] {
1542 write_fip_machine(
1543 dir.path(),
1544 name,
1545 "hetzner",
1546 "sovereign_group = \"prod\"\ningress_floating_ip = \"42\"",
1547 );
1548 }
1549 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1550 }
1551
1552 #[test]
1555 fn machines_in_no_group_may_hold_different_ips() {
1556 let dir = tempdir().unwrap();
1557 write_fip_machine(
1558 dir.path(),
1559 "us-east-001",
1560 "hetzner",
1561 "ingress_floating_ip = \"42\"",
1562 );
1563 write_fip_machine(
1564 dir.path(),
1565 "us-west-001",
1566 "vultr",
1567 "ingress_floating_ip = \"a-uuid\"",
1568 );
1569 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1570 }
1571
1572 #[test]
1573 fn missing_machines_dir_is_not_error_for_ingress_floating_ip() {
1574 let dir = tempdir().unwrap();
1575 assert!(check_ingress_floating_ip(dir.path()).unwrap().is_empty());
1576 }
1577
1578 #[test]
1579 fn a_group_with_no_role_is_reported_with_the_declaring_file() {
1580 let dir = tempdir().unwrap();
1581 let root = dir.path();
1582 write_sovereign_machine(root, "us-west-001", "sovereign_group = \"prod\"");
1583 let found = check_unroled_sovereign_members(root).unwrap();
1584 assert_eq!(found.len(), 1, "{found:?}");
1585 assert_eq!(found[0].machine, "us-west-001");
1586 assert_eq!(found[0].group, "prod");
1587 assert!(found[0].machine_toml.ends_with("us-west-001.toml"));
1588 let msg = found[0].message();
1589 assert!(msg.contains("voter") && msg.contains("non-voter"), "{msg}");
1592 assert!(msg.contains("TOP-LEVEL"), "{msg}");
1593 }
1594
1595 #[test]
1596 fn either_stated_role_is_clean() {
1597 let dir = tempdir().unwrap();
1598 let root = dir.path();
1599 write_sovereign_machine(
1600 root,
1601 "us-west-001",
1602 "sovereign_group = \"prod\"\nsovereign_role = \"voter\"",
1603 );
1604 write_sovereign_machine(
1605 root,
1606 "us-west-003",
1607 "sovereign_group = \"prod\"\nsovereign_role = \"non-voter\"",
1608 );
1609 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1610 }
1611
1612 #[test]
1616 fn a_machine_in_no_group_is_not_asked_for_a_role() {
1617 let dir = tempdir().unwrap();
1618 let root = dir.path();
1619 write_sovereign_machine(root, "us-west-002", "taints = [\"no-appliance\"]");
1620 assert!(check_unroled_sovereign_members(root).unwrap().is_empty());
1621 }
1622
1623 #[test]
1624 fn unroled_findings_are_ordered_by_file_so_output_is_stable() {
1625 let dir = tempdir().unwrap();
1626 let root = dir.path();
1627 write_sovereign_machine(root, "b-node", "sovereign_group = \"dev\"");
1628 write_sovereign_machine(root, "a-node", "sovereign_group = \"prod\"");
1629 let found = check_unroled_sovereign_members(root).unwrap();
1630 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1631 assert_eq!(names, vec!["a-node", "b-node"]);
1632 }
1633
1634 #[test]
1635 fn missing_machines_dir_is_not_error_for_unroled_members() {
1636 let dir = tempdir().unwrap();
1637 assert!(check_unroled_sovereign_members(dir.path())
1638 .unwrap()
1639 .is_empty());
1640 }
1641
1642 fn write_reach_machine(workspace: &Path, name: &str, connect: &str, registration: &str) {
1647 let dir = workspace.join(".yah/infra/machines");
1648 std::fs::create_dir_all(&dir).unwrap();
1649 let path = dir.join(format!("{name}.toml"));
1650 let reg = if registration.is_empty() {
1651 String::new()
1652 } else {
1653 format!("\n[registration]\n{registration}\n")
1654 };
1655 std::fs::write(
1656 &path,
1657 format!(
1658 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\n\n\
1659 [connect]\n{connect}\n{reg}"
1660 ),
1661 )
1662 .unwrap();
1663 assert_machine_toml_parses(&path);
1664 }
1665
1666 #[test]
1667 fn a_lan_literal_in_the_dialed_field_is_reported_with_its_file() {
1668 let dir = tempdir().unwrap();
1669 let root = dir.path();
1670 write_reach_machine(
1672 root,
1673 "us-west-011",
1674 "address = \"192.168.10.11\"\nssh = \"yah@192.168.10.11\"\n\
1675 identity_file = \"~/.ssh/yah\"\n\
1676 yubaba = \"http://192.168.10.11:7443\"",
1677 "",
1678 );
1679 let found = check_lan_dial_targets(root).unwrap();
1680 assert_eq!(found.len(), 1);
1681 assert_eq!(found[0].machine, "us-west-011");
1682 assert_eq!(found[0].url, "http://192.168.10.11:7443");
1683 assert_eq!(found[0].mesh_ipv4, None);
1684 let msg = found[0].message();
1685 assert!(msg.contains("mesh-join it"), "{msg}");
1686 assert!(msg.contains("us-west-011.toml"), "{msg}");
1687 }
1688
1689 #[test]
1692 fn a_mesh_joined_lan_declarer_is_told_to_delete_the_line() {
1693 let dir = tempdir().unwrap();
1694 let root = dir.path();
1695 write_reach_machine(
1696 root,
1697 "us-west-014",
1698 "address = \"192.168.10.14\"\nssh = \"yah@192.168.10.14\"\n\
1699 identity_file = \"~/.ssh/yah\"\n\
1700 yubaba = \"http://192.168.10.14:7443\"",
1701 "mesh_ipv4 = \"100.64.0.6\"",
1702 );
1703 let found = check_lan_dial_targets(root).unwrap();
1704 assert_eq!(found.len(), 1);
1705 assert_eq!(found[0].mesh_ipv4.as_deref(), Some("100.64.0.6"));
1706 let msg = found[0].message();
1707 assert!(msg.contains("DELETE the `yubaba` line"), "{msg}");
1708 assert!(msg.contains("100.64.0.6"), "{msg}");
1709 }
1710
1711 #[test]
1715 fn mesh_loopback_and_metadata_only_lan_addresses_are_clean() {
1716 let dir = tempdir().unwrap();
1717 let root = dir.path();
1718 write_reach_machine(
1719 root,
1720 "meshed",
1721 "address = \"192.168.10.15\"\nssh = \"yah@192.168.10.15\"\n\
1722 identity_file = \"~/.ssh/yah\"",
1723 "mesh_ipv4 = \"100.64.0.7\"",
1724 );
1725 write_reach_machine(
1726 root,
1727 "tunnelled",
1728 "address = \"192.168.10.16\"\nssh = \"yah@192.168.10.16\"\n\
1729 identity_file = \"~/.ssh/yah\"\n\
1730 yubaba = \"http://127.0.0.1:7443\"",
1731 "",
1732 );
1733 write_reach_machine(
1734 root,
1735 "public",
1736 "address = \"45.32.194.254\"\nssh = \"debian@45.32.194.254\"\n\
1737 identity_file = \"~/.ssh/yah\"\n\
1738 yubaba = \"http://45.32.194.254:7443\"",
1739 "",
1740 );
1741 assert!(check_lan_dial_targets(root).unwrap().is_empty());
1742 }
1743
1744 #[test]
1745 fn missing_machines_dir_is_not_error_for_lan_dial_targets() {
1746 let dir = tempdir().unwrap();
1747 assert!(check_lan_dial_targets(dir.path()).unwrap().is_empty());
1748 }
1749
1750 fn write_machine(workspace: &Path, name: &str, taints: &[&str]) {
1753 let dir = workspace.join(".yah/infra/machines");
1754 std::fs::create_dir_all(&dir).unwrap();
1755 let list: Vec<String> = taints.iter().map(|t| format!("\"{t}\"")).collect();
1756 let path = dir.join(format!("{name}.toml"));
1757 std::fs::write(
1758 &path,
1759 format!(
1760 "name = \"{name}\"\nprovider = \"static\"\nmesh_tags = []\ntaints = [{}]\n",
1761 list.join(", ")
1762 ),
1763 )
1764 .unwrap();
1765 assert_machine_toml_parses(&path);
1766 }
1767
1768 #[test]
1769 fn archetype_repel_keys_and_affinity_keys_are_clean() {
1770 let dir = tempdir().unwrap();
1771 let root = dir.path();
1772 write_machine(root, "worker", &["no-server", "no-appliance", "no-job"]);
1773 write_machine(root, "edge", &["public-ip"]);
1774 write_machine(root, "plain", &[]);
1775 assert!(check_inert_taints(root).unwrap().is_empty());
1776 }
1777
1778 #[test]
1779 fn a_free_form_taint_is_reported_with_the_declaring_file() {
1780 let dir = tempdir().unwrap();
1781 let root = dir.path();
1782 write_machine(root, "us-west-011", &["qa"]);
1784 let found = check_inert_taints(root).unwrap();
1785 assert_eq!(found.len(), 1, "{found:?}");
1786 assert_eq!(found[0].machine, "us-west-011");
1787 assert_eq!(found[0].key, "qa");
1788 assert!(found[0].machine_toml.ends_with("us-west-011.toml"));
1789 let msg = found[0].message();
1790 assert!(msg.contains("no-appliance"), "{msg}");
1793 assert!(msg.contains("public-ip"), "{msg}");
1794 assert!(msg.contains("mesh_tags"), "{msg}");
1795 }
1796
1797 #[test]
1798 fn no_voter_is_inert_because_voter_is_not_an_archetype() {
1799 let dir = tempdir().unwrap();
1802 let root = dir.path();
1803 write_machine(root, "us-west-015", &["no-server", "no-appliance", "no-voter"]);
1804 let found = check_inert_taints(root).unwrap();
1805 assert_eq!(found.len(), 1, "{found:?}");
1806 assert_eq!(found[0].key, "no-voter");
1807 }
1808
1809 #[test]
1810 fn findings_are_ordered_by_file_so_output_is_stable() {
1811 let dir = tempdir().unwrap();
1812 let root = dir.path();
1813 write_machine(root, "b-node", &["qa"]);
1814 write_machine(root, "a-node", &["staging"]);
1815 let found = check_inert_taints(root).unwrap();
1816 let names: Vec<&str> = found.iter().map(|f| f.machine.as_str()).collect();
1817 assert_eq!(names, vec!["a-node", "b-node"]);
1818 }
1819
1820 #[test]
1821 fn missing_machines_dir_is_not_error() {
1822 let dir = tempdir().unwrap();
1823 assert!(check_inert_taints(dir.path()).unwrap().is_empty());
1824 }
1825
1826 #[test]
1827 fn an_unparseable_machine_toml_is_skipped_not_fatal() {
1828 let dir = tempdir().unwrap();
1829 let root = dir.path();
1830 let mdir = root.join(".yah/infra/machines");
1831 std::fs::create_dir_all(&mdir).unwrap();
1832 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1833 write_machine(root, "good", &["qa"]);
1834 let found = check_inert_taints(root).unwrap();
1837 assert_eq!(found.len(), 1);
1838 assert_eq!(found[0].machine, "good");
1839 }
1840
1841 #[test]
1844 fn the_lint_loader_skips_an_unparseable_toml_and_still_pairs_the_path() {
1845 let dir = tempdir().unwrap();
1846 let root = dir.path();
1847 let mdir = root.join(".yah/infra/machines");
1848 std::fs::create_dir_all(&mdir).unwrap();
1849 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1850 write_machine(root, "good", &["qa"]);
1851
1852 let loaded = load_camp_local_machine_tomls(root).unwrap();
1853 assert_eq!(loaded.len(), 1, "{loaded:?}");
1854 assert_eq!(loaded[0].1.name, "good");
1855 assert!(loaded[0].0.ends_with("good.toml"));
1856 }
1857
1858 #[test]
1859 fn the_fleet_inventory_fails_the_whole_load_on_one_unparseable_camp_local_toml() {
1860 let dir = tempdir().unwrap();
1865 let root = dir.path();
1866 let mdir = root.join(".yah/infra/machines");
1867 std::fs::create_dir_all(&mdir).unwrap();
1868 std::fs::write(mdir.join("broken.toml"), "name = \n").unwrap();
1869 write_machine(root, "good", &["qa"]);
1870
1871 let err = crate::config::resolve_fleet_inventory(root).unwrap_err();
1872 assert!(format!("{err:#}").contains("broken.toml"), "{err:#}");
1873 }
1874
1875 #[test]
1884 fn a_borrowing_camp_collates_a_front_door_on_a_machine_it_declares_nowhere() {
1885 let dir = tempdir().unwrap();
1886 let owner = dir.path().join("owner");
1888 write_machine(&owner, "us-east-001", &["public-ip"]);
1889
1890 let borrower = dir.path().join("borrower");
1892 std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
1893 std::fs::write(
1894 borrower.join(".yah/infra/sources.toml"),
1895 "schema_version = 1\n\
1896 [[source]]\n\
1897 owner = \"owner\"\n\
1898 kind = \"path\"\n\
1899 path = \"../owner\"\n\
1900 mode = \"read-only\"\n",
1901 )
1902 .unwrap();
1903
1904 assert!(load_camp_local_machine_tomls(&borrower).unwrap().is_empty());
1907
1908 let inventory = crate::config::resolve_fleet_inventory(&borrower).unwrap();
1909 assert_eq!(
1910 inventory.machines.iter().map(|m| m.name.as_str()).collect::<Vec<_>>(),
1911 vec!["us-east-001"],
1912 "the borrowed fleet must resolve through sources.toml"
1913 );
1914 assert_eq!(
1915 inventory.origins.get("us-east-001").map(|o| o.owner.as_str()),
1916 Some("owner"),
1917 "a borrowed machine keeps its provenance"
1918 );
1919 assert_eq!(inventory.contributions.len(), 1);
1920 assert!(inventory.contributions[0].root_exists);
1921 assert_eq!(inventory.contributions[0].machines, 1);
1922
1923 write_service(&borrower, "marketing", "marketing/site");
1926 write_mirror(
1927 &borrower,
1928 "marketing",
1929 "cloud",
1930 &fronted_mirror("passway", &["us-east-001"], "api.example.com", 8080),
1931 );
1932 let report = collate_workspace_ingress(&borrower).unwrap();
1933 assert!(report.problems.is_empty(), "{:?}", report.problems);
1934 assert_eq!(
1935 report
1936 .collation
1937 .front_doors
1938 .iter()
1939 .map(|fd| fd.machine.as_str())
1940 .collect::<Vec<_>>(),
1941 vec!["us-east-001"],
1942 );
1943 }
1944
1945 #[test]
1955 fn a_borrowing_camp_can_place_by_constraint_rather_than_by_pin() {
1956 let dir = tempdir().unwrap();
1957 let owner = dir.path().join("owner");
1958 std::fs::create_dir_all(owner.join(".yah/infra/machines")).unwrap();
1959 std::fs::write(
1960 owner.join(".yah/infra/machines/us-east-001.toml"),
1961 "name = \"us-east-001\"\nprovider = \"static\"\nregion = \"us-east\"\n\
1962 mesh_tags = [\"tag:cloud-runner\"]\ntaints = [\"public-ip\"]\n",
1963 )
1964 .unwrap();
1965
1966 let borrower = dir.path().join("borrower");
1967 std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
1968 std::fs::write(
1969 borrower.join(".yah/infra/sources.toml"),
1970 "schema_version = 1\n[[source]]\nowner = \"owner\"\nkind = \"path\"\n\
1971 path = \"../owner\"\nmode = \"read-only\"\n",
1972 )
1973 .unwrap();
1974 write_service(&borrower, "marketing", "marketing/site");
1975 write_mirror(
1979 &borrower,
1980 "marketing",
1981 "cloud",
1982 "schema_version = 1\nshape = \"single-machine\"\n\
1983 ingress = \"passway\"\ningress_machines = [\"us-east-001\"]\n\
1984 [providers.compute]\nuse = \"hetzner\"\nzone = \"api.example.com\"\n\
1985 port = 8080\n\
1986 required = { regions = [\"us-east\"], mesh_tags = [\"tag:cloud-runner\"] }\n",
1987 );
1988
1989 let report = collate_workspace_ingress(&borrower).unwrap();
1990 assert!(
1991 report.problems.is_empty(),
1992 "a constraint must resolve against the borrowed fleet: {:?}",
1993 report.problems
1994 );
1995 assert_eq!(
1996 report
1997 .collation
1998 .front_doors
1999 .iter()
2000 .map(|fd| fd.machine.as_str())
2001 .collect::<Vec<_>>(),
2002 vec!["us-east-001"],
2003 );
2004 }
2005
2006 #[test]
2011 fn a_broken_link_is_reported_as_absent_rather_than_as_an_empty_fleet() {
2012 let dir = tempdir().unwrap();
2013 let borrower = dir.path().join("borrower");
2014 std::fs::create_dir_all(borrower.join(".yah/infra/machines")).unwrap();
2015 std::fs::write(
2016 borrower.join(".yah/infra/sources.toml"),
2017 "schema_version = 1\n\
2018 [[source]]\n\
2019 owner = \"owner\"\n\
2020 kind = \"path\"\n\
2021 path = \"../not-a-camp\"\n",
2022 )
2023 .unwrap();
2024
2025 let inventory = crate::config::resolve_fleet_inventory(&borrower).unwrap();
2026 assert!(inventory.machines.is_empty());
2027 assert!(!inventory.contributions[0].root_exists);
2028
2029 let described = inventory.describe_sources();
2030 assert!(described.contains("owner"), "{described}");
2031 assert!(described.contains("ABSENT"), "{described}");
2032 }
2033
2034 fn fronted_mirror(provider: &str, machines: &[&str], hostname: &str, port: u16) -> String {
2038 let list = machines
2039 .iter()
2040 .map(|m| format!("\"{m}\""))
2041 .collect::<Vec<_>>()
2042 .join(", ");
2043 format!(
2044 "schema_version = 1\nshape = \"single-machine\"\n\
2045 ingress = \"{provider}\"\ningress_machines = [{list}]\n\
2046 [providers.compute]\nuse = \"hetzner\"\nzone = \"{hostname}\"\n\
2047 port = {port}\nupstream_host = \"100.64.0.5\"\n"
2048 )
2049 }
2050
2051 #[test]
2052 fn two_services_fronting_one_node_collate_into_one_front_door() {
2053 let dir = tempdir().unwrap();
2054 let root = dir.path();
2055 write_service(root, "yah-marketing", "yah-marketing/site");
2056 write_mirror(
2057 root,
2058 "yah-marketing",
2059 "cloud",
2060 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
2061 );
2062 write_service(root, "yah-issues", "yah-issues/site");
2063 write_mirror(
2064 root,
2065 "yah-issues",
2066 "cloud",
2067 &fronted_mirror("passway", &["us-east-001"], "issues.yah.dev", 8731),
2068 );
2069
2070 let report = collate_workspace_ingress(root).unwrap();
2071 assert!(report.problems.is_empty(), "{:?}", report.problems);
2072 assert_eq!(report.collation.front_doors.len(), 1);
2073 let door = &report.collation.front_doors[0];
2074 assert_eq!(door.machine, "us-east-001");
2075 assert_eq!(
2076 door.passway_upstreams().unwrap(),
2077 vec!["issues.yah.dev=100.64.0.5:8731", "yah.dev=100.64.0.5:8080"]
2078 );
2079 }
2080
2081 #[test]
2082 fn a_cross_service_hostname_clash_is_reported_with_both_declarations() {
2083 let dir = tempdir().unwrap();
2087 let root = dir.path();
2088 write_service(root, "svc-a", "svc-a/site");
2089 write_mirror(
2090 root,
2091 "svc-a",
2092 "cloud",
2093 &fronted_mirror("passway", &["us-east-001"], "yah.dev", 8080),
2094 );
2095 write_service(root, "svc-b", "svc-b/site");
2096 write_mirror(
2097 root,
2098 "svc-b",
2099 "cloud",
2100 &fronted_mirror("cloudflare-tunnel", &["us-west-001"], "yah.dev", 8080),
2101 );
2102
2103 let report = collate_workspace_ingress(root).unwrap();
2104 let fatal: Vec<String> = report
2105 .problems
2106 .iter()
2107 .filter(|p| p.is_fatal())
2108 .map(|p| p.message())
2109 .collect();
2110 assert_eq!(fatal.len(), 1, "{fatal:?}");
2111 assert!(fatal[0].contains("svc-a/cloud"), "{}", fatal[0]);
2112 assert!(fatal[0].contains("svc-b/cloud"), "{}", fatal[0]);
2113 }
2114
2115 #[test]
2116 fn one_mirrors_broken_declaration_does_not_hide_the_rest() {
2117 let dir = tempdir().unwrap();
2121 let root = dir.path();
2122 write_service(root, "svc-broken", "svc-broken/site");
2123 write_mirror(
2124 root,
2125 "svc-broken",
2126 "cloud",
2127 "schema_version = 1\nshape = \"single-machine\"\n\
2128 ingress_machines = [\"us-east-001\"]\n\
2129 [providers.compute]\nuse = \"hetzner\"\nzone = \"a.yah.dev\"\nport = 8080\n",
2130 );
2131 write_service(root, "svc-ok", "svc-ok/site");
2132 write_mirror(
2133 root,
2134 "svc-ok",
2135 "cloud",
2136 &fronted_mirror("passway", &["us-east-001"], "b.yah.dev", 8080),
2137 );
2138
2139 let report = collate_workspace_ingress(root).unwrap();
2140 assert_eq!(report.problems.len(), 1);
2141 assert!(
2142 report.problems[0].message().contains("svc-broken/cloud"),
2143 "{}",
2144 report.problems[0].message()
2145 );
2146 assert_eq!(report.collation.front_doors.len(), 1, "svc-ok still collates");
2147 }
2148
2149 #[test]
2150 fn a_mirror_with_no_ingress_contributes_nothing_and_is_not_a_finding() {
2151 let dir = tempdir().unwrap();
2152 let root = dir.path();
2153 write_service(root, "svc", "svc/site");
2154 write_mirror(root, "svc", "dev", &local_static_mirror(4322));
2155 let report = collate_workspace_ingress(root).unwrap();
2156 assert!(report.problems.is_empty());
2157 assert!(report.collation.front_doors.is_empty());
2158 }
2159}