Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions Sources/Containerization/LinuxPod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
Expand Down Expand Up @@ -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)
Expand Down
258 changes: 258 additions & 0 deletions Sources/Integration/PodTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 4 additions & 0 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading