mirror of
https://github.com/apple/container.git
synced 2026-09-14 11:45:39 +00:00
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:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user