Skip to content

Commit a8758a6

Browse files
authored
LinuxPod: Add back top-level dns/hosts (#501)
Upon further thought, we should probably support this at the pod level and allow per container overrides. I've changed my mind on how tedious it is to specify it on a per-container basis.
1 parent c8ddc8a commit a8758a6

3 files changed

Lines changed: 272 additions & 3 deletions

File tree

Sources/Containerization/LinuxPod.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ public final class LinuxPod: Sendable {
5151
/// Whether containers in the pod should share a PID namespace.
5252
/// When enabled, all containers can see each other's processes.
5353
public var shareProcessNamespace: Bool = false
54+
/// The default DNS configuration for all containers in the pod.
55+
/// Individual containers can override this by setting their own `dns` configuration.
56+
public var dns: DNS?
57+
/// The default hosts file configuration for all containers in the pod.
58+
/// Individual containers can override this by setting their own `hosts` configuration.
59+
public var hosts: Hosts?
5460

5561
public init() {}
5662
}
@@ -435,15 +441,16 @@ extension LinuxPod {
435441
}
436442
}
437443

438-
// Setup /etc/resolv.conf and /etc/hosts for each container
444+
// Setup /etc/resolv.conf and /etc/hosts for each container.
445+
// Container-level config takes precedence over pod-level config.
439446
for (_, container) in containers {
440-
if let dns = container.config.dns {
447+
if let dns = container.config.dns ?? self.config.dns {
441448
try await agent.configureDNS(
442449
config: dns,
443450
location: Self.guestRootfsPath(container.id)
444451
)
445452
}
446-
if let hosts = container.config.hosts {
453+
if let hosts = container.config.hosts ?? self.config.hosts {
447454
try await agent.configureHosts(
448455
config: hosts,
449456
location: Self.guestRootfsPath(container.id)

Sources/Integration/PodTests.swift

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,4 +1089,262 @@ extension IntegrationSuite {
10891089
throw IntegrationError.assert(msg: "container2 should NOT have service-a entry, got: \(output2)")
10901090
}
10911091
}
1092+
1093+
func testPodLevelDNS() async throws {
1094+
let id = "test-pod-level-dns"
1095+
1096+
let bs = try await bootstrap(id)
1097+
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
1098+
config.cpus = 4
1099+
config.memoryInBytes = 1024.mib()
1100+
config.bootLog = bs.bootLog
1101+
// Set DNS at the pod level
1102+
config.dns = DNS(nameservers: ["9.9.9.9", "149.112.112.112"])
1103+
}
1104+
1105+
let buffer1 = BufferWriter()
1106+
let buffer2 = BufferWriter()
1107+
1108+
// Neither container specifies DNS. We should inherit from pod
1109+
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
1110+
config.process.arguments = ["cat", "/etc/resolv.conf"]
1111+
config.process.stdout = buffer1
1112+
}
1113+
1114+
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
1115+
config.process.arguments = ["cat", "/etc/resolv.conf"]
1116+
config.process.stdout = buffer2
1117+
}
1118+
1119+
try await pod.create()
1120+
1121+
try await pod.startContainer("container1")
1122+
let status1 = try await pod.waitContainer("container1")
1123+
1124+
try await pod.startContainer("container2")
1125+
let status2 = try await pod.waitContainer("container2")
1126+
1127+
try await pod.stop()
1128+
1129+
guard status1.exitCode == 0 else {
1130+
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
1131+
}
1132+
guard status2.exitCode == 0 else {
1133+
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
1134+
}
1135+
1136+
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
1137+
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
1138+
}
1139+
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
1140+
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
1141+
}
1142+
1143+
// Both containers should have the pod-level DNS
1144+
guard output1.contains("9.9.9.9") && output1.contains("149.112.112.112") else {
1145+
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
1146+
}
1147+
guard output2.contains("9.9.9.9") && output2.contains("149.112.112.112") else {
1148+
throw IntegrationError.assert(msg: "container2 should have pod-level DNS (9.9.9.9), got: \(output2)")
1149+
}
1150+
}
1151+
1152+
func testPodLevelDNSWithContainerOverride() async throws {
1153+
let id = "test-pod-level-dns-override"
1154+
1155+
let bs = try await bootstrap(id)
1156+
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
1157+
config.cpus = 4
1158+
config.memoryInBytes = 1024.mib()
1159+
config.bootLog = bs.bootLog
1160+
// Set DNS at the pod level
1161+
config.dns = DNS(nameservers: ["9.9.9.9"])
1162+
}
1163+
1164+
let buffer1 = BufferWriter()
1165+
let buffer2 = BufferWriter()
1166+
1167+
// Container1 does NOT specify DNS. It should inherit from pod
1168+
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
1169+
config.process.arguments = ["cat", "/etc/resolv.conf"]
1170+
config.process.stdout = buffer1
1171+
}
1172+
1173+
// Container2 specifies its own DNS. It should override pod-level
1174+
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
1175+
config.process.arguments = ["cat", "/etc/resolv.conf"]
1176+
config.process.stdout = buffer2
1177+
config.dns = DNS(nameservers: ["8.8.8.8"])
1178+
}
1179+
1180+
try await pod.create()
1181+
1182+
try await pod.startContainer("container1")
1183+
let status1 = try await pod.waitContainer("container1")
1184+
1185+
try await pod.startContainer("container2")
1186+
let status2 = try await pod.waitContainer("container2")
1187+
1188+
try await pod.stop()
1189+
1190+
guard status1.exitCode == 0 else {
1191+
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
1192+
}
1193+
guard status2.exitCode == 0 else {
1194+
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
1195+
}
1196+
1197+
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
1198+
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
1199+
}
1200+
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
1201+
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
1202+
}
1203+
1204+
// Container1 should have pod-level DNS
1205+
guard output1.contains("9.9.9.9") && !output1.contains("8.8.8.8") else {
1206+
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
1207+
}
1208+
// Container2 should have its own DNS, not pod-level
1209+
guard output2.contains("8.8.8.8") && !output2.contains("9.9.9.9") else {
1210+
throw IntegrationError.assert(msg: "container2 should have container-level DNS (8.8.8.8), got: \(output2)")
1211+
}
1212+
}
1213+
1214+
func testPodLevelHosts() async throws {
1215+
let id = "test-pod-level-hosts"
1216+
1217+
let bs = try await bootstrap(id)
1218+
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
1219+
config.cpus = 4
1220+
config.memoryInBytes = 1024.mib()
1221+
config.bootLog = bs.bootLog
1222+
// Set hosts at the pod level
1223+
config.hosts = Hosts(entries: [
1224+
Hosts.Entry.localHostIPV4(),
1225+
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
1226+
])
1227+
}
1228+
1229+
let buffer1 = BufferWriter()
1230+
let buffer2 = BufferWriter()
1231+
1232+
// Neither container specifies hosts. It should inherit from pod
1233+
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
1234+
config.process.arguments = ["cat", "/etc/hosts"]
1235+
config.process.stdout = buffer1
1236+
}
1237+
1238+
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
1239+
config.process.arguments = ["cat", "/etc/hosts"]
1240+
config.process.stdout = buffer2
1241+
}
1242+
1243+
try await pod.create()
1244+
1245+
try await pod.startContainer("container1")
1246+
let status1 = try await pod.waitContainer("container1")
1247+
1248+
try await pod.startContainer("container2")
1249+
let status2 = try await pod.waitContainer("container2")
1250+
1251+
try await pod.stop()
1252+
1253+
guard status1.exitCode == 0 else {
1254+
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
1255+
}
1256+
guard status2.exitCode == 0 else {
1257+
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
1258+
}
1259+
1260+
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
1261+
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
1262+
}
1263+
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
1264+
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
1265+
}
1266+
1267+
// Both containers should have the pod-level hosts entry
1268+
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
1269+
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
1270+
}
1271+
guard output2.contains("10.0.0.100") && output2.contains("shared-service.local") else {
1272+
throw IntegrationError.assert(msg: "container2 should have pod-level hosts entry, got: \(output2)")
1273+
}
1274+
}
1275+
1276+
func testPodLevelHostsWithContainerOverride() async throws {
1277+
let id = "test-pod-level-hosts-override"
1278+
1279+
let bs = try await bootstrap(id)
1280+
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
1281+
config.cpus = 4
1282+
config.memoryInBytes = 1024.mib()
1283+
config.bootLog = bs.bootLog
1284+
// Set hosts at the pod level
1285+
config.hosts = Hosts(entries: [
1286+
Hosts.Entry.localHostIPV4(),
1287+
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
1288+
])
1289+
}
1290+
1291+
let buffer1 = BufferWriter()
1292+
let buffer2 = BufferWriter()
1293+
1294+
// Container1 does NOT specify hosts. It should inherit from pod
1295+
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
1296+
config.process.arguments = ["cat", "/etc/hosts"]
1297+
config.process.stdout = buffer1
1298+
}
1299+
1300+
// Container2 specifies its own hosts. It should override pod-level
1301+
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
1302+
config.process.arguments = ["cat", "/etc/hosts"]
1303+
config.process.stdout = buffer2
1304+
config.hosts = Hosts(entries: [
1305+
Hosts.Entry.localHostIPV4(),
1306+
Hosts.Entry(ipAddress: "10.0.0.200", hostnames: ["container-specific.local"]),
1307+
])
1308+
}
1309+
1310+
try await pod.create()
1311+
1312+
try await pod.startContainer("container1")
1313+
let status1 = try await pod.waitContainer("container1")
1314+
1315+
try await pod.startContainer("container2")
1316+
let status2 = try await pod.waitContainer("container2")
1317+
1318+
try await pod.stop()
1319+
1320+
guard status1.exitCode == 0 else {
1321+
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
1322+
}
1323+
guard status2.exitCode == 0 else {
1324+
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
1325+
}
1326+
1327+
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
1328+
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
1329+
}
1330+
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
1331+
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
1332+
}
1333+
1334+
// Container1 should have pod-level hosts entry
1335+
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
1336+
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
1337+
}
1338+
guard !output1.contains("10.0.0.200") && !output1.contains("container-specific.local") else {
1339+
throw IntegrationError.assert(msg: "container1 should NOT have container2's hosts entry, got: \(output1)")
1340+
}
1341+
1342+
// Container2 should have its own hosts entry, not pod-level
1343+
guard output2.contains("10.0.0.200") && output2.contains("container-specific.local") else {
1344+
throw IntegrationError.assert(msg: "container2 should have container-level hosts entry, got: \(output2)")
1345+
}
1346+
guard !output2.contains("10.0.0.100") && !output2.contains("shared-service.local") else {
1347+
throw IntegrationError.assert(msg: "container2 should NOT have pod-level hosts entry, got: \(output2)")
1348+
}
1349+
}
10921350
}

Sources/Integration/Suite.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ struct IntegrationSuite: AsyncParsableCommand {
358358
Test("pod container hosts config", testPodContainerHostsConfig),
359359
Test("pod multiple containers different DNS", testPodMultipleContainersDifferentDNS),
360360
Test("pod multiple containers different hosts", testPodMultipleContainersDifferentHosts),
361+
Test("pod level DNS", testPodLevelDNS),
362+
Test("pod level DNS with container override", testPodLevelDNSWithContainerOverride),
363+
Test("pod level hosts", testPodLevelHosts),
364+
Test("pod level hosts with container override", testPodLevelHostsWithContainerOverride),
361365
] + macOS26Tests()
362366

363367
let filteredTests: [Test]

0 commit comments

Comments
 (0)