Skip to content

Commit 1503fd2

Browse files
author
Roy Lin
committed
fix(compose): resolve services by their bare name (service-to-service DNS)
Docker Compose registers each service on the project network under its bare service name, so `getent hosts db` works between services. a3s-box connected each box only as `{project}-{svc}`, so the bare name did not resolve. NetworkEndpoint gains an `aliases` field (serde-default for backward compat); peer_endpoints emits the box name plus each alias, so they all land in the peers' /etc/hosts. Compose now connects each service with its bare service name as an alias. Verified on Linux: `getent hosts db` from another service resolves (EXIT=0).
1 parent b04de9b commit 1503fd2

2 files changed

Lines changed: 71 additions & 2 deletions

File tree

src/cli/src/commands/compose.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,14 @@ async fn execute_up(
388388
)
389389
.await;
390390
}
391-
let endpoint = match net_config.connect(&box_id, &box_name) {
391+
// Register the bare service name as a DNS alias so peers can reach
392+
// this service as `svc` (Docker Compose behavior), not only as the
393+
// `{project}-{svc}` box name.
394+
let endpoint = match net_config.connect_with_aliases(
395+
&box_id,
396+
&box_name,
397+
std::slice::from_ref(svc_name),
398+
) {
392399
Ok(endpoint) => endpoint,
393400
Err(error) => {
394401
return rollback_compose_up(

src/core/src/network.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ pub struct NetworkEndpoint {
8181
/// Box name (for DNS resolution).
8282
pub box_name: String,
8383

84+
/// Additional DNS names that also resolve to this endpoint's IP (e.g. the
85+
/// bare Compose service name alongside the `{project}-{service}` box name).
86+
#[serde(default)]
87+
pub aliases: Vec<String>,
88+
8489
/// Assigned IPv4 address.
8590
pub ip_address: Ipv4Addr,
8691

@@ -432,6 +437,17 @@ impl NetworkConfig {
432437

433438
/// Allocate an IP and register a new endpoint for a box.
434439
pub fn connect(&mut self, box_id: &str, box_name: &str) -> Result<NetworkEndpoint, String> {
440+
self.connect_with_aliases(box_id, box_name, &[])
441+
}
442+
443+
/// Connect a box, also registering extra DNS aliases that resolve to its IP
444+
/// (e.g. the bare Compose service name in addition to the box name).
445+
pub fn connect_with_aliases(
446+
&mut self,
447+
box_id: &str,
448+
box_name: &str,
449+
aliases: &[String],
450+
) -> Result<NetworkEndpoint, String> {
435451
if self.endpoints.contains_key(box_id) {
436452
return Err(format!(
437453
"box '{}' is already connected to network '{}'",
@@ -447,6 +463,11 @@ impl NetworkConfig {
447463
let endpoint = NetworkEndpoint {
448464
box_id: box_id.to_string(),
449465
box_name: box_name.to_string(),
466+
aliases: aliases
467+
.iter()
468+
.filter(|a| !a.is_empty() && *a != box_name)
469+
.cloned()
470+
.collect(),
450471
ip_address: ip,
451472
mac_address: mac,
452473
};
@@ -487,7 +508,13 @@ impl NetworkConfig {
487508
self.endpoints
488509
.values()
489510
.filter(|ep| ep.box_id != exclude_box_id)
490-
.map(|ep| (ep.ip_address.to_string(), ep.box_name.clone()))
511+
.flat_map(|ep| {
512+
let ip = ep.ip_address.to_string();
513+
// The box name plus any aliases (e.g. the bare service name) all
514+
// resolve to this peer's IP.
515+
std::iter::once((ip.clone(), ep.box_name.clone()))
516+
.chain(ep.aliases.iter().map(move |a| (ip.clone(), a.clone())))
517+
})
491518
.collect()
492519
}
493520

@@ -783,6 +810,35 @@ mod tests {
783810
assert!(peers.iter().any(|(_, name)| name == "db"));
784811
}
785812

813+
#[test]
814+
fn test_connect_with_aliases_resolvable_as_peer() {
815+
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
816+
// Compose-style: box name is "proj-db", bare service name "db" is an alias.
817+
let ep = net
818+
.connect_with_aliases("box-db", "proj-db", &["db".to_string()])
819+
.unwrap();
820+
assert_eq!(ep.aliases, vec!["db".to_string()]);
821+
net.connect("box-web", "proj-web").unwrap();
822+
823+
let peers = net.peer_endpoints("box-web");
824+
let db_ip = ep.ip_address.to_string();
825+
// Both the box name AND the bare alias resolve to db's IP.
826+
assert!(peers
827+
.iter()
828+
.any(|(ip, name)| ip == &db_ip && name == "proj-db"));
829+
assert!(peers.iter().any(|(ip, name)| ip == &db_ip && name == "db"));
830+
}
831+
832+
#[test]
833+
fn test_connect_alias_skips_empty_and_self_duplicate() {
834+
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
835+
// An alias equal to the box name or empty is dropped.
836+
let ep = net
837+
.connect_with_aliases("b", "web", &["".to_string(), "web".to_string()])
838+
.unwrap();
839+
assert!(ep.aliases.is_empty());
840+
}
841+
786842
#[test]
787843
fn test_peer_endpoints_empty_when_alone() {
788844
let mut net = NetworkConfig::new("mynet", "10.88.0.0/24").unwrap();
@@ -821,13 +877,19 @@ mod tests {
821877
let ep = NetworkEndpoint {
822878
box_id: "abc123".to_string(),
823879
box_name: "web".to_string(),
880+
aliases: vec!["app".to_string()],
824881
ip_address: Ipv4Addr::new(10, 88, 0, 2),
825882
mac_address: "02:42:0a:58:00:02".to_string(),
826883
};
827884

828885
let json = serde_json::to_string(&ep).unwrap();
829886
let parsed: NetworkEndpoint = serde_json::from_str(&json).unwrap();
830887
assert_eq!(parsed, ep);
888+
889+
// Backward compat: older stored endpoints without `aliases` deserialize.
890+
let legacy = r#"{"box_id":"x","box_name":"web","ip_address":"10.88.0.3","mac_address":"02:42:0a:58:00:03"}"#;
891+
let parsed_legacy: NetworkEndpoint = serde_json::from_str(legacy).unwrap();
892+
assert!(parsed_legacy.aliases.is_empty());
831893
}
832894

833895
// --- NetworkPolicy tests ---

0 commit comments

Comments
 (0)