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.
This commit is contained in:
Danny Canter authored and GitHub committed 2026-01-28 14:23:19 -08:00
1 parent c8ddc8a826
commit a8758a6ab3
3 files changed
+272 -3

No files matched your search

+10 -3
View File
@@ -51,6 +51,12 @@ public final class LinuxPod: Sendable {
/// Whether containers in the pod should share a PID namespace.
/// When enabled, all containers can see each other's processes.
public var shareProcessNamespace: Bool = false
/// The default DNS configuration for all containers in the pod.
/// Individual containers can override this by setting their own `dns` configuration.
public var dns: DNS?
/// The default hosts file configuration for all containers in the pod.
/// Individual containers can override this by setting their own `hosts` configuration.
public var hosts: Hosts?
public init() {}
}
@@ -435,15 +441,16 @@ extension LinuxPod {
}
}
// Setup /etc/resolv.conf and /etc/hosts for each container
// Setup /etc/resolv.conf and /etc/hosts for each container.
// Container-level config takes precedence over pod-level config.
for (_, container) in containers {
if let dns = container.config.dns {
if let dns = container.config.dns ?? self.config.dns {
try await agent.configureDNS(
config: dns,
location: Self.guestRootfsPath(container.id)
)
}
if let hosts = container.config.hosts {
if let hosts = container.config.hosts ?? self.config.hosts {
try await agent.configureHosts(
config: hosts,
location: Self.guestRootfsPath(container.id)
+258
View File
@@ -1089,4 +1089,262 @@ extension IntegrationSuite {
throw IntegrationError.assert(msg: "container2 should NOT have service-a entry, got: \(output2)")
}
}
func testPodLevelDNS() async throws {
let id = "test-pod-level-dns"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set DNS at the pod level
config.dns = DNS(nameservers: ["9.9.9.9", "149.112.112.112"])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Neither container specifies DNS. We should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer1
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer2
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Both containers should have the pod-level DNS
guard output1.contains("9.9.9.9") && output1.contains("149.112.112.112") else {
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
}
guard output2.contains("9.9.9.9") && output2.contains("149.112.112.112") else {
throw IntegrationError.assert(msg: "container2 should have pod-level DNS (9.9.9.9), got: \(output2)")
}
}
func testPodLevelDNSWithContainerOverride() async throws {
let id = "test-pod-level-dns-override"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set DNS at the pod level
config.dns = DNS(nameservers: ["9.9.9.9"])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 does NOT specify DNS. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer1
}
// Container2 specifies its own DNS. It should override pod-level
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer2
config.dns = DNS(nameservers: ["8.8.8.8"])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Container1 should have pod-level DNS
guard output1.contains("9.9.9.9") && !output1.contains("8.8.8.8") else {
throw IntegrationError.assert(msg: "container1 should have pod-level DNS (9.9.9.9), got: \(output1)")
}
// Container2 should have its own DNS, not pod-level
guard output2.contains("8.8.8.8") && !output2.contains("9.9.9.9") else {
throw IntegrationError.assert(msg: "container2 should have container-level DNS (8.8.8.8), got: \(output2)")
}
}
func testPodLevelHosts() async throws {
let id = "test-pod-level-hosts"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hosts at the pod level
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Neither container specifies hosts. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer1
}
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer2
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Both containers should have the pod-level hosts entry
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
}
guard output2.contains("10.0.0.100") && output2.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container2 should have pod-level hosts entry, got: \(output2)")
}
}
func testPodLevelHostsWithContainerOverride() async throws {
let id = "test-pod-level-hosts-override"
let bs = try await bootstrap(id)
let pod = try LinuxPod(id, vmm: bs.vmm) { config in
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
// Set hosts at the pod level
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.100", hostnames: ["shared-service.local"]),
])
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
// Container1 does NOT specify hosts. It should inherit from pod
try await pod.addContainer("container1", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container1")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer1
}
// Container2 specifies its own hosts. It should override pod-level
try await pod.addContainer("container2", rootfs: try cloneRootfs(bs.rootfs, testID: id, containerID: "container2")) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer2
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.200", hostnames: ["container-specific.local"]),
])
}
try await pod.create()
try await pod.startContainer("container1")
let status1 = try await pod.waitContainer("container1")
try await pod.startContainer("container2")
let status2 = try await pod.waitContainer("container2")
try await pod.stop()
guard status1.exitCode == 0 else {
throw IntegrationError.assert(msg: "container1 cat failed with status \(status1)")
}
guard status2.exitCode == 0 else {
throw IntegrationError.assert(msg: "container2 cat failed with status \(status2)")
}
guard let output1 = String(data: buffer1.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container1 stdout to UTF8")
}
guard let output2 = String(data: buffer2.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert container2 stdout to UTF8")
}
// Container1 should have pod-level hosts entry
guard output1.contains("10.0.0.100") && output1.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container1 should have pod-level hosts entry, got: \(output1)")
}
guard !output1.contains("10.0.0.200") && !output1.contains("container-specific.local") else {
throw IntegrationError.assert(msg: "container1 should NOT have container2's hosts entry, got: \(output1)")
}
// Container2 should have its own hosts entry, not pod-level
guard output2.contains("10.0.0.200") && output2.contains("container-specific.local") else {
throw IntegrationError.assert(msg: "container2 should have container-level hosts entry, got: \(output2)")
}
guard !output2.contains("10.0.0.100") && !output2.contains("shared-service.local") else {
throw IntegrationError.assert(msg: "container2 should NOT have pod-level hosts entry, got: \(output2)")
}
}
}
+4
View File
@@ -358,6 +358,10 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("pod container hosts config", testPodContainerHostsConfig),
Test("pod multiple containers different DNS", testPodMultipleContainersDifferentDNS),
Test("pod multiple containers different hosts", testPodMultipleContainersDifferentHosts),
Test("pod level DNS", testPodLevelDNS),
Test("pod level DNS with container override", testPodLevelDNSWithContainerOverride),
Test("pod level hosts", testPodLevelHosts),
Test("pod level hosts with container override", testPodLevelHostsWithContainerOverride),
] + macOS26Tests()
let filteredTests: [Test]