Skip to content
Open
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
61 changes: 47 additions & 14 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Foundation
import TerminalProgress

public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"
public static let serviceIdentifier = "com.apple.container.apiserver"

/// Identifier of the container.
public var id: String {
Expand All @@ -43,16 +43,46 @@ public struct ClientContainer: Sendable, Codable {
/// Network allocated to the container.
public let networks: [Attachment]

/// Optional XPC client for connection reuse across operations.
private let xpcClient: XPCClient?

package init(configuration: ContainerConfiguration) {
self.configuration = configuration
self.status = .stopped
self.networks = []
self.xpcClient = nil
}

init(snapshot: ContainerSnapshot) {
public init(snapshot: ContainerSnapshot, xpcClient: XPCClient? = nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Having the caller provide a snapshot to a public method feels a little strange. I'd expect them to be able to create a ClientContainer using just a container ID.

Since we're now using snapshot as the return type for everything, do we really need to store configuration, status, and networks between calls?

self.configuration = snapshot.configuration
self.status = snapshot.status
self.networks = snapshot.networks
self.xpcClient = xpcClient
}

private func client() -> XPCClient {
xpcClient ?? Self.newXPCClient()
}

enum CodingKeys: String, CodingKey {
case configuration
case status
case networks
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.configuration = try container.decode(ContainerConfiguration.self, forKey: .configuration)
self.status = try container.decode(RuntimeStatus.self, forKey: .status)
self.networks = try container.decode([Attachment].self, forKey: .networks)
self.xpcClient = nil
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(configuration, forKey: .configuration)
try container.encode(status, forKey: .status)
try container.encode(networks, forKey: .networks)
}
}

Expand All @@ -74,7 +104,7 @@ extension ClientContainer {
configuration: ContainerConfiguration,
options: ContainerCreateOptions = .default,
kernel: Kernel
) async throws -> ClientContainer {
) async throws -> ContainerSnapshot {
do {
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)
Expand All @@ -87,7 +117,11 @@ extension ClientContainer {
request.set(key: .containerOptions, value: odata)

try await xpcSend(client: client, message: request)
return ClientContainer(configuration: configuration)
return ContainerSnapshot(
configuration: configuration,
status: .stopped,
networks: []
)
} catch {
throw ContainerizationError(
.internalError,
Expand All @@ -97,7 +131,7 @@ extension ClientContainer {
}
}

public static func list() async throws -> [ClientContainer] {
public static func list() async throws -> [ContainerSnapshot] {
do {
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)
Expand All @@ -111,8 +145,7 @@ extension ClientContainer {
guard let data else {
return []
}
let configs = try JSONDecoder().decode([ContainerSnapshot].self, from: data)
return configs.map { ClientContainer(snapshot: $0) }
return try JSONDecoder().decode([ContainerSnapshot].self, from: data)
} catch {
throw ContainerizationError(
.internalError,
Expand All @@ -123,9 +156,9 @@ extension ClientContainer {
}

/// Get the container for the provided id.
public static func get(id: String) async throws -> ClientContainer {
public static func get(id: String) async throws -> ContainerSnapshot {
let containers = try await list()
guard let container = containers.first(where: { $0.id == id }) else {
guard let container = containers.first(where: { $0.configuration.id == id }) else {
throw ContainerizationError(
.notFound,
message: "get failed: container \(id) not found"
Expand Down Expand Up @@ -176,7 +209,7 @@ extension ClientContainer {
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
let client = self.client()
try await client.send(request)
} catch {
throw ContainerizationError(
Expand All @@ -190,7 +223,7 @@ extension ClientContainer {
/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = Self.newXPCClient()
let client = self.client()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
Expand All @@ -209,7 +242,7 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = Self.newXPCClient()
let client = self.client()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
Expand Down Expand Up @@ -268,7 +301,7 @@ extension ClientContainer {

public func logs() async throws -> [FileHandle] {
do {
let client = Self.newXPCClient()
let client = self.client()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All @@ -295,7 +328,7 @@ extension ClientContainer {
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let client = self.client()
let response: XPCMessage
do {
response = try await client.send(request)
Expand Down
3 changes: 2 additions & 1 deletion Sources/ContainerCommands/BuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ extension Application {
group.addTask { [vsockPort, cpus, memory] in
while true {
do {
let container = try await ClientContainer.get(id: "buildkit")
let snapshot = try await ClientContainer.get(id: "buildkit")
let container = ClientContainer(snapshot: snapshot)
let fh = try await container.dial(vsockPort)

let threadGroup: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
Expand Down
5 changes: 3 additions & 2 deletions Sources/ContainerCommands/Builder/BuilderDelete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ extension Application {

public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
if container.status != .stopped {
let snapshot = try await ClientContainer.get(id: "buildkit")
let container = ClientContainer(snapshot: snapshot)
if snapshot.status != .stopped {
guard force else {
throw ContainerizationError(.invalidState, message: "BuildKit container is not stopped, use --force to override")
}
Expand Down
15 changes: 9 additions & 6 deletions Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ extension Application {

let builderPlatform = ContainerizationOCI.Platform(arch: "arm64", os: "linux", variant: "v8")

let existingContainer = try? await ClientContainer.get(id: "buildkit")
if let existingContainer {
let existingImage = existingContainer.configuration.image.reference
let existingResources = existingContainer.configuration.resources
let existingSnapshot = try? await ClientContainer.get(id: "buildkit")
if let existingSnapshot {
let existingImage = existingSnapshot.configuration.image.reference
let existingResources = existingSnapshot.configuration.resources

// Check if we need to recreate the builder due to different image
let imageChanged = existingImage != builderImage
Expand All @@ -113,7 +113,9 @@ extension Application {
return false
}()

switch existingContainer.status {
let existingContainer = ClientContainer(snapshot: existingSnapshot)

switch existingSnapshot.status {
case .running:
guard imageChanged || cpuChanged || memChanged else {
// If image, mem and cpu are the same, continue using the existing builder
Expand Down Expand Up @@ -228,11 +230,12 @@ extension Application {
.setDescription("Starting BuildKit container")
])

let container = try await ClientContainer.create(
let snapshot = try await ClientContainer.create(
configuration: config,
options: .default,
kernel: kernel
)
let container = ClientContainer(snapshot: snapshot)

try await container.startBuildKit(progressUpdate, taskManager)
}
Expand Down
20 changes: 10 additions & 10 deletions Sources/ContainerCommands/Builder/BuilderStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ extension Application {

public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
try printContainers(containers: [container], format: format)
let snapshot = try await ClientContainer.get(id: "buildkit")
try printContainers(snapshots: [snapshot], format: format)
} catch {
if error is ContainerizationError {
if (error as? ContainerizationError)?.code == .notFound && !quiet {
Expand All @@ -59,9 +59,9 @@ extension Application {
[["ID", "IMAGE", "STATE", "ADDR", "CPUS", "MEMORY"]]
}

private func printContainers(containers: [ClientContainer], format: ListFormat) throws {
private func printContainers(snapshots: [ContainerSnapshot], format: ListFormat) throws {
if format == .json {
let printables = containers.map {
let printables = snapshots.map {
PrintableContainer($0)
}
let data = try JSONEncoder().encode(printables)
Expand All @@ -71,15 +71,15 @@ extension Application {
}

if self.quiet {
containers
snapshots
.filter { $0.status == .running }
.forEach { print($0.id) }
.forEach { print($0.configuration.id) }
return
}

var rows = createHeader()
for container in containers {
rows.append(container.asRow)
for snapshot in snapshots {
rows.append(snapshot.asRow)
}

let formatter = TableOutput(rows: rows)
Expand All @@ -88,10 +88,10 @@ extension Application {
}
}

extension ClientContainer {
extension ContainerSnapshot {
fileprivate var asRow: [String] {
[
self.id,
self.configuration.id,
self.configuration.image.reference,
self.status.rawValue,
self.networks.compactMap { try? CIDRAddress($0.address).address.description }.joined(separator: ","),
Expand Down
3 changes: 2 additions & 1 deletion Sources/ContainerCommands/Builder/BuilderStop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ extension Application {

public func run() async throws {
do {
let container = try await ClientContainer.get(id: "buildkit")
let snapshot = try await ClientContainer.get(id: "buildkit")
let container = ClientContainer(snapshot: snapshot)
try await container.stop()
} catch {
if error is ContainerizationError {
Expand Down
6 changes: 3 additions & 3 deletions Sources/ContainerCommands/Container/ContainerCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ extension Application {
)

let options = ContainerCreateOptions(autoRemove: managementFlags.remove)
let container = try await ClientContainer.create(configuration: ck.0, options: options, kernel: ck.1)
let snapshot = try await ClientContainer.create(configuration: ck.0, options: options, kernel: ck.1)

if !self.managementFlags.cidfile.isEmpty {
let path = self.managementFlags.cidfile
let data = container.id.data(using: .utf8)
let data = snapshot.configuration.id.data(using: .utf8)
var attributes = [FileAttributeKey: Any]()
attributes[.posixPermissions] = 0o644
let success = FileManager.default.createFile(
Expand All @@ -96,7 +96,7 @@ extension Application {
}
progress.finish()

print(container.id)
print(snapshot.configuration.id)
}
}
}
28 changes: 16 additions & 12 deletions Sources/ContainerCommands/Container/ContainerDelete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import ArgumentParser
import ContainerClient
import ContainerXPC
import ContainerizationError
import Foundation

Expand Down Expand Up @@ -54,22 +55,22 @@ extension Application {

public mutating func run() async throws {
let set = Set<String>(containerIds)
var containers = [ClientContainer]()
var snapshots = [ContainerSnapshot]()

if all {
containers = try await ClientContainer.list()
snapshots = try await ClientContainer.list()
} else {
let ctrs = try await ClientContainer.list()
containers = ctrs.filter { c in
set.contains(c.id)
snapshots = ctrs.filter { c in
set.contains(c.configuration.id)
}
// If one of the containers requested isn't present, let's throw. We don't need to do
// this for --all as --all should be perfectly usable with no containers to remove; otherwise,
// it'd be quite clunky.
if containers.count != set.count {
if snapshots.count != set.count {
let missing = set.filter { id in
!containers.contains { c in
c.id == id
!snapshots.contains { c in
c.configuration.id == id
}
}
throw ContainerizationError(
Expand All @@ -82,23 +83,26 @@ extension Application {
var failed = [String]()
let force = self.force
let all = self.all
let sharedClient = XPCClient(service: ClientContainer.serviceIdentifier)

try await withThrowingTaskGroup(of: String?.self) { group in
for container in containers {
for snapshot in snapshots {
group.addTask {
do {
if container.status == .running && !force {
if snapshot.status == .running && !force {
guard all else {
throw ContainerizationError(.invalidState, message: "container is running")
}
return nil // Skip running container when using --all
}

let container = ClientContainer(snapshot: snapshot, xpcClient: sharedClient)
try await container.delete(force: force)
print(container.id)
print(snapshot.configuration.id)
return nil
} catch {
log.error("failed to delete container \(container.id): \(error)")
return container.id
log.error("failed to delete container \(snapshot.configuration.id): \(error)")
return snapshot.configuration.id
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/ContainerCommands/Container/ContainerExec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ extension Application {

public func run() async throws {
var exitCode: Int32 = 127
let container = try await ClientContainer.get(id: containerId)
try ensureRunning(container: container)
let snapshot = try await ClientContainer.get(id: containerId)
let container = ClientContainer(snapshot: snapshot)
try ensureRunning(snapshot: snapshot)

let stdin = self.processFlags.interactive
let tty = self.processFlags.tty

var config = container.configuration.initProcess
var config = snapshot.configuration.initProcess
config.executable = arguments.first!
config.arguments = [String](self.arguments.dropFirst())
config.terminal = tty
Expand Down
Loading