Skip to content
Open
4 changes: 2 additions & 2 deletions Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ extension Application {
// If they changed, stop and delete the existing builder
try await client.stop(id: existingContainer.id)
try await client.delete(id: existingContainer.id)
case .stopped:
case .stopped, .bootstrapped:
// If the builder is stopped and matches our requirements, start it
// Otherwise, delete it and create a new one
guard imageChanged || cpuChanged || memChanged || envChanged || dnsChanged else {
Expand All @@ -186,7 +186,7 @@ extension Application {
.invalidState,
message: "builder is stopping, please wait until it is fully stopped before proceeding"
)
case .unknown:
case .unknown, .restarting:
break
}
}
Expand Down
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, restartPolicy: managementFlags.restart)
let client = ContainerClient()
try await client.create(configuration: ck.0, options: options, kernel: ck.1, initImage: ck.2)

Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Container/ContainerList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ extension ContainerSnapshot {
}

struct PrintableContainer: Codable {
let status: RuntimeStatus
let status: ContainerStatus
let configuration: ContainerConfiguration
let networks: [Attachment]
let startedDate: Date?
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, restartPolicy: managementFlags.restart)
try await client.create(
configuration: ck.0,
options: options,
Expand Down
23 changes: 21 additions & 2 deletions Sources/ContainerResource/Container/ContainerCreateOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,32 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

public enum RestartPolicy: String, Sendable, Codable {
case no
case onFailure
case always
}

public struct ContainerCreateOptions: Codable, Sendable {
public let autoRemove: Bool
public let restartPolicy: RestartPolicy

public init(autoRemove: Bool) {
public init(autoRemove: Bool, restartPolicy: RestartPolicy) {
self.autoRemove = autoRemove
self.restartPolicy = restartPolicy
}

public static let `default` = ContainerCreateOptions(autoRemove: false, restartPolicy: .no)

enum CodingKeys: String, CodingKey {
case autoRemove
case restartPolicy
}

public static let `default` = ContainerCreateOptions(autoRemove: false)
public init(from decoder: Decoder) throws {
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.

Why was the decoder not being used before when it was just autoRemove?

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.

I actually have no idea :)
Maybe, wanted to keep the code as simple as possible at that time?

let container = try decoder.container(keyedBy: CodingKeys.self)

autoRemove = try container.decode(Bool.self, forKey: .autoRemove)
restartPolicy = try container.decodeIfPresent(RestartPolicy.self, forKey: .restartPolicy) ?? .no
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public struct ContainerListFilters: Sendable, Codable {
/// Filter by container IDs. If non-empty, only containers with matching IDs are returned.
public var ids: [String]
/// Filter by container status.
public var status: RuntimeStatus?
public var status: ContainerStatus?
/// Filter by labels. All specified labels must match.
public var labels: [String: String]

Expand All @@ -30,7 +30,7 @@ public struct ContainerListFilters: Sendable, Codable {

public init(
ids: [String] = [],
status: RuntimeStatus? = nil,
status: ContainerStatus? = nil,
labels: [String: String] = [:]
) {
self.ids = ids
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContainerResource/Container/ContainerSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public struct ContainerSnapshot: Codable, Sendable {
}

/// The runtime status of the container.
public var status: RuntimeStatus
public var status: ContainerStatus
/// Network interfaces attached to the sandbox that are provided to the container.
public var networks: [Attachment]
/// When the container was started.
public var startedDate: Date?

public init(
configuration: ContainerConfiguration,
status: RuntimeStatus,
status: ContainerStatus,
networks: [Attachment],
startedDate: Date? = nil
) {
Expand Down
33 changes: 33 additions & 0 deletions Sources/ContainerResource/Container/ContainerStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
// 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

/// Runtime status for a sandbox or container.
public enum ContainerStatus: String, CaseIterable, Sendable, Codable {
/// The object is in an unknown status.
case unknown
/// The object is currently stopped.
case stopped
/// The object is waiting to be restarted.
case restarting
/// The object is currently bootstrapped.
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 does the bootstrapped status mean? The container or sandbox is stopped but ready to be started?

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.

bootstrapped means (by itself) the container is bootstrapped (i.e., bootstrap function has been successfully executed on that container).

Before this update, the state was same stopped whether it is bootstrapped or not. The only difference was SandboxClient is allocated or not---I personally felt that somewhat weird, but left there.

But after adding restarting state, it becomes more weird, since container state should go restarting (waiting for backOff), then stopped (after bootstrap) then running`.

BTW, now we split the RuntimeStatus to ContainerStatus and SandboxStatus, so it only applies to containers.

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.

While writing this comment, I found that I missed setting the container restarting in scheduler lol.

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.

bootstrapped means (by itself) the container is bootstrapped (i.e., bootstrap function has been successfully executed on that container).

I understand that this has to do with the SandboxService and SandboxClient. Except, I have yet to understand what is exactly the point of those two things?

BTW, now we split the RuntimeStatus to ContainerStatus and SandboxStatus, so it only applies to containers.

So it seems like Containers and the Sandbox are two separate entities with different capabilities. Goes back to my question above and how they differentiate.

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.

Container and Sandbox are separate entities with different lifecycle (though they are tightly coupled).

Sandbox is more for managing the lifecycle of VM.

Sandbox is only alive after the Container successfully executed bootstrap.

case bootstrapped
/// The object is currently running.
case running
/// The object is currently stopping.
case stopping
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import Foundation

/// Runtime status for a sandbox or container.
public enum RuntimeStatus: String, CaseIterable, Sendable, Codable {
public enum SandboxStatus: String, CaseIterable, Sendable, Codable {
/// The object is in an unknown status.
case unknown
/// The object is currently stopped.
Expand Down
6 changes: 4 additions & 2 deletions Sources/Helpers/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension APIServer {
var routes = [XPCRoute: XPCServer.RouteHandler]()
let pluginLoader = try initializePluginLoader(log: log)
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes)
let containersService = try initializeContainersService(
let containersService = try await initializeContainersService(
pluginLoader: pluginLoader,
log: log,
routes: &routes
Expand Down Expand Up @@ -261,7 +261,7 @@ extension APIServer {
routes[XPCRoute.getDefaultKernel] = harness.getDefaultKernel
}

private func initializeContainersService(pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws -> ContainersService {
private func initializeContainersService(pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) async throws -> ContainersService {
log.info("initializing containers service")

let service = try ContainersService(
Expand All @@ -288,6 +288,8 @@ extension APIServer {
routes[XPCRoute.containerDiskUsage] = harness.diskUsage
routes[XPCRoute.containerExport] = harness.export

async let _ = try service.runRestartScheduler()

return service
}

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 @@ -15,9 +15,12 @@
//===----------------------------------------------------------------------===//

import ArgumentParser
import ContainerResource
import ContainerizationError
import Foundation

extension RestartPolicy: ExpressibleByArgument {}

public struct Flags {
public struct Logging: ParsableArguments {
public init() {}
Expand Down Expand Up @@ -304,6 +307,9 @@ public struct Flags {
@Flag(name: [.customLong("rm"), .long], help: "Remove the container after it stops")
public var remove = false

@Option(name: .long, help: "Restart policy when the container exits")
public var restart: RestartPolicy = .no

@Flag(name: .long, help: "Enable Rosetta in the container")
public var rosetta = false

Expand Down
Loading