diff --git a/Sources/ContainerCommands/Container/ContainerCreate.swift b/Sources/ContainerCommands/Container/ContainerCreate.swift index ac26d205a..21e82492e 100644 --- a/Sources/ContainerCommands/Container/ContainerCreate.swift +++ b/Sources/ContainerCommands/Container/ContainerCreate.swift @@ -82,7 +82,7 @@ extension Application { log: log ) - let options = ContainerCreateOptions(autoRemove: managementFlags.remove) + let options = ContainerCreateOptions(autoRemove: managementFlags.remove, systemStart: managementFlags.systemStart) let client = ContainerClient() try await client.create(configuration: ck.0, options: options, kernel: ck.1, initImage: ck.2) diff --git a/Sources/ContainerCommands/Container/ContainerList.swift b/Sources/ContainerCommands/Container/ContainerList.swift index 53b110bfa..4312b861d 100644 --- a/Sources/ContainerCommands/Container/ContainerList.swift +++ b/Sources/ContainerCommands/Container/ContainerList.swift @@ -50,7 +50,7 @@ extension Application { } private func createHeader() -> [[String]] { - [["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "STARTED"]] + [["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "STARTED", "SYSTEM-START"]] } private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws { @@ -94,6 +94,7 @@ extension ContainerSnapshot { "\(self.configuration.resources.cpus)", "\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB", self.startedDate.map { ISO8601DateFormatter().string(from: $0) } ?? "", + (self.createOptions?.systemStart ?? false) ? "enabled" : "disabled", ] } } @@ -101,12 +102,14 @@ extension ContainerSnapshot { struct PrintableContainer: Codable { let status: RuntimeStatus let configuration: ContainerConfiguration + let createOptions: ContainerCreateOptions? let networks: [Attachment] let startedDate: Date? init(_ container: ContainerSnapshot) { self.status = container.status self.configuration = container.configuration + self.createOptions = container.createOptions self.networks = container.networks self.startedDate = container.startedDate } diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index c83fbf790..e68320223 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -109,7 +109,7 @@ extension Application { progress.set(description: "Starting container") - let options = ContainerCreateOptions(autoRemove: managementFlags.remove) + let options = ContainerCreateOptions(autoRemove: managementFlags.remove, systemStart: managementFlags.systemStart) try await client.create( configuration: ck.0, options: options, diff --git a/Sources/ContainerCommands/System/SystemStart.swift b/Sources/ContainerCommands/System/SystemStart.swift index afb5074da..5c1eb0b4f 100644 --- a/Sources/ContainerCommands/System/SystemStart.swift +++ b/Sources/ContainerCommands/System/SystemStart.swift @@ -20,10 +20,13 @@ import ContainerPersistence import ContainerPlugin import ContainerizationError import Foundation +import Logging import TerminalProgress extension Application { public struct SystemStart: AsyncLoggableCommand { + private static let startTimeoutSeconds: Int32 = 5 + public static let configuration = CommandConfiguration( commandName: "start", abstract: "Start `container` services" @@ -53,6 +56,14 @@ extension Application { public init() {} public func run() async throws { + // TODO: update this to use the new logger + let log = Logger( + label: "com.apple.container.cli", + factory: { label in + StreamLogHandler.standardOutput(label: label) + } + ) + // Without the true path to the binary in the plist, `container-apiserver` won't launch properly. // TODO: Can we use the plugin loader to bootstrap the API server? let executableUrl = CommandLine.executablePathUrl @@ -104,10 +115,56 @@ extension Application { try? await installInitialFilesystem() } - guard await !kernelExists() else { - return + if await !kernelExists() { + try? await installDefaultKernel() + } + + // Start all the containers that have system-start enabled + log.info("starting containers", metadata: ["startTimeoutSeconds": "\(Self.startTimeoutSeconds)"]) + try await startContainers() + } + + /// Starts all containers that are configured to automatically start on system start. + /// + /// - Throws: `AggregateError` containing all errors encountered during container startup. + private func startContainers() async throws { + let client = ContainerClient() + let containers = try await client.list() + let systemStartContainers = containers.filter { $0.createOptions?.systemStart == true && $0.status != .running } + var errors: [any Error] = [] + + await withTaskGroup(of: (any Error)?.self) { group in + for container in systemStartContainers { + group.addTask { + do { + let io = try ProcessIO.create( + tty: container.configuration.initProcess.terminal, + interactive: false, + detach: true + ) + defer { try? io.close() } + + let process = try await client.bootstrap(id: container.id, stdio: io.stdio) + try await process.start() + try io.closeAfterStart() + print(container.id) + return nil + } catch { + return error + } + } + } + + for await error in group { + if let error { + errors.append(error) + } + } + } + + if !errors.isEmpty { + throw AggregateError(errors) } - try await installDefaultKernel() } private func installInitialFilesystem() async throws { diff --git a/Sources/ContainerResource/Container/Bundle.swift b/Sources/ContainerResource/Container/Bundle.swift index 999b13dae..0f0600c94 100644 --- a/Sources/ContainerResource/Container/Bundle.swift +++ b/Sources/ContainerResource/Container/Bundle.swift @@ -26,6 +26,7 @@ public struct Bundle: Sendable { private static let containerRootFsFilename = "rootfs.json" static let containerConfigFilename = "config.json" + static let containerOptionsConfigFilename = "options.json" /// The path to the bundle. public let path: URL @@ -75,6 +76,12 @@ public struct Bundle: Sendable { try load(path: self.path.appendingPathComponent(Self.containerConfigFilename)) } } + + public var options: ContainerCreateOptions { + get throws { + try load(path: self.path.appendingPathComponent(Self.containerOptionsConfigFilename)) + } + } } extension Bundle { diff --git a/Sources/ContainerResource/Container/ContainerCreateOptions.swift b/Sources/ContainerResource/Container/ContainerCreateOptions.swift index dd9da217a..e374d506d 100644 --- a/Sources/ContainerResource/Container/ContainerCreateOptions.swift +++ b/Sources/ContainerResource/Container/ContainerCreateOptions.swift @@ -16,11 +16,13 @@ public struct ContainerCreateOptions: Codable, Sendable { public let autoRemove: Bool + public let systemStart: Bool - public init(autoRemove: Bool) { + public init(autoRemove: Bool, systemStart: Bool) { self.autoRemove = autoRemove + self.systemStart = systemStart } - public static let `default` = ContainerCreateOptions(autoRemove: false) + public static let `default` = ContainerCreateOptions(autoRemove: false, systemStart: false) } diff --git a/Sources/ContainerResource/Container/ContainerSnapshot.swift b/Sources/ContainerResource/Container/ContainerSnapshot.swift index bae992423..fe196f845 100644 --- a/Sources/ContainerResource/Container/ContainerSnapshot.swift +++ b/Sources/ContainerResource/Container/ContainerSnapshot.swift @@ -23,6 +23,9 @@ public struct ContainerSnapshot: Codable, Sendable { /// The configuration of the container. public var configuration: ContainerConfiguration + /// The creation options of the container. + public var createOptions: ContainerCreateOptions? + /// Identifier of the container. public var id: String { configuration.id @@ -42,11 +45,13 @@ public struct ContainerSnapshot: Codable, Sendable { public init( configuration: ContainerConfiguration, + createOptions: ContainerCreateOptions? = nil, status: RuntimeStatus, networks: [Attachment], startedDate: Date? = nil ) { self.configuration = configuration + self.createOptions = createOptions self.status = status self.networks = networks self.startedDate = startedDate diff --git a/Sources/Services/ContainerAPIService/Client/Flags.swift b/Sources/Services/ContainerAPIService/Client/Flags.swift index 7d8f30626..35fc32bc0 100644 --- a/Sources/Services/ContainerAPIService/Client/Flags.swift +++ b/Sources/Services/ContainerAPIService/Client/Flags.swift @@ -230,6 +230,12 @@ public struct Flags { @Option(name: .long, help: "Set the runtime handler for the container (default: container-runtime-linux)") public var runtime: String? + + @Flag( + name: .customLong("systemstart"), + help: "Automatically start the container when the container system starts" + ) + public var systemStart: Bool = false } public struct Progress: ParsableArguments { diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift index 8a2009f37..cb5a14fe8 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift @@ -83,9 +83,11 @@ public actor ContainersService { do { let bundle = ContainerResource.Bundle(path: dir) let config = try bundle.configuration + let options = try bundle.options let state = ContainerState( snapshot: .init( configuration: config, + createOptions: options, status: .stopped, networks: [], startedDate: nil @@ -275,6 +277,7 @@ public actor ContainersService { let snapshot = ContainerSnapshot( configuration: configuration, + createOptions: options, status: .stopped, networks: [], startedDate: nil diff --git a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift index 3cc645696..b33c2dab0 100644 --- a/Sources/Services/ContainerSandboxService/Server/SandboxService.swift +++ b/Sources/Services/ContainerSandboxService/Server/SandboxService.swift @@ -400,6 +400,7 @@ public actor SandboxService { networks = ctr.attachments cs = ContainerSnapshot( configuration: ctr.config, + createOptions: cs?.createOptions, status: RuntimeStatus.running, networks: networks ) diff --git a/docs/command-reference.md b/docs/command-reference.md index d2aa31179..fe609e123 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -68,6 +68,7 @@ container run [] [ ...] * `-v, --volume `: Bind mount a volume into the container * `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support) * `--runtime`: Set the runtime handler for the container (default: container-runtime-linux) +* `--systemstart`: Automatically start the container when the container system starts **Registry Options** @@ -223,6 +224,7 @@ container create [] [ ...] * `-v, --volume `: Bind mount a volume into the container * `--virtualization`: Expose virtualization capabilities to the container (requires host and guest support) * `--runtime`: Set the runtime handler for the container (default: container-runtime-linux) +* `--systemstart`: Automatically start the container when the container system starts **Registry Options**