Skip to content

Commit 6f94498

Browse files
committed
feat: add Tier 2 fork APIs (CHAOS-1319/1321/1322/1323/1324)
- Flags.ProcessBase: minimal subset without -e/-u/-w/-i/-t short flags - ContainerLogOptions: since/timestamps parameters for logs API - HealthStatus enum + health field on ContainerSnapshot - ContainerEvent type + event recording in lifecycle methods + events() API - RestartPolicy type + restartPolicy on ContainerCreateOptions + --restart flag
1 parent ac52ced commit 6f94498

13 files changed

Lines changed: 300 additions & 13 deletions

File tree

Sources/APIServer/APIServer+Start.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ extension APIServer {
285285
routes[XPCRoute.containerStats] = harness.stats
286286
routes[XPCRoute.containerDiskUsage] = harness.diskUsage
287287
routes[XPCRoute.containerExport] = harness.export
288+
routes[XPCRoute.containerEvent] = harness.events
288289

289290
return service
290291
}

Sources/ContainerCommands/Container/ContainerRun.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ extension Application {
104104

105105
progress.set(description: "Starting container")
106106

107-
let options = ContainerCreateOptions(autoRemove: managementFlags.remove)
107+
let restartPolicy = Self.parseRestartPolicy(managementFlags.restart)
108+
let options = ContainerCreateOptions(
109+
autoRemove: managementFlags.remove,
110+
restartPolicy: restartPolicy
111+
)
108112
try await client.create(
109113
configuration: ck.0,
110114
options: options,
@@ -172,5 +176,18 @@ extension Application {
172176
}
173177
throw ArgumentParser.ExitCode(exitCode)
174178
}
179+
180+
static func parseRestartPolicy(_ raw: String?) -> RestartPolicy? {
181+
guard let raw, !raw.isEmpty else { return nil }
182+
if raw == "no" { return .none }
183+
if raw == "always" { return RestartPolicy(mode: .always) }
184+
if raw == "unless-stopped" { return RestartPolicy(mode: .unlessStopped) }
185+
if raw.hasPrefix("on-failure") {
186+
let parts = raw.split(separator: ":", maxSplits: 1)
187+
let retries = parts.count > 1 ? Int(parts[1]) ?? 0 : 0
188+
return RestartPolicy(mode: .onFailure, maxRetries: retries)
189+
}
190+
return nil
191+
}
175192
}
176193
}

