Skip to content

Commit a469c52

Browse files
committed
Add explicit host entries to container configuration
1 parent 5976f38 commit a469c52

5 files changed

Lines changed: 135 additions & 12 deletions

File tree

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,9 @@ let package = Package(
390390
name: "ContainerSandboxServiceTests",
391391
dependencies: [
392392
.product(name: "Containerization", package: "containerization"),
393+
"ContainerAPIClient",
393394
"ContainerResource",
395+
"ContainerSandboxService",
394396
"ContainerSandboxServiceClient",
395397
]
396398
),

Sources/ContainerResource/Container/ContainerConfiguration.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public struct ContainerConfiguration: Sendable, Codable {
3535
public var networks: [AttachmentConfiguration] = []
3636
/// The DNS configuration for the container.
3737
public var dns: DNSConfiguration? = nil
38+
/// Additional hosts entries to inject into /etc/hosts.
39+
public var hosts: [HostEntry] = []
3840
/// Whether to enable rosetta x86-64 translation for the container.
3941
public var rosetta: Bool = false
4042
/// Initial or main process of the container.
@@ -64,6 +66,7 @@ public struct ContainerConfiguration: Sendable, Codable {
6466
case sysctls
6567
case networks
6668
case dns
69+
case hosts
6770
case rosetta
6871
case initProcess
6972
case platform
@@ -95,6 +98,7 @@ public struct ContainerConfiguration: Sendable, Codable {
9598
}
9699

97100
dns = try container.decodeIfPresent(DNSConfiguration.self, forKey: .dns)
101+
hosts = try container.decodeIfPresent([HostEntry].self, forKey: .hosts) ?? []
98102
rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? false
99103
initProcess = try container.decode(ProcessConfiguration.self, forKey: .initProcess)
100104
platform = try container.decodeIfPresent(ContainerizationOCI.Platform.self, forKey: .platform) ?? .current
@@ -127,6 +131,16 @@ public struct ContainerConfiguration: Sendable, Codable {
127131
}
128132
}
129133

134+
public struct HostEntry: Sendable, Codable, Equatable {
135+
public let ipAddress: String
136+
public let hostnames: [String]
137+
138+
public init(ipAddress: String, hostnames: [String]) {
139+
self.ipAddress = ipAddress
140+
self.hostnames = hostnames
141+
}
142+
}
143+
130144
/// Resources like cpu, memory, and storage quota.
131145
public struct Resources: Sendable, Codable {
132146
/// Number of CPU cores allocated.

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -220,18 +220,13 @@ public actor SandboxService {
220220
czConfig.process.stdout = stdout
221221
czConfig.process.stderr = stderr
222222
czConfig.process.stdin = stdin
223-
// NOTE: We can support a user providing new entries eventually, but for now craft
224-
// a default /etc/hosts.
225-
var hostsEntries = [Hosts.Entry.localHostIPV4()]
226-
if !interfaces.isEmpty {
227-
let primaryIfaceAddr = interfaces[0].ipv4Address
228-
hostsEntries.append(
229-
Hosts.Entry(
230-
ipAddress: primaryIfaceAddr.address.description,
231-
hostnames: [czConfig.hostname ?? id],
232-
))
233-
}
234-
czConfig.hosts = Hosts(entries: hostsEntries)
223+
czConfig.hosts = Hosts(entries: Self.resolvedHosts(
224+
hostname: czConfig.hostname ?? id,
225+
primaryAddress: interfaces.first?.ipv4Address.address.description,
226+
extraHosts: config.hosts
227+
).map {
228+
Hosts.Entry(ipAddress: $0.ipAddress, hostnames: $0.hostnames)
229+
})
235230
czConfig.bootLog = BootLog.file(path: bundle.bootlog, append: true)
236231
}
237232

@@ -1292,6 +1287,22 @@ extension FileHandle: @retroactive ReaderStream, @retroactive Writer {
12921287
// MARK: State handler and bundle creation helpers
12931288

12941289
extension SandboxService {
1290+
static func resolvedHosts(
1291+
hostname: String,
1292+
primaryAddress: String?,
1293+
extraHosts: [ContainerConfiguration.HostEntry]
1294+
) -> [ContainerConfiguration.HostEntry] {
1295+
var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]
1296+
1297+
if let primaryAddress {
1298+
let ip = String(primaryAddress.split(separator: "/")[0])
1299+
hosts.append(ContainerConfiguration.HostEntry(ipAddress: ip, hostnames: [hostname]))
1300+
}
1301+
1302+
hosts.append(contentsOf: extraHosts)
1303+
return hosts
1304+
}
1305+
12951306
private func initializeWaiters(for id: String) throws {
12961307
guard waiters[id] == nil else {
12971308
throw ContainerizationError(.invalidState, message: "waiter for \(id) already initialized")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
import ContainerizationOCI
20+
21+
@testable import ContainerClient
22+
23+
struct ContainerConfigurationTest {
24+
@Test
25+
func testContainerConfigurationRoundTripsHosts() throws {
26+
let image = ImageDescription(
27+
reference: "docker.io/library/alpine:latest",
28+
descriptor: Descriptor(
29+
mediaType: "application/vnd.oci.image.manifest.v1+json",
30+
digest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
31+
size: 123
32+
)
33+
)
34+
var configuration = ContainerConfiguration(
35+
id: "web",
36+
image: image,
37+
process: ProcessConfiguration(
38+
executable: "/bin/sh",
39+
arguments: ["-c", "sleep infinity"],
40+
environment: []
41+
)
42+
)
43+
configuration.hosts = [
44+
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"]),
45+
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
46+
]
47+
48+
let encoded = try JSONEncoder().encode(configuration)
49+
let decoded = try JSONDecoder().decode(ContainerConfiguration.self, from: encoded)
50+
51+
#expect(decoded.hosts == configuration.hosts)
52+
}
53+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Testing
18+
19+
@testable import ContainerClient
20+
@testable import ContainerSandboxService
21+
22+
struct SandboxServiceHostsTest {
23+
@Test
24+
func testResolvedHostsIncludesDefaultsPrimaryAddressAndExtraHosts() {
25+
let extraHosts = [
26+
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
27+
ContainerConfiguration.HostEntry(ipAddress: "10.0.0.15", hostnames: ["db", "db.internal"]),
28+
]
29+
30+
let hosts = SandboxService.resolvedHosts(
31+
hostname: "web",
32+
primaryAddress: "192.168.64.22/24",
33+
extraHosts: extraHosts
34+
)
35+
36+
#expect(hosts == [
37+
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"]),
38+
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.22", hostnames: ["web"]),
39+
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
40+
ContainerConfiguration.HostEntry(ipAddress: "10.0.0.15", hostnames: ["db", "db.internal"]),
41+
])
42+
}
43+
}

0 commit comments

Comments
 (0)