mirror of
https://github.com/apple/container.git
synced 2026-09-23 16:15:37 +00:00
Centralize utilities for configuration loading and path parsing (#1448)
This creates a more centralized utility to find and load configuration files, as a follow up to (pr: https://github.com/apple/container/pull/1425).
This commit is contained in:
@@ -229,6 +229,7 @@ let package = Package(
|
||||
.product(name: "SystemPackage", package: "swift-system"),
|
||||
"ContainerAPIClient",
|
||||
"ContainerPersistence",
|
||||
"ContainerTestSupport",
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
|
||||
@@ -50,9 +50,7 @@ extension APIServer {
|
||||
var logRoot = LogRoot.path
|
||||
|
||||
func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let commandName = APIServer._commandName
|
||||
let logPath = logRoot.map { $0.appending("\(commandName).log") }
|
||||
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
|
||||
|
||||
@@ -149,9 +149,7 @@ extension Application {
|
||||
var pull: Bool = false
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
do {
|
||||
let timeout: Duration = .seconds(300)
|
||||
let progressConfig = try ProgressConfig(
|
||||
|
||||
@@ -55,9 +55,7 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let progressConfig = try ProgressConfig(
|
||||
showTasks: true,
|
||||
showItems: true,
|
||||
|
||||
@@ -56,9 +56,7 @@ extension Application {
|
||||
var arguments: [String] = []
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let progressConfig = try ProgressConfig(
|
||||
showTasks: true,
|
||||
showItems: true,
|
||||
|
||||
@@ -63,9 +63,7 @@ extension Application {
|
||||
var arguments: [String] = []
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
var exitCode: Int32 = 127
|
||||
let id = Utility.createContainerID(name: self.managementFlags.name)
|
||||
|
||||
|
||||
@@ -109,9 +109,7 @@ extension Application {
|
||||
}
|
||||
|
||||
public mutating func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
try await DeleteImageImplementation.removeImage(options: options, containerSystemConfig: containerSystemConfig, log: log)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,7 @@ extension Application {
|
||||
}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
var printable: [ImageDetail] = []
|
||||
var succeededImages: [String] = []
|
||||
var allErrors: [(String, Error)] = []
|
||||
|
||||
@@ -45,9 +45,7 @@ extension Application {
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public mutating func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
try Self.validate(format: format, quiet: quiet, verbose: verbose)
|
||||
|
||||
var images = try await ClientImage.list().filter { img in
|
||||
|
||||
@@ -69,9 +69,7 @@ extension Application {
|
||||
}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
|
||||
|
||||
let scheme = try RequestScheme(registry.scheme)
|
||||
|
||||
@@ -57,9 +57,7 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
|
||||
|
||||
let scheme = try RequestScheme(registry.scheme)
|
||||
|
||||
@@ -62,9 +62,7 @@ extension Application {
|
||||
@Argument var references: [String]
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
|
||||
|
||||
let progressConfig = try ProgressConfig(
|
||||
|
||||
@@ -36,9 +36,7 @@ extension Application {
|
||||
public var logOptions: Flags.Logging
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let existing = try await ClientImage.get(reference: source, containerSystemConfig: containerSystemConfig)
|
||||
let targetReference = try ClientImage.normalizeReference(target, containerSystemConfig: containerSystemConfig)
|
||||
try await existing.tag(new: targetReference)
|
||||
|
||||
@@ -47,9 +47,7 @@ extension Application {
|
||||
var server: String
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
var username = self.username
|
||||
var password = ""
|
||||
if passwordStdin {
|
||||
|
||||
@@ -53,9 +53,7 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
if recommended {
|
||||
let url = containerSystemConfig.kernel.url
|
||||
let path: String = containerSystemConfig.kernel.binaryPath
|
||||
|
||||
@@ -42,9 +42,7 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let output =
|
||||
switch format {
|
||||
case .json: try Output.renderJSON(containerSystemConfig)
|
||||
|
||||
@@ -72,12 +72,10 @@ extension Application {
|
||||
public init() {}
|
||||
|
||||
public func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
// Copy the user's config into appRoot/config/ so that all plugins
|
||||
// and the apiserver subsequently read the same snapshot.
|
||||
SystemRuntimeOptions.copyConfigToAppRoot(appRoot: appRoot)
|
||||
let appRootPath = FilePath(appRoot.path(percentEncoded: false))
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(to: appRootPath)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load(
|
||||
configurationFile: ConfigurationLoader.configurationFile(in: appRootPath))
|
||||
|
||||
// Without the true path to the binary in the plist, `container-apiserver` won't launch properly.
|
||||
// Resolve the symlink to get the true binary path before writing the launchd plist.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationError
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import TOML
|
||||
|
||||
public protocol Initable {
|
||||
init()
|
||||
}
|
||||
|
||||
public typealias LoadableConfiguration = Codable & Sendable & Initable
|
||||
|
||||
public enum ConfigurationLoader {
|
||||
private static let configFilename = "runtime-config.toml"
|
||||
private static let configDirectory = "config"
|
||||
private static let READ_ONLY: Int = 0o444
|
||||
private static let READ_AND_WRITE: Int = 0o644
|
||||
|
||||
/// Returns the canonical configuration file path under an appRoot base directory:
|
||||
/// `<base>/config/runtime-config.toml`.
|
||||
public static func configurationFile(in base: FilePath) -> FilePath {
|
||||
base.appending(configDirectory).appending(configFilename)
|
||||
}
|
||||
|
||||
/// Loads and decodes a TOML configuration file as type `T`.
|
||||
///
|
||||
/// - Parameter configurationFile: Absolute path to the configuration file.
|
||||
/// When `nil`, falls back to
|
||||
/// `configurationFile(in: PathUtils.BaseConfigPath.appRoot.basePath())`.
|
||||
/// - Returns: A decoded value of type `T`, or a default-initialized `T` if the
|
||||
/// configuration file does not exist.
|
||||
public static func load<T: LoadableConfiguration>(configurationFile: FilePath? = nil) throws -> T {
|
||||
let path = configurationFile ?? Self.configurationFile(in: PathUtils.BaseConfigPath.appRoot.basePath())
|
||||
guard FileManager.default.fileExists(atPath: path.string) else {
|
||||
return T()
|
||||
}
|
||||
do {
|
||||
let data = try Data(contentsOf: URL(filePath: path.string))
|
||||
return try TOMLDecoder().decode(T.self, from: data)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.invalidArgument,
|
||||
message: "failed to load configuration from '\(path)': \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies a TOML configuration file into a read-only destination under an appRoot base.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The file to copy. When `nil`, defaults to
|
||||
/// `<home>/container/runtime-config.toml`. If the source does not exist,
|
||||
/// this is a no-op.
|
||||
/// - destination: Base directory under which the file is written at
|
||||
/// `<destination>/config/runtime-config.toml`. When `nil`, falls back to
|
||||
/// `PathUtils.BaseConfigPath.appRoot.basePath()`. The destination file is written
|
||||
/// with `READ_ONLY` (`0o444`) permissions.
|
||||
public static func copyConfigurationToReadOnly(
|
||||
from source: FilePath? = nil,
|
||||
to destination: FilePath? = nil
|
||||
) throws {
|
||||
let source =
|
||||
source
|
||||
?? PathUtils.BaseConfigPath.home.basePath()
|
||||
.appending(configFilename)
|
||||
let destinationFile = Self.configurationFile(in: destination ?? PathUtils.BaseConfigPath.appRoot.basePath())
|
||||
do {
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: source.string) else { return }
|
||||
|
||||
let destDir = destinationFile.removingLastComponent()
|
||||
try fm.createDirectory(
|
||||
atPath: destDir.string,
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
if fm.fileExists(atPath: destinationFile.string) {
|
||||
try fm.setAttributes(
|
||||
[.posixPermissions: READ_AND_WRITE],
|
||||
ofItemAtPath: destinationFile.string
|
||||
)
|
||||
try fm.removeItem(at: URL(filePath: destinationFile.string))
|
||||
}
|
||||
try fm.copyItem(
|
||||
at: URL(filePath: source.string),
|
||||
to: URL(filePath: destinationFile.string)
|
||||
)
|
||||
try fm.setAttributes(
|
||||
[.posixPermissions: READ_ONLY],
|
||||
ofItemAtPath: destinationFile.string
|
||||
)
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.invalidState, message: "Failed to copy user TOML to AppRoot `\(error)`")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import Foundation
|
||||
///
|
||||
/// Each section maps to a nested struct. Missing keys fall back to
|
||||
/// hardcoded defaults via custom `init(from:)` implementations.
|
||||
final public class ContainerSystemConfig: Codable, Sendable {
|
||||
public final class ContainerSystemConfig: Codable, Sendable, Initable {
|
||||
public let build: BuildConfig
|
||||
public let container: ContainerConfig
|
||||
public let dns: DNSConfig
|
||||
@@ -50,6 +50,16 @@ final public class ContainerSystemConfig: Codable, Sendable {
|
||||
self.vminit = vminit
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.build = .init()
|
||||
self.container = .init()
|
||||
self.dns = .init()
|
||||
self.kernel = .init()
|
||||
self.network = .init()
|
||||
self.registry = .init()
|
||||
self.vminit = .init()
|
||||
}
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.build = try container.decodeIfPresent(BuildConfig.self, forKey: .build) ?? .init()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 SystemPackage
|
||||
|
||||
public enum PathUtils {
|
||||
public enum BaseConfigPath {
|
||||
case home
|
||||
case appRoot
|
||||
|
||||
public func basePath(env: [String: String] = ProcessInfo.processInfo.environment) -> FilePath {
|
||||
switch self {
|
||||
case .home:
|
||||
let configHome: String
|
||||
if let xdg = env["XDG_CONFIG_HOME"], !xdg.isEmpty {
|
||||
configHome = xdg
|
||||
} else {
|
||||
configHome = NSHomeDirectory() + "/.config"
|
||||
}
|
||||
return FilePath(configHome).appending("container")
|
||||
case .appRoot:
|
||||
if let envPath = env["CONTAINER_APP_ROOT"], !envPath.isEmpty {
|
||||
return FilePath(envPath)
|
||||
}
|
||||
let appSupportURL = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.appendingPathComponent("com.apple.container")
|
||||
return FilePath(appSupportURL.path(percentEncoded: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerizationError
|
||||
import Foundation
|
||||
import Logging
|
||||
import SystemPackage
|
||||
import TOML
|
||||
|
||||
private let log = Logger(label: "SystemRuntimeOptions")
|
||||
|
||||
/// TOML-backed configuration loader.
|
||||
///
|
||||
/// Decodes a user-provided TOML file into a typed configuration struct.
|
||||
/// Missing keys fall back to the struct's hardcoded defaults (via custom
|
||||
/// `init(from:)` implementations using `decodeIfPresent`).
|
||||
///
|
||||
/// Configuration priority (highest to lowest):
|
||||
/// 1. User config: `$XDG_CONFIG_HOME/container/runtime-config.toml`
|
||||
/// 2. Hardcoded defaults in the config struct's initializer
|
||||
public enum SystemRuntimeOptions {
|
||||
/// Path to the user's configuration file.
|
||||
public static var defaultUserConfigPath: URL {
|
||||
let configHome: String
|
||||
if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], !xdg.isEmpty {
|
||||
configHome = xdg
|
||||
} else {
|
||||
configHome = NSHomeDirectory() + "/.config"
|
||||
}
|
||||
return URL(fileURLWithPath: configHome)
|
||||
.appendingPathComponent("container")
|
||||
.appendingPathComponent("runtime-config.toml")
|
||||
}
|
||||
|
||||
/// The path to the config file within an application root directory.
|
||||
public static func configFileFromAppRoot(_ appRoot: URL) -> URL {
|
||||
appRoot
|
||||
.appendingPathComponent("config")
|
||||
.appendingPathComponent("runtime-config.toml")
|
||||
}
|
||||
|
||||
/// Load configuration by decoding a TOML file.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - configFile: Full path to the TOML config file.
|
||||
/// - Returns: The decoded configuration. If the file does not exist, all values
|
||||
/// fall back to the type's hardcoded defaults.
|
||||
public static func loadConfig<T: Codable & Sendable>(configFile: URL) throws -> T {
|
||||
let fm = FileManager.default
|
||||
let path = configFile.path(percentEncoded: false)
|
||||
guard fm.fileExists(atPath: path) else {
|
||||
do {
|
||||
return try TOMLDecoder().decode(T.self, from: Data("".utf8))
|
||||
} catch {
|
||||
throw ContainerizationError(.internalError, message: "failed to initialize default configuration: \(error)")
|
||||
}
|
||||
}
|
||||
do {
|
||||
let data = try Data(contentsOf: configFile)
|
||||
return try TOMLDecoder().decode(T.self, from: data)
|
||||
} catch {
|
||||
throw ContainerizationError(.invalidArgument, message: "failed to load configuration from '\(path)': \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public static func copyConfigToAppRoot(
|
||||
appRoot: URL,
|
||||
userConfigPath: URL = defaultUserConfigPath,
|
||||
) {
|
||||
let fm = FileManager.default
|
||||
|
||||
if fm.fileExists(atPath: userConfigPath.path(percentEncoded: false)) {
|
||||
let configDir = appRoot.appendingPathComponent("config")
|
||||
let destPath = configDir.appendingPathComponent("runtime-config.toml")
|
||||
do {
|
||||
try fm.createDirectory(at: configDir, withIntermediateDirectories: true)
|
||||
if fm.fileExists(atPath: destPath.path(percentEncoded: false)) {
|
||||
try fm.setAttributes([.posixPermissions: 0o644], ofItemAtPath: destPath.path(percentEncoded: false))
|
||||
try fm.removeItem(at: destPath)
|
||||
}
|
||||
try fm.copyItem(
|
||||
at: URL(fileURLWithPath: userConfigPath.path(percentEncoded: false)),
|
||||
to: destPath
|
||||
)
|
||||
try fm.setAttributes(
|
||||
[.posixPermissions: 0o444],
|
||||
ofItemAtPath: destPath.path(percentEncoded: false)
|
||||
)
|
||||
log.info("copied runtime config", metadata: ["dest": "\(destPath.path(percentEncoded: false))"])
|
||||
} catch {
|
||||
// If the config copy-ing fails, we will log an error but it is not fatal since we can utilize the fallback config.
|
||||
log.error("failed to copy runtime config to app root", metadata: ["error": "\(error)"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,7 @@ extension ImagesHelper {
|
||||
var logRoot = LogRoot.path
|
||||
|
||||
func run() async throws {
|
||||
let containerSystemConfig: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(
|
||||
configFile: SystemRuntimeOptions.configFileFromAppRoot(ApplicationRoot.url)
|
||||
)
|
||||
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
|
||||
let commandName = ImagesHelper._commandName
|
||||
let logPath = logRoot.map { $0.appending("\(commandName).log") }
|
||||
let log = ServiceLogger.bootstrap(category: "ImagesHelper", debug: debug, logPath: logPath)
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerTestSupport
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import Testing
|
||||
|
||||
@testable import ContainerPersistence
|
||||
|
||||
struct ConfigurationLoaderTests {
|
||||
private static func writeToml(_ contents: String, to path: FilePath) throws {
|
||||
try contents.write(
|
||||
to: URL(filePath: path.string),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
}
|
||||
|
||||
@Test func testDefaultsWithNoFile() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let path = tempDir.appending("nonexistent.toml")
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: path)
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.cpus == 2)
|
||||
#expect(config.build.memory == BuildConfig.defaultMemory)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.container.memory == ContainerConfig.defaultMemory)
|
||||
#expect(config.dns.domain == nil)
|
||||
#expect(!config.build.image.isEmpty)
|
||||
#expect(!config.vminit.image.isEmpty)
|
||||
#expect(!config.kernel.binaryPath.isEmpty)
|
||||
#expect(!config.kernel.url.absoluteString.isEmpty)
|
||||
#expect(config.network.subnet == nil)
|
||||
#expect(config.network.subnetv6 == nil)
|
||||
#expect(config.registry.domain == "docker.io")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testTomlOverrideAllKeys() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let toml = """
|
||||
[build]
|
||||
rosetta = false
|
||||
cpus = 8
|
||||
memory = "4096MB"
|
||||
image = "custom-builder:latest"
|
||||
|
||||
[container]
|
||||
cpus = 16
|
||||
memory = "8g"
|
||||
|
||||
[dns]
|
||||
domain = "custom"
|
||||
|
||||
[kernel]
|
||||
binaryPath = "custom/path"
|
||||
url = "https://example.com/kernel.tar"
|
||||
|
||||
[network]
|
||||
subnet = "10.0.0.1/16"
|
||||
subnetv6 = "fd01::/48"
|
||||
|
||||
[registry]
|
||||
domain = "ghcr.io"
|
||||
|
||||
[vminit]
|
||||
image = "custom-init:latest"
|
||||
"""
|
||||
let tmpFile = tempDir.appending("test.toml")
|
||||
try Self.writeToml(toml, to: tmpFile)
|
||||
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
#expect(config.build.rosetta == false)
|
||||
#expect(config.build.cpus == 8)
|
||||
let expectedBuildMemory = try MemorySize("4096MB")
|
||||
#expect(config.build.memory == expectedBuildMemory)
|
||||
#expect(config.container.cpus == 16)
|
||||
let expectedContainerMemory = try MemorySize("8g")
|
||||
#expect(config.container.memory == expectedContainerMemory)
|
||||
#expect(config.dns.domain == "custom")
|
||||
#expect(config.build.image == "custom-builder:latest")
|
||||
#expect(config.vminit.image == "custom-init:latest")
|
||||
#expect(config.kernel.binaryPath == "custom/path")
|
||||
#expect(config.kernel.url.absoluteString == "https://example.com/kernel.tar")
|
||||
let expectedSubnet = try CIDRv4("10.0.0.1/16")
|
||||
let expectedSubnetV6 = try CIDRv6("fd01::/48")
|
||||
#expect(config.network.subnet == expectedSubnet)
|
||||
#expect(config.network.subnetv6 == expectedSubnetV6)
|
||||
#expect(config.registry.domain == "ghcr.io")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testPartialToml() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 16
|
||||
"""
|
||||
let tmpFile = tempDir.appending("test.toml")
|
||||
try Self.writeToml(toml, to: tmpFile)
|
||||
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
#expect(config.build.cpus == 16)
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.memory == BuildConfig.defaultMemory)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.container.memory == ContainerConfig.defaultMemory)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testUnknownKeysIgnored() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 4
|
||||
unknownBuildKey = "ignored"
|
||||
|
||||
[unknownSection]
|
||||
foo = "bar"
|
||||
"""
|
||||
let tmpFile = tempDir.appending("test.toml")
|
||||
try Self.writeToml(toml, to: tmpFile)
|
||||
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
#expect(config.build.cpus == 4)
|
||||
#expect(config.build.rosetta == true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testInvalidTomlThrows() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let tmpFile = tempDir.appending("test-invalid.toml")
|
||||
try Self.writeToml("this is [not valid toml", to: tmpFile)
|
||||
#expect(throws: (any Error).self) {
|
||||
let _: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testEmptyTomlDecodesToDefaults() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let tmpFile = tempDir.appending("test.toml")
|
||||
try Self.writeToml("", to: tmpFile)
|
||||
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.cpus == 2)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.registry.domain == "docker.io")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyConfigToAppRoot() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let source = tempDir.appending("runtime-config.toml")
|
||||
try Self.writeToml("[build]\ncpus = 8", to: source)
|
||||
|
||||
let destBase = tempDir.appending("dest")
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)
|
||||
|
||||
let destFile = ConfigurationLoader.configurationFile(in: destBase)
|
||||
let copied = try String(contentsOf: URL(filePath: destFile.string), encoding: .utf8)
|
||||
#expect(copied.contains("cpus = 8"))
|
||||
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: destFile.string)
|
||||
let perms = attrs[.posixPermissions] as! Int
|
||||
#expect(perms == 0o444)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyConfigOverwritesExistingReadOnlyDestination() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let source = tempDir.appending("runtime-config.toml")
|
||||
let destBase = tempDir.appending("dest")
|
||||
|
||||
try Self.writeToml("[build]\ncpus = 8", to: source)
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)
|
||||
|
||||
try Self.writeToml("[build]\ncpus = 16", to: source)
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)
|
||||
|
||||
let destFile = ConfigurationLoader.configurationFile(in: destBase)
|
||||
let copied = try String(contentsOf: URL(filePath: destFile.string), encoding: .utf8)
|
||||
#expect(copied.contains("cpus = 16"))
|
||||
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: destFile.string)
|
||||
let perms = attrs[.posixPermissions] as! Int
|
||||
#expect(perms == 0o444)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testCopyConfigNoOpsWhenSourceMissing() async throws {
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let source = tempDir.appending("nonexistent.toml")
|
||||
let destBase = tempDir.appending("dest")
|
||||
try ConfigurationLoader.copyConfigurationToReadOnly(from: source, to: destBase)
|
||||
let destFile = ConfigurationLoader.configurationFile(in: destBase)
|
||||
#expect(!FileManager.default.fileExists(atPath: destFile.string))
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testIndependentPluginConfig() async throws {
|
||||
struct PluginConfig: Codable, Sendable, Initable {
|
||||
var network: NetworkConfig
|
||||
init() { self.network = .init() }
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.network = try container.decodeIfPresent(NetworkConfig.self, forKey: .network) ?? .init()
|
||||
}
|
||||
}
|
||||
|
||||
try await TemporaryStorage.withTempDir { tempDir in
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 8
|
||||
|
||||
[network]
|
||||
subnet = "10.1.2.3/24"
|
||||
subnetv6 = "fd02::/48"
|
||||
"""
|
||||
let tmpFile = tempDir.appending("test.toml")
|
||||
try Self.writeToml(toml, to: tmpFile)
|
||||
|
||||
let config: PluginConfig = try ConfigurationLoader.load(configurationFile: tmpFile)
|
||||
let expectedSubnet = try CIDRv4("10.1.2.3/24")
|
||||
let expectedSubnetV6 = try CIDRv6("fd02::/48")
|
||||
#expect(config.network.subnet == expectedSubnet)
|
||||
#expect(config.network.subnetv6 == expectedSubnetV6)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,14 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerPersistence
|
||||
import ContainerizationError
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import Testing
|
||||
|
||||
@testable import ContainerAPIClient
|
||||
@testable import ContainerPersistence
|
||||
|
||||
struct ParserTest {
|
||||
@Test
|
||||
@@ -1210,7 +1211,7 @@ struct ParserTest {
|
||||
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
||||
defer { try? FileManager.default.removeItem(at: tempFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: FilePath(tempFile.path(percentEncoded: false)))
|
||||
let result = try Parser.resources(
|
||||
cpus: nil, memory: nil,
|
||||
defaultCPUs: config.build.cpus,
|
||||
@@ -1229,7 +1230,7 @@ struct ParserTest {
|
||||
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
||||
defer { try? FileManager.default.removeItem(at: tempFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: FilePath(tempFile.path(percentEncoded: false)))
|
||||
let result = try Parser.resources(
|
||||
cpus: nil, memory: nil,
|
||||
defaultCPUs: config.container.cpus,
|
||||
@@ -1247,7 +1248,7 @@ struct ParserTest {
|
||||
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
||||
defer { try? FileManager.default.removeItem(at: tempFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: FilePath(tempFile.path(percentEncoded: false)))
|
||||
let result = try Parser.resources(
|
||||
cpus: nil, memory: nil,
|
||||
defaultCPUs: config.container.cpus,
|
||||
@@ -1266,7 +1267,7 @@ struct ParserTest {
|
||||
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
||||
defer { try? FileManager.default.removeItem(at: tempFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: FilePath(tempFile.path(percentEncoded: false)))
|
||||
let result = try Parser.resources(
|
||||
cpus: 1, memory: "256m",
|
||||
defaultCPUs: config.container.cpus,
|
||||
@@ -1286,7 +1287,7 @@ struct ParserTest {
|
||||
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
||||
defer { try? FileManager.default.removeItem(at: tempFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
||||
let config: ContainerSystemConfig = try ConfigurationLoader.load(configurationFile: FilePath(tempFile.path(percentEncoded: false)))
|
||||
let result = try Parser.resources(
|
||||
cpus: nil, memory: nil,
|
||||
defaultCPUs: config.build.cpus,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerPersistence
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
import Testing
|
||||
|
||||
struct PathUtilsTests {
|
||||
private static let homeFallback = FilePath(NSHomeDirectory() + "/.config").appending("container")
|
||||
private static let appRootFallback: FilePath = {
|
||||
let url = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.appendingPathComponent("com.apple.container")
|
||||
return FilePath(url.path(percentEncoded: false))
|
||||
}()
|
||||
|
||||
@Test func testHomeUsesXdgConfigHomeWhenSet() {
|
||||
let path = PathUtils.BaseConfigPath.home.basePath(env: ["XDG_CONFIG_HOME": "/tmp/xdg-test"])
|
||||
#expect(path == FilePath("/tmp/xdg-test/container"))
|
||||
}
|
||||
|
||||
@Test func testHomeFallsBackToHomeDirectoryWhenXdgUnset() {
|
||||
let path = PathUtils.BaseConfigPath.home.basePath(env: [:])
|
||||
#expect(path == Self.homeFallback)
|
||||
}
|
||||
|
||||
@Test func testHomeTreatsEmptyXdgAsUnset() {
|
||||
let path = PathUtils.BaseConfigPath.home.basePath(env: ["XDG_CONFIG_HOME": ""])
|
||||
#expect(path == Self.homeFallback)
|
||||
}
|
||||
|
||||
@Test func testAppRootUsesContainerAppRootWhenSet() {
|
||||
let path = PathUtils.BaseConfigPath.appRoot.basePath(env: ["CONTAINER_APP_ROOT": "/tmp/foo"])
|
||||
#expect(path == FilePath("/tmp/foo"))
|
||||
}
|
||||
|
||||
@Test func testAppRootFallsBackToApplicationSupportWhenUnset() {
|
||||
let path = PathUtils.BaseConfigPath.appRoot.basePath(env: [:])
|
||||
#expect(path == Self.appRootFallback)
|
||||
}
|
||||
|
||||
@Test func testAppRootTreatsEmptyEnvAsUnset() {
|
||||
let path = PathUtils.BaseConfigPath.appRoot.basePath(env: ["CONTAINER_APP_ROOT": ""])
|
||||
#expect(path == Self.appRootFallback)
|
||||
}
|
||||
|
||||
@Test func testAppRootIgnoresXdgConfigHome() {
|
||||
let path = PathUtils.BaseConfigPath.appRoot.basePath(env: ["XDG_CONFIG_HOME": "/tmp/xdg-test"])
|
||||
#expect(path == Self.appRootFallback)
|
||||
}
|
||||
|
||||
@Test func testHomeIgnoresContainerAppRoot() {
|
||||
let path = PathUtils.BaseConfigPath.home.basePath(env: ["CONTAINER_APP_ROOT": "/tmp/foo"])
|
||||
#expect(path == Self.homeFallback)
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 ContainerPersistence
|
||||
import ContainerizationExtras
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
struct SystemRuntimeOptionsTests {
|
||||
@Test func testDefaultsWithNoToml() throws {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("nonexistent-\(UUID().uuidString).toml")
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: url)
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.cpus == 2)
|
||||
#expect(config.build.memory == BuildConfig.defaultMemory)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.container.memory == ContainerConfig.defaultMemory)
|
||||
#expect(config.dns.domain == nil)
|
||||
#expect(!config.build.image.isEmpty)
|
||||
#expect(!config.vminit.image.isEmpty)
|
||||
#expect(!config.kernel.binaryPath.isEmpty)
|
||||
#expect(!config.kernel.url.absoluteString.isEmpty)
|
||||
#expect(config.network.subnet == nil)
|
||||
#expect(config.network.subnetv6 == nil)
|
||||
#expect(config.registry.domain == "docker.io")
|
||||
}
|
||||
|
||||
@Test func testTomlOverrideAllKeys() throws {
|
||||
let toml = """
|
||||
[build]
|
||||
rosetta = false
|
||||
cpus = 8
|
||||
memory = "4096MB"
|
||||
image = "custom-builder:latest"
|
||||
|
||||
[container]
|
||||
cpus = 16
|
||||
memory = "8g"
|
||||
|
||||
[dns]
|
||||
domain = "custom"
|
||||
|
||||
[kernel]
|
||||
binaryPath = "custom/path"
|
||||
url = "https://example.com/kernel.tar"
|
||||
|
||||
[network]
|
||||
subnet = "10.0.0.1/16"
|
||||
subnetv6 = "fd01::/48"
|
||||
|
||||
[registry]
|
||||
domain = "ghcr.io"
|
||||
|
||||
[vminit]
|
||||
image = "custom-init:latest"
|
||||
"""
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-\(UUID().uuidString).toml")
|
||||
try toml.write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
#expect(config.build.rosetta == false)
|
||||
#expect(config.build.cpus == 8)
|
||||
let expectedBuildMemory = try MemorySize("4096MB")
|
||||
#expect(config.build.memory == expectedBuildMemory)
|
||||
#expect(config.container.cpus == 16)
|
||||
let expectedContainerMemory = try MemorySize("8g")
|
||||
#expect(config.container.memory == expectedContainerMemory)
|
||||
#expect(config.dns.domain == "custom")
|
||||
#expect(config.build.image == "custom-builder:latest")
|
||||
#expect(config.vminit.image == "custom-init:latest")
|
||||
#expect(config.kernel.binaryPath == "custom/path")
|
||||
#expect(config.kernel.url.absoluteString == "https://example.com/kernel.tar")
|
||||
let expectedSubnet = try CIDRv4("10.0.0.1/16")
|
||||
let expectedSubnetV6 = try CIDRv6("fd01::/48")
|
||||
#expect(config.network.subnet == expectedSubnet)
|
||||
#expect(config.network.subnetv6 == expectedSubnetV6)
|
||||
#expect(config.registry.domain == "ghcr.io")
|
||||
}
|
||||
|
||||
@Test func testPartialToml() throws {
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 16
|
||||
"""
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-\(UUID().uuidString).toml")
|
||||
try toml.write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
#expect(config.build.cpus == 16)
|
||||
// All other fields should have their hardcoded defaults
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.memory == BuildConfig.defaultMemory)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.container.memory == ContainerConfig.defaultMemory)
|
||||
#expect(config.dns.domain == nil)
|
||||
#expect(config.network.subnet == nil)
|
||||
#expect(config.network.subnetv6 == nil)
|
||||
#expect(config.registry.domain == "docker.io")
|
||||
}
|
||||
|
||||
@Test func testUnknownKeysIgnored() throws {
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 4
|
||||
unknownBuildKey = "ignored"
|
||||
|
||||
[unknownSection]
|
||||
foo = "bar"
|
||||
"""
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-\(UUID().uuidString).toml")
|
||||
try toml.write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
#expect(config.build.cpus == 4)
|
||||
#expect(config.build.rosetta == true)
|
||||
}
|
||||
|
||||
@Test func testIndependentPluginConfig() throws {
|
||||
struct PluginConfig: Codable, Sendable {
|
||||
var network: NetworkConfig
|
||||
init(network: NetworkConfig = .init()) { self.network = network }
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.network = try container.decodeIfPresent(NetworkConfig.self, forKey: .network) ?? .init()
|
||||
}
|
||||
}
|
||||
|
||||
let toml = """
|
||||
[build]
|
||||
cpus = 8
|
||||
|
||||
[network]
|
||||
subnet = "10.1.2.3/24"
|
||||
subnetv6 = "fd02::/48"
|
||||
"""
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-\(UUID().uuidString).toml")
|
||||
try toml.write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
|
||||
let config: PluginConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
// Only network is decoded; build section is ignored
|
||||
let expectedSubnet = try CIDRv4("10.1.2.3/24")
|
||||
let expectedSubnetV6 = try CIDRv6("fd02::/48")
|
||||
#expect(config.network.subnet == expectedSubnet)
|
||||
#expect(config.network.subnetv6 == expectedSubnetV6)
|
||||
}
|
||||
|
||||
@Test func testInvalidTomlThrows() throws {
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-invalid-toml.toml")
|
||||
try "this is [not valid toml".write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
#expect(throws: (any Error).self) {
|
||||
let _: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testEmptyTomlDecodesToDefaults() throws {
|
||||
let tmpFile = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("test-\(UUID().uuidString).toml")
|
||||
try "".write(to: tmpFile, atomically: true, encoding: .utf8)
|
||||
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
||||
|
||||
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tmpFile)
|
||||
#expect(config.build.rosetta == true)
|
||||
#expect(config.build.cpus == 2)
|
||||
#expect(config.build.memory == BuildConfig.defaultMemory)
|
||||
#expect(config.container.cpus == 4)
|
||||
#expect(config.container.memory == ContainerConfig.defaultMemory)
|
||||
#expect(config.dns.domain == nil)
|
||||
#expect(config.network.subnet == nil)
|
||||
#expect(config.network.subnetv6 == nil)
|
||||
#expect(config.registry.domain == "docker.io")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user