Skip to content
Draft
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
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Container/ContainerCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion Sources/ContainerCommands/Container/ContainerList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]]
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure it's the best to print "SYSTEM-START" here..

Copy link
Contributor Author

@saehejkang saehejkang Feb 13, 2026

Choose a reason for hiding this comment

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

I was mulling on the idea of adding this to the print output. I feel users should have the ability to know if a container will be started automatically on system startup. It might be worthwhile adding a --verbose option (like image list), where this field will be added to the output (default output will not included it).

}

private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws {
Expand Down Expand Up @@ -94,19 +94,22 @@ 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",
]
}
}

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
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Container/ContainerRun.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
63 changes: 60 additions & 3 deletions Sources/ContainerCommands/System/SystemStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

We might be able to move starting containers to the API server, near loadAtBoot? Then, we can avoid changing ContainerSnapshot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can take a look at this but ContainerSnapshot was changed just so the SYSTEM-START field could be outputted.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh got it. We need to change ContainerSnapshot anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

NVM this comment, I was testing AI based PR review yesterday and it corrupted my brain :)

let client = ContainerClient()
let containers = try await client.list()
let systemStartContainers = containers.filter { $0.createOptions?.systemStart == true && $0.status != .running }
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be $0.status == .stopped?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

either or in this case

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 {
Expand Down
7 changes: 7 additions & 0 deletions Sources/ContainerResource/Container/Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@

public struct ContainerCreateOptions: Codable, Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

@jglogan @dcantah How do you think merging this into ContainerConfiguration.label?

For now, there's no way to inform the users of this create options.
label might be a good choice to expose it without drastic changing?

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)

}
5 changes: 5 additions & 0 deletions Sources/ContainerResource/Container/ContainerSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Sources/Services/ContainerAPIService/Client/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

@saehejkang saehejkang Feb 12, 2026

Choose a reason for hiding this comment

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

I neglected to use autoStart because containers are automatically spun up with run and create. It will cause confusion, as that is a totally different process from what this flag should inherently be doing.

}

public struct Progress: ParsableArguments {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -275,6 +277,7 @@ public actor ContainersService {

let snapshot = ContainerSnapshot(
configuration: configuration,
createOptions: options,
status: .stopped,
networks: [],
startedDate: nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ public actor SandboxService {
networks = ctr.attachments
cs = ContainerSnapshot(
configuration: ctr.config,
createOptions: cs?.createOptions,
status: RuntimeStatus.running,
networks: networks
)
Expand Down
2 changes: 2 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ container run [<options>] <image> [<arguments> ...]
* `-v, --volume <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
Copy link
Contributor

Choose a reason for hiding this comment

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

--system-start would be better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Will make that change before this gets merged.


**Registry Options**

Expand Down Expand Up @@ -223,6 +224,7 @@ container create [<options>] <image> [<arguments> ...]
* `-v, --volume <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**

Expand Down