diff --git a/core/archipelago/src/container/quadlet.rs b/core/archipelago/src/container/quadlet.rs index c8df26bc..680fa039 100644 --- a/core/archipelago/src/container/quadlet.rs +++ b/core/archipelago/src/container/quadlet.rs @@ -410,7 +410,18 @@ impl QuadletUnit { environment: app.environment.clone(), devices: app.devices.clone(), add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())], - network_aliases: vec![name.to_string()], + // Container always answers to its own name; manifest extras add the + // short hostnames peers bake in (e.g. indeedhub api/minio/relay). + // Only emitted for Bridge networks (slirp/pasta reject aliases). + network_aliases: { + let mut a = vec![name.to_string()]; + for extra in &app.container.network_aliases { + if !a.iter().any(|x| x == extra) { + a.push(extra.clone()); + } + } + a + }, entrypoint: app.container.entrypoint.clone(), command: app.container.custom_args.clone(), read_only_root: app.security.readonly_root, @@ -1060,6 +1071,7 @@ app: version: 1.0.0 container: image: registry/bitcoin-knots:1.0 + network: archy-net entrypoint: ["/usr/local/bin/bitcoind"] custom_args: ["-server=1", "-rpcbind=0.0.0.0"] ports: @@ -1080,7 +1092,7 @@ app: security: capabilities: ["NET_BIND_SERVICE"] readonly_root: true - network_policy: archy-net + network_policy: isolated "#; let m = AppManifest::parse(yaml).expect("manifest must parse"); let u = QuadletUnit::from_manifest(&m, "bitcoin-knots"); @@ -1220,7 +1232,7 @@ app: image: x:latest volumes: - type: bind - source: /etc/host-conf + source: /var/lib/archipelago/x-conf target: /etc/conf options: ["ro"] "#; @@ -1244,7 +1256,7 @@ app: target: /tmp tmpfs_options: "rw,size=64m" - type: bind - source: /var/lib/x + source: /var/lib/archipelago/x target: /data options: [] "#; @@ -1252,7 +1264,7 @@ app: let u = QuadletUnit::from_manifest(&m, "x"); // tmpfs entry is dropped from bind_mounts; bind entry survives. assert_eq!(u.bind_mounts.len(), 1); - assert_eq!(u.bind_mounts[0].host, PathBuf::from("/var/lib/x")); + assert_eq!(u.bind_mounts[0].host, PathBuf::from("/var/lib/archipelago/x")); } #[test] @@ -1431,6 +1443,31 @@ app: assert!(!publish_ports_changed(new, new)); } + #[test] + fn from_manifest_appends_manifest_network_aliases_for_bridge() { + let yaml = r#" +app: + id: indeedhub-api + name: IndeedHub API + version: 1.0.0 + container: + image: registry/indeedhub-api:1.0.0 + network: indeedhub-net + network_aliases: [api] + security: + capabilities: [] + network_policy: isolated +"#; + let m = AppManifest::parse(yaml).expect("manifest must parse"); + let u = QuadletUnit::from_manifest(&m, "indeedhub-api"); + assert!(matches!(u.network, NetworkMode::Bridge(ref n) if n == "indeedhub-net")); + // Own name first, then the baked-in short alias the frontend nginx uses. + assert_eq!(u.network_aliases, vec!["indeedhub-api", "api"]); + let s = u.render(); + assert!(s.contains("NetworkAlias=api")); + assert!(s.contains("PodmanArgs=--network-alias=api")); + } + #[test] fn network_aliases_changed_detects_service_discovery_drift() { let old = "[Container]\nNetwork=archy-net\n"; @@ -1489,6 +1526,7 @@ app: version: 1.0.0 container: image: registry/lnd:latest + network: archy-net ports: - host: 10009 container: 10009 @@ -1504,7 +1542,7 @@ app: memory_limit: 1g security: capabilities: [] - network_policy: archy-net + network_policy: isolated "#; let m = AppManifest::parse(yaml).unwrap(); let body = QuadletUnit::from_manifest(&m, "lnd").render(); diff --git a/core/container/src/manifest.rs b/core/container/src/manifest.rs index 73f76786..fa6d4a11 100644 --- a/core/container/src/manifest.rs +++ b/core/container/src/manifest.rs @@ -170,6 +170,17 @@ pub struct ContainerConfig { #[serde(default)] pub network: Option, + /// Extra DNS aliases the container answers to on its `network`, in addition + /// to its own container name (which is always added). Mirrors podman + /// `--network-alias`. Used by multi-container stacks whose images reference + /// peers by a short baked-in hostname — e.g. indeedhub's frontend nginx + /// proxies to `api:4000` / `minio:9000` / `relay:8080`, so the api/minio/relay + /// members declare `network_aliases: [api]` / `[minio]` / `[relay]` to keep + /// those short names resolvable on the dedicated `indeedhub-net`. Ignored for + /// slirp4netns/pasta (podman rejects aliases there). + #[serde(default)] + pub network_aliases: Vec, + /// Extra positional arguments appended to the container command /// after the image. Mirrors `SPEC_CUSTOM_ARGS` in /// `scripts/container-specs.sh` (bitcoin-knots prune/dbcache flags, @@ -539,6 +550,25 @@ impl AppManifest { } } + // network_aliases: each must be a non-empty DNS label (lowercase + // alphanumeric + hyphen, no leading/trailing hyphen) so it renders as a + // valid podman --network-alias / aardvark-dns name. + for (i, alias) in self.app.container.network_aliases.iter().enumerate() { + let ok = !alias.is_empty() + && alias.len() <= 63 + && alias + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') + && !alias.starts_with('-') + && !alias.ends_with('-'); + if !ok { + return Err(ManifestError::Invalid(format!( + "container.network_aliases[{i}] '{alias}' must be a non-empty DNS label \ + (lowercase a-z, 0-9, '-'; no leading/trailing '-')" + ))); + } + } + // custom_args: no empty strings (would inject literal "" into // the podman command line and confuse downstream parsing). for (i, a) in self.app.container.custom_args.iter().enumerate() { @@ -1662,6 +1692,7 @@ app: pull_policy: "if-not-present".to_string(), build: None, network: None, + network_aliases: vec![], custom_args: vec![], entrypoint: None, derived_env: vec![ @@ -1716,6 +1747,7 @@ app: pull_policy: "if-not-present".to_string(), build: None, network: None, + network_aliases: vec![], custom_args: vec![], entrypoint: None, derived_env: vec![], @@ -1758,6 +1790,7 @@ app: pull_policy: "if-not-present".to_string(), build: None, network: None, + network_aliases: vec![], custom_args: vec![], entrypoint: None, derived_env: vec![], diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index 35fcad36..1d4c06c0 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -385,11 +385,21 @@ impl PodmanClient { }, }); if let Some(network) = custom_network { + // The container always answers to its own name; manifest + // network_aliases add extra short hostnames peers may bake in + // (e.g. indeedhub's api/minio/relay). Dedup so a manifest that + // redundantly lists its own name doesn't double it. + let mut aliases = vec![name.to_string()]; + for a in &manifest.app.container.network_aliases { + if !aliases.iter().any(|x| x == a) { + aliases.push(a.clone()); + } + } body.as_object_mut() .expect("container create body is a JSON object") .insert( "networks".to_string(), - serde_json::json!({ network: { "aliases": [name] } }), + serde_json::json!({ network: { "aliases": aliases } }), ); }