feat(manifest): network_aliases — extra DNS aliases on a container's network
Add `container.network_aliases: Vec<String>` (serde default, DNS-label validated) so a stack member can answer to short hostnames its peers bake in, beyond its own container name. Rendered in both runtime paths: - podman_client: merged (deduped) into the custom-network aliases array. - quadlet from_manifest: appended after the container name; emitted only for Bridge networks (slirp/pasta reject aliases). Needed for the indeedhub migration: its frontend nginx proxies to `api:4000` / `minio:9000` / `relay:8080`, so those members declare `network_aliases: [api|minio|relay]` to keep the short names resolvable on the dedicated indeedhub-net (vs. colliding generic aliases on archy-net). Also fixes 4 pre-existing from_manifest test failures (unrelated to this change, surfaced now that the quadlet suite runs green): test manifests used the long-invalid `network_policy: archy-net` (allowlist is isolated/bridge/host → moved to network_policy: isolated + container.network) and bind sources outside /var/lib/archipelago. Tests: container crate 53 pass; archipelago quadlet+alias 47 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ccb5b7ca39
commit
b94b61f640
@ -410,7 +410,18 @@ impl QuadletUnit {
|
|||||||
environment: app.environment.clone(),
|
environment: app.environment.clone(),
|
||||||
devices: app.devices.clone(),
|
devices: app.devices.clone(),
|
||||||
add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())],
|
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(),
|
entrypoint: app.container.entrypoint.clone(),
|
||||||
command: app.container.custom_args.clone(),
|
command: app.container.custom_args.clone(),
|
||||||
read_only_root: app.security.readonly_root,
|
read_only_root: app.security.readonly_root,
|
||||||
@ -1060,6 +1071,7 @@ app:
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
container:
|
container:
|
||||||
image: registry/bitcoin-knots:1.0
|
image: registry/bitcoin-knots:1.0
|
||||||
|
network: archy-net
|
||||||
entrypoint: ["/usr/local/bin/bitcoind"]
|
entrypoint: ["/usr/local/bin/bitcoind"]
|
||||||
custom_args: ["-server=1", "-rpcbind=0.0.0.0"]
|
custom_args: ["-server=1", "-rpcbind=0.0.0.0"]
|
||||||
ports:
|
ports:
|
||||||
@ -1080,7 +1092,7 @@ app:
|
|||||||
security:
|
security:
|
||||||
capabilities: ["NET_BIND_SERVICE"]
|
capabilities: ["NET_BIND_SERVICE"]
|
||||||
readonly_root: true
|
readonly_root: true
|
||||||
network_policy: archy-net
|
network_policy: isolated
|
||||||
"#;
|
"#;
|
||||||
let m = AppManifest::parse(yaml).expect("manifest must parse");
|
let m = AppManifest::parse(yaml).expect("manifest must parse");
|
||||||
let u = QuadletUnit::from_manifest(&m, "bitcoin-knots");
|
let u = QuadletUnit::from_manifest(&m, "bitcoin-knots");
|
||||||
@ -1220,7 +1232,7 @@ app:
|
|||||||
image: x:latest
|
image: x:latest
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /etc/host-conf
|
source: /var/lib/archipelago/x-conf
|
||||||
target: /etc/conf
|
target: /etc/conf
|
||||||
options: ["ro"]
|
options: ["ro"]
|
||||||
"#;
|
"#;
|
||||||
@ -1244,7 +1256,7 @@ app:
|
|||||||
target: /tmp
|
target: /tmp
|
||||||
tmpfs_options: "rw,size=64m"
|
tmpfs_options: "rw,size=64m"
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /var/lib/x
|
source: /var/lib/archipelago/x
|
||||||
target: /data
|
target: /data
|
||||||
options: []
|
options: []
|
||||||
"#;
|
"#;
|
||||||
@ -1252,7 +1264,7 @@ app:
|
|||||||
let u = QuadletUnit::from_manifest(&m, "x");
|
let u = QuadletUnit::from_manifest(&m, "x");
|
||||||
// tmpfs entry is dropped from bind_mounts; bind entry survives.
|
// tmpfs entry is dropped from bind_mounts; bind entry survives.
|
||||||
assert_eq!(u.bind_mounts.len(), 1);
|
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]
|
#[test]
|
||||||
@ -1431,6 +1443,31 @@ app:
|
|||||||
assert!(!publish_ports_changed(new, new));
|
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]
|
#[test]
|
||||||
fn network_aliases_changed_detects_service_discovery_drift() {
|
fn network_aliases_changed_detects_service_discovery_drift() {
|
||||||
let old = "[Container]\nNetwork=archy-net\n";
|
let old = "[Container]\nNetwork=archy-net\n";
|
||||||
@ -1489,6 +1526,7 @@ app:
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
container:
|
container:
|
||||||
image: registry/lnd:latest
|
image: registry/lnd:latest
|
||||||
|
network: archy-net
|
||||||
ports:
|
ports:
|
||||||
- host: 10009
|
- host: 10009
|
||||||
container: 10009
|
container: 10009
|
||||||
@ -1504,7 +1542,7 @@ app:
|
|||||||
memory_limit: 1g
|
memory_limit: 1g
|
||||||
security:
|
security:
|
||||||
capabilities: []
|
capabilities: []
|
||||||
network_policy: archy-net
|
network_policy: isolated
|
||||||
"#;
|
"#;
|
||||||
let m = AppManifest::parse(yaml).unwrap();
|
let m = AppManifest::parse(yaml).unwrap();
|
||||||
let body = QuadletUnit::from_manifest(&m, "lnd").render();
|
let body = QuadletUnit::from_manifest(&m, "lnd").render();
|
||||||
|
|||||||
@ -170,6 +170,17 @@ pub struct ContainerConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub network: Option<String>,
|
pub network: Option<String>,
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
|
||||||
/// Extra positional arguments appended to the container command
|
/// Extra positional arguments appended to the container command
|
||||||
/// after the image. Mirrors `SPEC_CUSTOM_ARGS` in
|
/// after the image. Mirrors `SPEC_CUSTOM_ARGS` in
|
||||||
/// `scripts/container-specs.sh` (bitcoin-knots prune/dbcache flags,
|
/// `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
|
// custom_args: no empty strings (would inject literal "" into
|
||||||
// the podman command line and confuse downstream parsing).
|
// the podman command line and confuse downstream parsing).
|
||||||
for (i, a) in self.app.container.custom_args.iter().enumerate() {
|
for (i, a) in self.app.container.custom_args.iter().enumerate() {
|
||||||
@ -1662,6 +1692,7 @@ app:
|
|||||||
pull_policy: "if-not-present".to_string(),
|
pull_policy: "if-not-present".to_string(),
|
||||||
build: None,
|
build: None,
|
||||||
network: None,
|
network: None,
|
||||||
|
network_aliases: vec![],
|
||||||
custom_args: vec![],
|
custom_args: vec![],
|
||||||
entrypoint: None,
|
entrypoint: None,
|
||||||
derived_env: vec![
|
derived_env: vec![
|
||||||
@ -1716,6 +1747,7 @@ app:
|
|||||||
pull_policy: "if-not-present".to_string(),
|
pull_policy: "if-not-present".to_string(),
|
||||||
build: None,
|
build: None,
|
||||||
network: None,
|
network: None,
|
||||||
|
network_aliases: vec![],
|
||||||
custom_args: vec![],
|
custom_args: vec![],
|
||||||
entrypoint: None,
|
entrypoint: None,
|
||||||
derived_env: vec![],
|
derived_env: vec![],
|
||||||
@ -1758,6 +1790,7 @@ app:
|
|||||||
pull_policy: "if-not-present".to_string(),
|
pull_policy: "if-not-present".to_string(),
|
||||||
build: None,
|
build: None,
|
||||||
network: None,
|
network: None,
|
||||||
|
network_aliases: vec![],
|
||||||
custom_args: vec![],
|
custom_args: vec![],
|
||||||
entrypoint: None,
|
entrypoint: None,
|
||||||
derived_env: vec![],
|
derived_env: vec![],
|
||||||
|
|||||||
@ -385,11 +385,21 @@ impl PodmanClient {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if let Some(network) = custom_network {
|
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()
|
body.as_object_mut()
|
||||||
.expect("container create body is a JSON object")
|
.expect("container create body is a JSON object")
|
||||||
.insert(
|
.insert(
|
||||||
"networks".to_string(),
|
"networks".to_string(),
|
||||||
serde_json::json!({ network: { "aliases": [name] } }),
|
serde_json::json!({ network: { "aliases": aliases } }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user