Skip to content
Open
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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ let package = Package(
dependencies: [
.product(name: "Containerization", package: "containerization"),
"ContainerResource",
"ContainerSandboxService",
"ContainerSandboxServiceClient",
]
),
Expand Down
14 changes: 14 additions & 0 deletions Sources/ContainerResource/Container/ContainerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public struct ContainerConfiguration: Sendable, Codable {
public var networks: [AttachmentConfiguration] = []
/// The DNS configuration for the container.
public var dns: DNSConfiguration? = nil
/// Additional hosts entries to inject into /etc/hosts.
public var hosts: [HostEntry] = []
/// Whether to enable rosetta x86-64 translation for the container.
public var rosetta: Bool = false
/// Initial or main process of the container.
Expand Down Expand Up @@ -64,6 +66,7 @@ public struct ContainerConfiguration: Sendable, Codable {
case sysctls
case networks
case dns
case hosts
case rosetta
case initProcess
case platform
Expand Down Expand Up @@ -95,6 +98,7 @@ public struct ContainerConfiguration: Sendable, Codable {
}

dns = try container.decodeIfPresent(DNSConfiguration.self, forKey: .dns)
hosts = try container.decodeIfPresent([HostEntry].self, forKey: .hosts) ?? []
rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? false
initProcess = try container.decode(ProcessConfiguration.self, forKey: .initProcess)
platform = try container.decodeIfPresent(ContainerizationOCI.Platform.self, forKey: .platform) ?? .current
Expand Down Expand Up @@ -127,6 +131,16 @@ public struct ContainerConfiguration: Sendable, Codable {
}
}

public struct HostEntry: Sendable, Codable, Equatable {
public let ipAddress: String
public let hostnames: [String]

public init(ipAddress: String, hostnames: [String]) {
self.ipAddress = ipAddress
self.hostnames = hostnames
}
}

/// Resources like cpu, memory, and storage quota.
public struct Resources: Sendable, Codable {
/// Number of CPU cores allocated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,13 @@ public actor SandboxService {
czConfig.process.stdout = stdout
czConfig.process.stderr = stderr
czConfig.process.stdin = stdin
// NOTE: We can support a user providing new entries eventually, but for now craft
// a default /etc/hosts.
var hostsEntries = [Hosts.Entry.localHostIPV4()]
if !interfaces.isEmpty {
let primaryIfaceAddr = interfaces[0].ipv4Address
hostsEntries.append(
Hosts.Entry(
ipAddress: primaryIfaceAddr.address.description,
hostnames: [czConfig.hostname ?? id],
))
}
czConfig.hosts = Hosts(entries: hostsEntries)
czConfig.hosts = Hosts(entries: Self.resolvedHosts(
hostname: czConfig.hostname ?? id,
primaryAddress: interfaces.first?.ipv4Address.address.description,
extraHosts: config.hosts
).map {
Hosts.Entry(ipAddress: $0.ipAddress, hostnames: $0.hostnames)
})
czConfig.bootLog = BootLog.file(path: bundle.bootlog, append: true)
}

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

extension SandboxService {
static func resolvedHosts(
hostname: String,
primaryAddress: String?,
extraHosts: [ContainerConfiguration.HostEntry]
) -> [ContainerConfiguration.HostEntry] {
var hosts = [ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"])]

if let primaryAddress {
let ip = String(primaryAddress.split(separator: "/")[0])
hosts.append(ContainerConfiguration.HostEntry(ipAddress: ip, hostnames: [hostname]))
}

hosts.append(contentsOf: extraHosts)
return hosts
}

private func initializeWaiters(for id: String) throws {
guard waiters[id] == nil else {
throw ContainerizationError(.invalidState, message: "waiter for \(id) already initialized")
Expand Down
53 changes: 53 additions & 0 deletions Tests/ContainerResourceTests/ContainerConfigurationTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import Foundation
import Testing
import ContainerizationOCI

@testable import ContainerResource

struct ContainerConfigurationTest {
@Test
func testContainerConfigurationRoundTripsHosts() throws {
let image = ImageDescription(
reference: "docker.io/library/alpine:latest",
descriptor: Descriptor(
mediaType: "application/vnd.oci.image.manifest.v1+json",
digest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
size: 123
)
)
var configuration = ContainerConfiguration(
id: "web",
image: image,
process: ProcessConfiguration(
executable: "/bin/sh",
arguments: ["-c", "sleep infinity"],
environment: []
)
)
configuration.hosts = [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"]),
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
]

let encoded = try JSONEncoder().encode(configuration)
let decoded = try JSONDecoder().decode(ContainerConfiguration.self, from: encoded)

#expect(decoded.hosts == configuration.hosts)
}
}
43 changes: 43 additions & 0 deletions Tests/ContainerSandboxServiceTests/SandboxServiceHostsTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import Testing

@testable import ContainerResource
@testable import ContainerSandboxService

struct SandboxServiceHostsTest {
@Test
func testResolvedHostsIncludesDefaultsPrimaryAddressAndExtraHosts() {
let extraHosts = [
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
ContainerConfiguration.HostEntry(ipAddress: "10.0.0.15", hostnames: ["db", "db.internal"]),
]

let hosts = SandboxService.resolvedHosts(
hostname: "web",
primaryAddress: "192.168.64.22/24",
extraHosts: extraHosts
)

#expect(hosts == [
ContainerConfiguration.HostEntry(ipAddress: "127.0.0.1", hostnames: ["localhost"]),
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.22", hostnames: ["web"]),
ContainerConfiguration.HostEntry(ipAddress: "192.168.64.1", hostnames: ["host.docker.internal"]),
ContainerConfiguration.HostEntry(ipAddress: "10.0.0.15", hostnames: ["db", "db.internal"]),
])
}
}