Files
container/Sources/Services/MachineAPIService/Client/MachineClient.swift
T
J Logan 0a48a1bdbf Remove machine user extension points, update machine docs. (#2322)
- Closes #2321.
- Removes image script and ORAS resource extension
  point handling for container machine user setup.
- Replaces image script resource with hardcoded script.
- Adds more flexible user setup options to
  `container machine`.
- Updates container machine documentation to clearly
  identify convenience-isolation tradeoffs.
2026-09-30 16:57:10 -07:00

312 lines
11 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 ContainerAPIClient
import ContainerPersistence
import ContainerResource
import ContainerXPC
import ContainerizationError
import ContainerizationOCI
import Foundation
import TerminalProgress
/// A client for interacting with the container machine API server.
public struct MachineClient: Sendable {
public static let serviceIdentifier = "com.apple.container.core.machine-apiserver"
public static func machineConfigFromFlags(
id: String,
image: String,
management: Flags.MachineManagement,
user: Flags.MachineUser,
registry: Flags.Registry,
imageFetch: Flags.ImageFetch,
containerSystemConfig: ContainerSystemConfig,
progressUpdate: @escaping ProgressUpdateHandler
) async throws -> MachineConfiguration {
var requestedPlatform = Parser.platform(os: management.os, arch: management.arch)
// Prefer --platform
if let platform = management.platform {
requestedPlatform = try Parser.platform(from: platform)
}
let scheme = try RequestScheme(registry.scheme)
await progressUpdate([
.setDescription("Fetching image"),
.setItemsName("blobs"),
])
let taskManager = ProgressTaskCoordinator()
let fetchTask = await taskManager.startTask()
let img = try await ClientImage.fetch(
reference: image,
platform: requestedPlatform,
scheme: scheme,
containerSystemConfig: containerSystemConfig,
progressUpdate: ProgressTaskCoordinator.handler(for: fetchTask, from: progressUpdate),
maxConcurrentDownloads: imageFetch.maxConcurrentDownloads
)
// Unpack a fetched image before use
await progressUpdate([
.setDescription("Unpacking image"),
.setItemsName("entries"),
])
let unpackTask = await taskManager.startTask()
try await img.getCreateSnapshot(
platform: requestedPlatform,
progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: progressUpdate))
let (username, uid, gid) = try Parser.userAccount(
user: user.user, uid: user.uid, gid: user.gid,
defaultUsername: NSUserName(), defaultUID: getuid(), defaultGID: getgid())
let userSetup = UserSetup(
username: username,
uid: uid,
gid: gid,
home: user.home)
let config = try MachineConfiguration(
id: id,
image: img.description,
platform: requestedPlatform,
userSetup: userSetup)
return config
}
private let xpcClient: XPCClient
public init() {
self.xpcClient = XPCClient(service: Self.serviceIdentifier)
}
@discardableResult
private func xpcSend(
message: XPCMessage,
timeout: Duration? = .seconds(10)
) async throws -> XPCMessage {
try await xpcClient.send(message, responseTimeout: timeout)
}
/// List container machines
public func list() async throws -> [MachineSnapshot] {
do {
let request = XPCMessage(route: MachineRoutes.listMachine.rawValue)
let response = try await xpcSend(
message: request,
timeout: nil
)
let data = response.dataNoCopy(key: MachineKeys.machines.rawValue)
guard let data else {
return []
}
return try JSONDecoder().decode([MachineSnapshot].self, from: data)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to list container machines",
cause: error
)
}
}
/// Create a new container machine with the given configuration
public func create(
configuration: MachineConfiguration,
bootConfig: MachineConfig,
) async throws {
do {
let request = XPCMessage(route: MachineRoutes.createMachine.rawValue)
let config = try JSONEncoder().encode(configuration)
request.set(key: MachineKeys.machineConfig.rawValue, value: config)
let bootData = try JSONEncoder().encode(bootConfig)
request.set(key: MachineKeys.bootConfig.rawValue, value: bootData)
let _ = try await xpcSend(message: request, timeout: nil)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to create container machine",
cause: error
)
}
}
/// Delete the container machine along with any resources.
public func delete(id: String) async throws {
do {
let request = XPCMessage(route: MachineRoutes.deleteMachine.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let _ = try await xpcSend(message: request, timeout: .seconds(60))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to delete container machine",
cause: error
)
}
}
/// Get the default container machine.
public func getDefault() async throws -> String? {
do {
let request = XPCMessage(route: MachineRoutes.getDefault.rawValue)
let response = try await xpcSend(message: request)
let id = response.string(key: MachineKeys.id.rawValue)
guard let id else {
return nil
}
return id
} catch {
throw ContainerizationError(
.internalError,
message: "failed to get the default container machine",
cause: error
)
}
}
/// Set a default container machine.
public func setDefault(id: String) async throws {
do {
let request = XPCMessage(route: MachineRoutes.setDefault.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let _ = try await xpcSend(message: request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to set a default container machine",
cause: error
)
}
}
/// Boot a container machine.
public func boot(id: String?, dynamicEnv: [String: String] = [:]) async throws -> MachineSnapshot {
do {
let request = XPCMessage(route: MachineRoutes.bootMachine.rawValue)
if let id {
request.set(key: MachineKeys.id.rawValue, value: id)
}
let dynamicEnvData = try JSONEncoder().encode(dynamicEnv)
request.set(key: MachineKeys.dynamicEnv.rawValue, value: dynamicEnvData)
let response = try await xpcSend(message: request, timeout: nil)
guard let data = response.dataNoCopy(key: MachineKeys.snapshot.rawValue) else {
throw ContainerizationError(
.internalError,
message: "missing snapshot in response"
)
}
return try JSONDecoder().decode(MachineSnapshot.self, from: data)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to boot container machine",
cause: error
)
}
}
/// Stop a running container machine.
public func stop(id: String) async throws {
do {
let request = XPCMessage(route: MachineRoutes.stopMachine.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let _ = try await xpcSend(message: request, timeout: nil)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to stop container machine",
cause: error
)
}
}
/// Set boot-time config for a container machine.
public func setConfig(id: String, bootConfig: MachineConfig) async throws {
do {
let request = XPCMessage(route: MachineRoutes.setConfig.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let data = try JSONEncoder().encode(bootConfig)
request.set(key: MachineKeys.bootConfig.rawValue, value: data)
let _ = try await xpcSend(message: request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to set container machine config",
cause: error
)
}
}
/// Inspect a container machine and return its snapshot.
public func inspect(id: String) async throws -> MachineSnapshot {
do {
let request = XPCMessage(route: MachineRoutes.inspectMachine.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let response = try await xpcSend(message: request, timeout: nil)
guard let data = response.dataNoCopy(key: MachineKeys.snapshot.rawValue) else {
throw ContainerizationError(
.internalError,
message: "missing snapshot in response"
)
}
return try JSONDecoder().decode(MachineSnapshot.self, from: data)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to inspect container machine",
cause: error
)
}
}
/// Get the log file handles for a container machine.
public func logs(id: String) async throws -> [FileHandle] {
do {
let request = XPCMessage(route: MachineRoutes.logsMachine.rawValue)
request.set(key: MachineKeys.id.rawValue, value: id)
let response = try await xpcSend(message: request)
let fds = response.fileHandles(key: MachineKeys.logs.rawValue)
guard let fds else {
throw ContainerizationError(
.internalError,
message: "no log fds returned"
)
}
return fds
} catch {
throw ContainerizationError(
.internalError,
message: "failed to get logs for container machine \(id)",
cause: error
)
}
}
}