Sources/ContainerResource/Container/ContainerCreateOptions.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
public struct ContainerCreateOptions: Codable, Sendable {
18-
/// Remove the container and wipe out its data on container stop
1918
public let autoRemove: Bool
20-
/// Override the rootFs with this one other than the image-cloned version
2119
public let rootFsOverride: Filesystem?
20+
public let restartPolicy: RestartPolicy?
2221

23-
public init(autoRemove: Bool, rootFsOverride: Filesystem? = nil) {
22+
public init(autoRemove: Bool, rootFsOverride: Filesystem? = nil, restartPolicy: RestartPolicy? = nil) {
2423
self.autoRemove = autoRemove
2524
self.rootFsOverride = rootFsOverride
25+
self.restartPolicy = restartPolicy
2626
}
2727

2828
public static let `default` = ContainerCreateOptions(autoRemove: false)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
public struct ContainerEvent: Codable, Sendable, Equatable {
20+
public enum Action: String, Codable, Sendable, Equatable {
21+
case create
22+
case start
23+
case stop
24+
case die
25+
case destroy
26+
}
27+
28+
public let containerId: String
29+
public let action: Action
30+
public let timestamp: Date
31+
32+
public init(containerId: String, action: Action, timestamp: Date = Date()) {
33+
self.containerId = containerId
34+
self.action = action
35+
self.timestamp = timestamp
36+
}
37+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
public struct ContainerLogOptions: Sendable, Codable {
20+
public let since: Date?
21+
public let timestamps: Bool
22+
23+
public static let `default` = ContainerLogOptions(since: nil, timestamps: false)
24+
25+
public init(since: Date? = nil, timestamps: Bool = false) {
26+
self.since = since
27+
self.timestamps = timestamps
28+
}
29+
}

Sources/ContainerResource/Container/ContainerSnapshot.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,21 @@ public struct ContainerSnapshot: Codable, Sendable {
4343
/// Populated when the container transitions to ``RuntimeStatus/stopped``.
4444
/// `nil` if the container has never exited or its exit was not captured.
4545
public var lastExitCode: Int32?
46+
public var health: HealthStatus?
4647

4748
public init(
4849
configuration: ContainerConfiguration,
4950
status: RuntimeStatus,
5051
networks: [Attachment],
5152
startedDate: Date? = nil,
52-
lastExitCode: Int32? = nil
53+
lastExitCode: Int32? = nil,
54+
health: HealthStatus? = nil
5355
) {
5456
self.configuration = configuration
5557
self.status = status
5658
self.networks = networks
5759
self.startedDate = startedDate
5860
self.lastExitCode = lastExitCode
61+
self.health = health
5962
}
6063
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
public enum HealthStatus: String, CaseIterable, Sendable, Codable {
20+
case none
21+
case starting
22+
case healthy
23+
case unhealthy
24+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
19+
public struct RestartPolicy: Codable, Sendable, Equatable {
20+
public enum Mode: String, Codable, Sendable, Equatable {
21+
case no
22+
case always
23+
case onFailure = "on-failure"
24+
case unlessStopped = "unless-stopped"
25+
}
26+
27+
public let mode: Mode
28+
public let maxRetries: Int
29+
30+
public static let none = RestartPolicy(mode: .no, maxRetries: 0)
31+
32+
public init(mode: Mode, maxRetries: Int = 0) {
33+
self.mode = mode
34+
self.maxRetries = maxRetries
35+
}
36+
}

Sources/Services/ContainerAPIService/Client/ContainerClient.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,18 @@ public struct ContainerClient: Sendable {
262262
}
263263
}
264264

265-
/// Get the log file handles for a container.
266265
public func logs(id: String) async throws -> [FileHandle] {
266+
try await logs(id: id, options: .default)
267+
}
268+
269+
public func logs(id: String, options: ContainerLogOptions) async throws -> [FileHandle] {
267270
do {
268271
let request = XPCMessage(route: .containerLogs)
269272
request.set(key: .id, value: id)
273+
if let since = options.since {
274+
request.set(key: .logSince, value: since)
275+
}
276+
request.set(key: .logTimestamps, value: options.timestamps)
270277

271278
let response = try await xpcClient.send(request)
272279
let fds = response.fileHandles(key: .logs)
@@ -349,4 +356,21 @@ public struct ContainerClient: Sendable {
349356
)
350357
}
351358
}
359+
360+
public func events() async throws -> [ContainerEvent] {
361+
do {
362+
let request = XPCMessage(route: .containerEvent)
363+
let response = try await xpcClient.send(request)
364+
guard let data = response.dataNoCopy(key: .containerEvent) else {
365+
return []
366+
}
367+
return try JSONDecoder().decode([ContainerEvent].self, from: data)
368+
} catch {
369+
throw ContainerizationError(
370+
.internalError,
371+
message: "failed to get container events",
372+
cause: error
373+
)
374+
}
375+
}
352376
}

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ public struct Flags {
3030
public var debug = false
3131
}
3232

33+
/// Minimal subset of process-related flags that don't claim the common
34+
/// short flags (`-e`, `-u`, `-w`, `-i`, `-t`). Downstream tools like
35+
/// Container-Compose can `@OptionGroup` this instead of `Flags.Process`
36+
/// to reclaim those short names for their own compose-specific options.
37+
public struct ProcessBase: ParsableArguments {
38+
public init() {}
39+
40+
public init(cwd: String?, envFile: [String]) {
41+
self.cwd = cwd
42+
self.envFile = envFile
43+
}
44+
45+
@Option(
46+
name: .long,
47+
help: .init(
48+
"Set the initial working directory inside the container",
49+
valueName: "dir"
50+
)
51+
)
52+
public var cwd: String?
53+
54+
@Option(
55+
name: .long,
56+
help: "Read in a file of environment variables (key=value format, ignores # comments and blank lines)"
57+
)
58+
public var envFile: [String] = []
59+
}
60+
3361
public struct Process: ParsableArguments {
3462
public init() {}
3563

@@ -341,6 +369,9 @@ public struct Flags {
341369

342370
@Option(name: [.customLong("volume"), .short], help: "Bind mount a volume into the container")
343371
public var volumes: [String] = []
372+
373+
@Option(name: .long, help: "Restart policy (no, always, on-failure[:max-retries], unless-stopped)")
374+
public var restart: String?
344375
}
345376

346377
public struct Progress: ParsableArguments {

0 commit comments

Comments
 (0)