LinuxPod: Support per container dns/hosts (#496)

This shouldn't have been pod level, and we had no way to set hosts today
either. This just exposes hosts and the dns configs per container now.
This commit is contained in:
Danny Canter
2026-01-27 10:11:32 -05:00
committed by GitHub
parent fd62f311b4
commit db113ffec9
3 changed files with 184 additions and 7 deletions
+13 -6
View File
@@ -44,8 +44,6 @@ public final class LinuxPod: Sendable {
public var memoryInBytes: UInt64 = 1024.mib()
/// The network interfaces for the pod.
public var interfaces: [any Interface] = []
/// The DNS configuration for the pod.
public var dns: DNS?
/// Whether nested virtualization should be turned on for the pod.
public var virtualization: Bool = false
/// Optional file path to store serial boot logs.
@@ -73,6 +71,10 @@ public final class LinuxPod: Sendable {
public var mounts: [Mount] = LinuxContainer.defaultMounts()
/// The Unix domain socket relays to setup for the container.
public var sockets: [UnixSocketConfiguration] = []
/// The DNS configuration for the container.
public var dns: DNS?
/// The hosts file configuration for the container.
public var hosts: Hosts?
public init() {}
}
@@ -433,15 +435,20 @@ extension LinuxPod {
}
}
// Setup /etc/resolv.conf if asked for
if let dns = self.config.dns {
// Configure DNS in each container's rootfs
for (_, container) in containers {
// Setup /etc/resolv.conf and /etc/hosts for each container
for (_, container) in containers {
if let dns = container.config.dns {
try await agent.configureDNS(
config: dns,
location: Self.guestRootfsPath(container.id)
)
}
if let hosts = container.config.hosts {
try await agent.configureHosts(
config: hosts,
location: Self.guestRootfsPath(container.id)
)
}
}
}
+168 -1
View File
@@ -844,7 +844,6 @@ extension IntegrationSuite {
config.cpus = 4
config.memoryInBytes = 1024.mib()
config.bootLog = bs.bootLog
config.dns = DNS(nameservers: ["8.8.8.8", "8.8.4.4"])
}
let buffer = BufferWriter()
@@ -852,6 +851,7 @@ extension IntegrationSuite {
// Verify /etc/resolv.conf was written before rootfs was remounted read-only
config.process.arguments = ["cat", "/etc/resolv.conf"]
config.process.stdout = buffer
config.dns = DNS(nameservers: ["8.8.8.8", "8.8.4.4"])
}
try await pod.create()
@@ -922,4 +922,171 @@ extension IntegrationSuite {
throw error
}
}
func testPodContainerHostsConfig() async throws {
let id = "test-pod-container-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
}
let buffer = BufferWriter()
try await pod.addContainer("container1", rootfs: bs.rootfs) { config in
config.process.arguments = ["cat", "/etc/hosts"]
config.process.stdout = buffer
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry.localHostIPV6(),
Hosts.Entry(ipAddress: "10.0.0.50", hostnames: ["myservice.local", "myservice"]),
])
}
try await pod.create()
try await pod.startContainer("container1")
let status = try await pod.waitContainer("container1")
try await pod.stop()
guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "cat /etc/hosts failed with status \(status)")
}
guard let output = String(data: buffer.data, encoding: .utf8) else {
throw IntegrationError.assert(msg: "failed to convert stdout to UTF8")
}
guard output.contains("10.0.0.50") && output.contains("myservice.local") else {
throw IntegrationError.assert(msg: "expected /etc/hosts to contain custom entry, got: \(output)")
}
}
func testPodMultipleContainersDifferentDNS() async throws {
let id = "test-pod-multi-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
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
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
config.dns = DNS(nameservers: ["1.1.1.1"])
}
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")
}
guard output1.contains("1.1.1.1") && !output1.contains("8.8.8.8") else {
throw IntegrationError.assert(msg: "container1 should have 1.1.1.1 DNS, got: \(output1)")
}
guard output2.contains("8.8.8.8") && !output2.contains("1.1.1.1") else {
throw IntegrationError.assert(msg: "container2 should have 8.8.8.8 DNS, got: \(output2)")
}
}
func testPodMultipleContainersDifferentHosts() async throws {
let id = "test-pod-multi-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
}
let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
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
config.hosts = Hosts(entries: [
Hosts.Entry.localHostIPV4(),
Hosts.Entry(ipAddress: "10.0.0.1", hostnames: ["service-a.local", "service-a"]),
])
}
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.2", hostnames: ["service-b.local", "service-b"]),
])
}
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")
}
guard output1.contains("10.0.0.1") && output1.contains("service-a.local") else {
throw IntegrationError.assert(msg: "container1 should have service-a entry, got: \(output1)")
}
guard !output1.contains("10.0.0.2") && !output1.contains("service-b") else {
throw IntegrationError.assert(msg: "container1 should NOT have service-b entry, got: \(output1)")
}
guard output2.contains("10.0.0.2") && output2.contains("service-b.local") else {
throw IntegrationError.assert(msg: "container2 should have service-b entry, got: \(output2)")
}
guard !output2.contains("10.0.0.1") && !output2.contains("service-a") else {
throw IntegrationError.assert(msg: "container2 should NOT have service-a entry, got: \(output2)")
}
}
}
+3
View File
@@ -355,6 +355,9 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("pod read-only rootfs", testPodReadOnlyRootfs),
Test("pod read-only rootfs DNS", testPodReadOnlyRootfsDNSConfigured),
Test("pod single file mount", testPodSingleFileMount),
Test("pod container hosts config", testPodContainerHostsConfig),
Test("pod multiple containers different DNS", testPodMultipleContainersDifferentDNS),
Test("pod multiple containers different hosts", testPodMultipleContainersDifferentHosts),
] + macOS26Tests()
let filteredTests: [Test]