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
14 changes: 12 additions & 2 deletions Sources/ContainerCommands/BuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ extension Application {
progress.set(description: "Dialing builder")

let dnsNameservers = self.dns.nameservers
let dnsDomain = self.dns.domain
let dnsSearchDomains = self.dns.searchDomains
let dnsOptions = self.dns.options

// Ensure the builder is started (or restarted) with the correct SSH configuration
// before attempting to dial. This handles the case where the builder is already
Expand All @@ -181,16 +184,20 @@ extension Application {
log: log,
ssh: ssh == "default",
dnsNameservers: dnsNameservers,
dnsDomain: dnsDomain,
dnsSearchDomains: dnsSearchDomains,
dnsOptions: dnsOptions,
progressUpdate: progress.handler,
containerSystemConfig: containerSystemConfig,
)

let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, dnsNameservers, ssh] group in
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) {
[vsockPort, cpus, memory, dnsNameservers, dnsDomain, dnsSearchDomains, dnsOptions, ssh] group in
defer {
group.cancelAll()
}

group.addTask { [vsockPort, cpus, memory, log, dnsNameservers, ssh] in
group.addTask { [vsockPort, cpus, memory, log, dnsNameservers, dnsDomain, dnsSearchDomains, dnsOptions, ssh] in
let client = ContainerClient()
while true {
do {
Expand All @@ -214,6 +221,9 @@ extension Application {
log: log,
ssh: ssh == "default",
dnsNameservers: dnsNameservers,
dnsDomain: dnsDomain,
dnsSearchDomains: dnsSearchDomains,
dnsOptions: dnsOptions,
progressUpdate: progress.handler,
containerSystemConfig: containerSystemConfig,
)
Expand Down
45 changes: 26 additions & 19 deletions Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ extension Application {
progressUpdate: @escaping ProgressUpdateHandler,
containerSystemConfig: ContainerSystemConfig,
) async throws {
let dns = Utility.dnsConfiguration(
from: .init(
domain: dnsDomain,
nameservers: dnsNameservers,
options: dnsOptions,
searchDomains: dnsSearchDomains
),
defaults: containerSystemConfig.container.dns
)

await progressUpdate([
.setDescription("Fetching BuildKit image"),
.setItemsName("blobs"),
Expand Down Expand Up @@ -154,21 +164,7 @@ extension Application {
let sshForwarded = existingContainer.configuration.ssh
let sshWanted = ssh && ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil
let sshChanged = sshForwarded != sshWanted
let dnsChanged = {
if !dnsNameservers.isEmpty {
return existingDNS?.nameservers != dnsNameservers
}
if dnsDomain != nil {
return existingDNS?.domain != dnsDomain
}
if !dnsSearchDomains.isEmpty {
return existingDNS?.searchDomains != dnsSearchDomains
}
if !dnsOptions.isEmpty {
return existingDNS?.options != dnsOptions
}
return false
}()
let dnsChanged = Self.dnsConfigurationChanged(existing: existingDNS, desired: dns)

switch existingContainer.status {
case .running:
Expand Down Expand Up @@ -288,10 +284,10 @@ extension Application {
AttachmentConfiguration(network: defaultNetwork.id, options: AttachmentOptions(hostname: Builder.builderContainerId))
]
config.dns = ContainerConfiguration.DNSConfiguration(
nameservers: dnsNameservers,
domain: dnsDomain,
searchDomains: dnsSearchDomains,
options: dnsOptions
nameservers: dns.nameservers,
domain: dns.domain,
searchDomains: dns.searchDomains,
options: dns.options
)

let kernel = try await {
Expand Down Expand Up @@ -323,6 +319,17 @@ extension Application {
try await startBuildKit(client: client, id: Builder.builderContainerId, progressUpdate, taskManager)
log.debug("starting BuildKit and BuildKit-shim")
}

static func dnsConfigurationChanged(
existing: ContainerConfiguration.DNSConfiguration?,
desired: ContainerConfiguration.DNSConfiguration
) -> Bool {
let existing = existing ?? .init()
return existing.nameservers != desired.nameservers
|| existing.domain != desired.domain
|| existing.searchDomains != desired.searchDomains
|| existing.options != desired.options
}
}
}

Expand Down
36 changes: 35 additions & 1 deletion Sources/ContainerPersistence/ContainerSystemConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,23 @@ final public class ContainerConfig: Codable, Sendable {

public let cpus: Int
public let memory: MemorySize
public let dns: ContainerDNSConfig?

public init(cpus: Int = defaultCPUs, memory: MemorySize = defaultMemory) {
public init(
cpus: Int = defaultCPUs,
memory: MemorySize = defaultMemory,
dns: ContainerDNSConfig? = nil
) {
self.cpus = cpus
self.memory = memory
self.dns = dns
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.cpus = try container.decodeIfPresent(Int.self, forKey: .cpus) ?? Self.defaultCPUs
self.memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory) ?? Self.defaultMemory
self.dns = try container.decodeIfPresent(ContainerDNSConfig.self, forKey: .dns)
}
}

Expand All @@ -144,6 +151,33 @@ final public class DNSConfig: Codable, Sendable {
}
}

final public class ContainerDNSConfig: Codable, Sendable {
public let domain: String?
public let nameservers: [String]?
public let searchDomains: [String]?
public let options: [String]?

public init(
domain: String? = nil,
nameservers: [String]? = nil,
searchDomains: [String]? = nil,
options: [String]? = nil
) {
self.domain = domain
self.nameservers = nameservers
self.searchDomains = searchDomains
self.options = options
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.domain = try container.decodeIfPresent(String.self, forKey: .domain)
self.nameservers = try container.decodeIfPresent([String].self, forKey: .nameservers)
self.searchDomains = try container.decodeIfPresent([String].self, forKey: .searchDomains)
self.options = try container.decodeIfPresent([String].self, forKey: .options)
}
}

final public class VminitConfig: Codable, Sendable {
public static var defaultImage: String {
let tag = String(cString: get_swift_containerization_version())
Expand Down
43 changes: 37 additions & 6 deletions Sources/Services/ContainerAPIService/Client/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,10 @@ public struct Utility {
if management.dnsDisabled {
config.dns = nil
} else {
let domain = management.dns.domain ?? containerSystemConfig.dns.domain
config.dns = .init(
nameservers: management.dns.nameservers,
domain: domain,
searchDomains: management.dns.searchDomains,
options: management.dns.options
config.dns = dnsConfiguration(
from: management.dns,
defaults: containerSystemConfig.container.dns,
hostDomainFallback: containerSystemConfig.dns.domain
)
}

Expand Down Expand Up @@ -328,6 +326,39 @@ public struct Utility {
return [AttachmentConfiguration(network: builtinNetworkId, options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: nil, mtu: 1280))]
}

/// Resolves the DNS configuration for a container from CLI flags, falling back
/// to the defaults configured in `~/.config/container/config.toml`.
///
/// Precedence: CLI flags > `[container.dns]` defaults > host domain fallback.
/// `hostDomainFallback` is the API server's own DNS domain (the `[dns]` section),
/// used so a container's default domain still matches the host domain when no
/// `--dns-domain` flag and no `[container.dns]` domain are configured.
public static func dnsConfiguration(
from flags: Flags.DNS,
defaults: ContainerDNSConfig?,
hostDomainFallback: String? = nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This to preserve the existing behavior. hostDomainFallback uses [dns].domain only when no --dns-domain flag or [container.dns].domain is set. I added tests for the fallback and for container specific override

) -> ContainerConfiguration.DNSConfiguration {
let nameservers =
flags.nameservers.isEmpty
? (defaults?.nameservers ?? [])
: flags.nameservers
let domain = flags.domain ?? defaults?.domain ?? hostDomainFallback
let searchDomains =
flags.searchDomains.isEmpty
? (defaults?.searchDomains ?? [])
: flags.searchDomains
let options =
flags.options.isEmpty
? (defaults?.options ?? [])
: flags.options
return .init(
nameservers: nameservers,
domain: domain,
searchDomains: searchDomains,
options: options
)
}

private static func getKernel(management: Flags.Management) async throws -> Kernel {
// For the image itself we'll take the user input and try with it as we can do userspace
// emulation for x86, but for the kernel we need it to match the hosts architecture.
Expand Down
101 changes: 101 additions & 0 deletions Tests/ContainerAPIClientTests/UtilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerPersistence
import ContainerResource
import ContainerizationError
import Foundation
Expand Down Expand Up @@ -113,6 +114,106 @@ struct UtilityTests {
#expect(Utility.trimDigest(digest: "sha256:abc") == "abc")
}

@Test
func testDNSConfigurationUsesDefaultsWhenFlagsAbsent() {
let defaults = ContainerDNSConfig(
domain: "corp.local",
nameservers: ["1.1.1.1"],
searchDomains: ["corp.local"],
options: ["ndots:2"]
)

let flags = Flags.DNS(
domain: nil,
nameservers: [],
options: [],
searchDomains: []
)

let result = Utility.dnsConfiguration(from: flags, defaults: defaults)

#expect(result.domain == "corp.local")
#expect(result.nameservers == ["1.1.1.1"])
#expect(result.searchDomains == ["corp.local"])
#expect(result.options == ["ndots:2"])
}

@Test
func testDNSConfigurationFlagsOverrideDefaults() {
let defaults = ContainerDNSConfig(
domain: "corp.local",
nameservers: ["1.1.1.1"],
searchDomains: ["corp.local"],
options: ["ndots:2"]
)
let flags = Flags.DNS(
domain: "cli.local",
nameservers: ["8.8.8.8"],
options: ["debug"],
searchDomains: ["cli.local"]
)

let result = Utility.dnsConfiguration(from: flags, defaults: defaults)

#expect(result.domain == "cli.local")
#expect(result.nameservers == ["8.8.8.8"])
#expect(result.searchDomains == ["cli.local"])
#expect(result.options == ["debug"])
}

@Test
func testDNSConfigurationPartialFlagsFallbackToDefaults() {
let defaults = ContainerDNSConfig(
domain: "corp.local",
nameservers: ["1.1.1.1"],
searchDomains: ["corp.local"],
options: ["ndots:2"]
)
let flags = Flags.DNS(
domain: nil,
nameservers: ["8.8.8.8"],
options: [],
searchDomains: []
)

let result = Utility.dnsConfiguration(from: flags, defaults: defaults)

#expect(result.domain == "corp.local")
#expect(result.nameservers == ["8.8.8.8"])
#expect(result.searchDomains == ["corp.local"])
#expect(result.options == ["ndots:2"])
}

@Test
func testDNSConfigurationDomainFallsBackToHostDomain() {
let defaults = ContainerDNSConfig()
let flags = Flags.DNS(
domain: nil,
nameservers: [],
options: [],
searchDomains: []
)

let result = Utility.dnsConfiguration(from: flags, defaults: defaults, hostDomainFallback: "host.local")

#expect(result.domain == "host.local")
}

@Test
func testDNSConfigurationContainerDomainWinsOverHostDomain() {
let defaults = ContainerDNSConfig(domain: "container.local")
let flags = Flags.DNS(
domain: nil,
nameservers: [],
options: [],
searchDomains: []
)

let result = Utility.dnsConfiguration(from: flags, defaults: defaults, hostDomainFallback: "host.local")

#expect(result.domain == "container.local")
}

@Test
func testPublishPortParser() throws {
let ports = try Parser.publishPorts([
Expand Down
Loading