From 208d9716a393a530cb3ec1d167922263107fc40f Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Wed, 28 Jan 2026 14:06:00 -0800 Subject: [PATCH] LinuxPod: Add back top-level dns/hosts 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. --- Sources/Containerization/LinuxPod.swift | 13 +- Sources/Integration/PodTests.swift | 258 ++++++++++++++++++++++++ Sources/Integration/Suite.swift | 4 + 3 files changed, 272 insertions(+), 3 deletions(-) diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 101a7bba..cfd3ef7f 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -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) diff --git a/Sources/Integration/PodTests.swift b/Sources/Integration/PodTests.swift index 155a8f42..db7d367d 100644 --- a/Sources/Integration/PodTests.swift +++ b/Sources/Integration/PodTests.swift @@ -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)") + } + } } diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index d9f5a79b..d252c3b3 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -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]