diff --git a/Sources/ContainerCommands/BuildCommand.swift b/Sources/ContainerCommands/BuildCommand.swift index f15aa60cc..779454719 100644 --- a/Sources/ContainerCommands/BuildCommand.swift +++ b/Sources/ContainerCommands/BuildCommand.swift @@ -162,15 +162,32 @@ extension Application { } progress.start() - progress.set(description: "Dialing builder") - let dnsNameservers = self.dns.nameservers - let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, dnsNameservers] group in + let dnsDomain = self.dns.domain + let dnsSearchDomains = self.dns.searchDomains + let dnsOptions = self.dns.options + progress.set(tasks: 0) + progress.set(totalTasks: 3) + try await BuilderStart.start( + cpus: self.cpus, + memory: self.memory, + log: log, + dnsNameservers: dnsNameservers, + dnsDomain: dnsDomain, + dnsSearchDomains: dnsSearchDomains, + dnsOptions: dnsOptions, + progressUpdate: progress.handler, + containerSystemConfig: containerSystemConfig, + ) + + progress.set(description: "Dialing builder") + let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { + [vsockPort, cpus, memory, dnsNameservers, dnsDomain, dnsSearchDomains, dnsOptions] group in defer { group.cancelAll() } - group.addTask { [vsockPort, cpus, memory, log, dnsNameservers] in + group.addTask { [vsockPort, cpus, memory, log, dnsNameservers, dnsDomain, dnsSearchDomains, dnsOptions] in let client = ContainerClient() while true { do { @@ -193,6 +210,9 @@ extension Application { memory: memory, log: log, dnsNameservers: dnsNameservers, + dnsDomain: dnsDomain, + dnsSearchDomains: dnsSearchDomains, + dnsOptions: dnsOptions, progressUpdate: progress.handler, containerSystemConfig: containerSystemConfig, ) diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index a467ea123..9d3d7c54e 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -91,6 +91,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.dns + ) + await progressUpdate([ .setDescription("Fetching BuildKit image"), .setItemsName("blobs"), @@ -150,20 +160,17 @@ extension Application { let imageChanged = existingImage != builderImage let cpuChanged = existingResources.cpus != resources.cpus let memChanged = existingResources.memoryInBytes != resources.memoryInBytes + let hasDNSConfig = + !dns.nameservers.isEmpty + || dns.domain != nil + || !dns.searchDomains.isEmpty + || !dns.options.isEmpty 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 + guard hasDNSConfig else { return false } + return existingDNS?.nameservers != dns.nameservers + || existingDNS?.domain != dns.domain + || existingDNS?.searchDomains != dns.searchDomains + || existingDNS?.options != dns.options }() switch existingContainer.status { @@ -273,10 +280,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 { diff --git a/Sources/ContainerPersistence/ContainerSystemConfig.swift b/Sources/ContainerPersistence/ContainerSystemConfig.swift index 1b7ee0f46..a3b08d582 100644 --- a/Sources/ContainerPersistence/ContainerSystemConfig.swift +++ b/Sources/ContainerPersistence/ContainerSystemConfig.swift @@ -127,15 +127,33 @@ final public class ContainerConfig: Codable, Sendable { } final public class DNSConfig: Codable, Sendable { + public static let defaultNameservers: [String] = [] + public static let defaultSearchDomains: [String] = [] + public static let defaultOptions: [String] = [] + public let domain: String? + public let nameservers: [String] + public let searchDomains: [String] + public let options: [String] - public init(domain: String? = nil) { + public init( + domain: String? = nil, + nameservers: [String] = defaultNameservers, + searchDomains: [String] = defaultSearchDomains, + options: [String] = defaultOptions + ) { 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.defaultNameservers + self.searchDomains = try container.decodeIfPresent([String].self, forKey: .searchDomains) ?? Self.defaultSearchDomains + self.options = try container.decodeIfPresent([String].self, forKey: .options) ?? Self.defaultOptions } } diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index cf5b8d6df..e4f98a4f5 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -226,13 +226,7 @@ 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.dns) } config.rosetta = management.rosetta || (Platform.current.architecture == "arm64" && requestedPlatform.architecture == "amd64") @@ -333,6 +327,31 @@ public struct Utility { return [AttachmentConfiguration(network: builtinNetworkId, options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: nil, mtu: 1280))] } + public static func dnsConfiguration( + from flags: Flags.DNS, + defaults: DNSConfig + ) -> ContainerConfiguration.DNSConfiguration { + let nameservers = + flags.nameservers.isEmpty + ? defaults.nameservers + : flags.nameservers + let domain = flags.domain ?? defaults.domain + 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. diff --git a/Tests/ContainerAPIClientTests/UtilityTests.swift b/Tests/ContainerAPIClientTests/UtilityTests.swift index bc48df975..db4048e19 100644 --- a/Tests/ContainerAPIClientTests/UtilityTests.swift +++ b/Tests/ContainerAPIClientTests/UtilityTests.swift @@ -14,6 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerPersistence import ContainerResource import ContainerizationError import Foundation @@ -90,6 +91,76 @@ struct UtilityTests { } } + @Test + func testDNSConfigurationUsesDefaultsWhenFlagsAbsent() { + let defaults = DNSConfig( + 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 = DNSConfig( + 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 = DNSConfig( + 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 testPublishPortParser() throws { let ports = try Parser.publishPorts([ diff --git a/Tests/ContainerPersistenceTests/ConfigurationLoaderTests.swift b/Tests/ContainerPersistenceTests/ConfigurationLoaderTests.swift index 5f27694dc..ef2fd2058 100644 --- a/Tests/ContainerPersistenceTests/ConfigurationLoaderTests.swift +++ b/Tests/ContainerPersistenceTests/ConfigurationLoaderTests.swift @@ -91,6 +91,9 @@ struct ConfigurationLoaderTests { #expect(config.container.cpus == 4) #expect(config.container.memory == ContainerConfig.defaultMemory) #expect(config.dns.domain == nil) + #expect(config.dns.nameservers == []) + #expect(config.dns.searchDomains == []) + #expect(config.dns.options == []) #expect(!config.build.image.isEmpty) #expect(!config.vminit.image.isEmpty) #expect(!config.kernel.binaryPath.isEmpty) @@ -116,6 +119,9 @@ struct ConfigurationLoaderTests { [dns] domain = "custom" + nameservers = ["1.1.1.1", "8.8.8.8"] + searchDomains = ["corp.local", "lab.corp.local"] + options = ["ndots:2", "timeout:1"] [kernel] binaryPath = "custom/path" @@ -143,6 +149,9 @@ struct ConfigurationLoaderTests { let expectedContainerMemory = try MemorySize("8g") #expect(config.container.memory == expectedContainerMemory) #expect(config.dns.domain == "custom") + #expect(config.dns.nameservers == ["1.1.1.1", "8.8.8.8"]) + #expect(config.dns.searchDomains == ["corp.local", "lab.corp.local"]) + #expect(config.dns.options == ["ndots:2", "timeout:1"]) #expect(config.build.image == "custom-builder:latest") #expect(config.vminit.image == "custom-init:latest") #expect(config.kernel.binaryPath == "custom/path") @@ -173,6 +182,23 @@ struct ConfigurationLoaderTests { } } + @Test func dnsArraysLoadFromTomlIndependently() async throws { + try await TemporaryStorage.withTempDir { tempDir in + let toml = """ + [dns] + nameservers = ["1.1.1.1", "8.8.8.8"] + """ + let tmpFile = tempDir.appending("dns.toml") + try Self.writeToml(toml, to: tmpFile) + + let config: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile]) + #expect(config.dns.nameservers == ["1.1.1.1", "8.8.8.8"]) + #expect(config.dns.domain == nil) + #expect(config.dns.searchDomains == []) + #expect(config.dns.options == []) + } + } + @Test func unknownKeysIgnored() async throws { try await TemporaryStorage.withTempDir { tempDir in let toml = """ diff --git a/docs/how-to.md b/docs/how-to.md index 1b3c3d62f..31fc4d2cb 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -681,6 +681,20 @@ rosetta = false This is useful when you want to ensure builds only produce native arm64 images and avoid any x86_64 emulation. +### Example: Set default DNS settings + +To avoid passing `--dns`, `--dns-search`, and `--dns-option` on every `container run`, `container build`, or `container builder start` invocation, set defaults in `~/.config/container/config.toml`: + +```toml +[dns] +domain = "corp.local" +nameservers = ["1.1.1.1", "8.8.8.8"] +searchDomains = ["corp.local", "lab.corp.local"] +options = ["ndots:2", "timeout:1"] +``` + +CLI flags override these defaults when provided. Use `container run --no-dns` to skip DNS configuration entirely. Restart the daemon (`container system stop && container system start`) for changes to take effect. + ## View system logs The `container system logs` command allows you to look at the log messages that `container` writes: