mirror of
https://github.com/apple/container.git
synced 2026-09-10 09:45:39 +00:00
Continue documenting public surface (#25)
Signed-off-by: Danny Canter <danny_canter@apple.com>
This commit is contained in:
@@ -38,6 +38,15 @@ Applications built using the package will run on macOS Sequoia or later, but the
|
||||
|
||||
- Non-isolated container networking - with macOS Sequoia, containers on the same vmnet network cannot communicate with each other
|
||||
|
||||
## Example Usage
|
||||
|
||||
For examples of how to use some of the libraries surface, the cctl executable is a good start. This tools primary job is as a playground to trial out the API. It contains commands that exercise some of the core functionality of the various products, such as:
|
||||
|
||||
1. [Manipulating OCI images](./Sources/cctl/ImageCommand.swift)
|
||||
2. [Logging in to container registries](./Sources/cctl/LoginCommand.swift)
|
||||
3. [Creating root filesystem blocks](./Sources/cctl/RootfsCommand.swift)
|
||||
4. [Running simple Linux containers](./Sources/cctl/RunCommand.swift)
|
||||
|
||||
## Linux kernel
|
||||
|
||||
A Linux kernel is required for spawning light weight virtual machines on macOS.
|
||||
|
||||
@@ -42,12 +42,15 @@ public struct Vminitd: Sendable {
|
||||
self.client = .init(connection: connection, group: group)
|
||||
}
|
||||
|
||||
/// Close the connection to the guest agent.
|
||||
public func close() async throws {
|
||||
try await client.close()
|
||||
}
|
||||
}
|
||||
|
||||
extension Vminitd: VirtualMachineAgent {
|
||||
/// Perform the standard guest setup necessary for vminitd to be able to
|
||||
/// run containers.
|
||||
public func standardSetup() async throws {
|
||||
try await up(name: "lo")
|
||||
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
|
||||
/// The core protocol container implementations must implement.
|
||||
public protocol Container {
|
||||
/// ID for the container.
|
||||
var id: String { get }
|
||||
|
||||
/// The amount of cpus assigned to the container.
|
||||
var cpus: Int { get }
|
||||
/// The memory in bytes assigned to the container.
|
||||
var memoryInBytes: UInt64 { get }
|
||||
|
||||
/// The network interfaces assigned to the container.
|
||||
var interfaces: [any Interface] { get }
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@
|
||||
/// DNS configuration for a container. The values will be used to
|
||||
/// construct /etc/resolv.conf for a given container.
|
||||
public struct DNS: Sendable {
|
||||
/// The set of default nameservers to use if none are provided
|
||||
/// in the constructor.
|
||||
public static let defaultNameservers = ["1.1.1.1"]
|
||||
|
||||
/// The nameservers a container should use.
|
||||
public var nameservers: [String]
|
||||
/// The DNS domain to use.
|
||||
public var domain: String?
|
||||
/// The DNS search domains to use.
|
||||
public var searchDomains: [String]
|
||||
/// The DNS options to use.
|
||||
public var options: [String]
|
||||
|
||||
public init(
|
||||
|
||||
@@ -29,15 +29,19 @@ import ContainerizationExtras
|
||||
|
||||
/// Type representing an OCI container image.
|
||||
public struct Image: Sendable {
|
||||
|
||||
private let contentStore: ContentStore
|
||||
/// The description for the image that comprises of its name and a reference to its root descriptor.
|
||||
public let description: Description
|
||||
|
||||
/// A description of the OCI image.
|
||||
public struct Description: Sendable {
|
||||
/// The string reference of the image.
|
||||
public let reference: String
|
||||
/// The descriptor identifying the image.
|
||||
public let descriptor: Descriptor
|
||||
/// The digest for the image.
|
||||
public var digest: String { descriptor.digest }
|
||||
/// The media type of the image.
|
||||
public var mediaType: String { descriptor.mediaType }
|
||||
|
||||
public init(reference: String, descriptor: Descriptor) {
|
||||
@@ -46,9 +50,13 @@ public struct Image: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// The descriptor for the image.
|
||||
public var descriptor: Descriptor { description.descriptor }
|
||||
/// The digest of the image.
|
||||
public var digest: String { description.digest }
|
||||
/// The media type of the image.
|
||||
public var mediaType: String { description.mediaType }
|
||||
/// The string reference for the image.
|
||||
public var reference: String { description.reference }
|
||||
|
||||
public init(description: Description, contentStore: ContentStore) {
|
||||
@@ -79,6 +87,8 @@ public struct Image: Sendable {
|
||||
return try content.decode()
|
||||
}
|
||||
|
||||
/// Returns the descriptor for the given platform. If it does not exist
|
||||
/// will throw a ContainerizationError with the code set to .invalidArgument.
|
||||
public func descriptor(for platform: Platform) async throws -> Descriptor {
|
||||
let index = try await self.index()
|
||||
let desc = index.manifests.first { $0.platform == platform }
|
||||
|
||||
@@ -297,6 +297,7 @@ public enum EXT4 {
|
||||
}
|
||||
|
||||
extension EXT4 {
|
||||
// `EXT4` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible, Sendable, Equatable {
|
||||
case notFound(_ path: String)
|
||||
case couldNotReadSuperBlock(_ path: String, _ offset: UInt64, _ size: Int)
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
/// Most API surfaces for the core container/process/agent types will
|
||||
/// return a ContainerizationError.
|
||||
public struct ContainerizationError: Swift.Error, Sendable {
|
||||
/// A code describing the error encountered.
|
||||
public var code: Code
|
||||
/// A description of the error.
|
||||
public var message: String
|
||||
/// The original error which led to this error being thrown.
|
||||
public var cause: (any Error)?
|
||||
|
||||
/// Creates a new error.
|
||||
@@ -48,21 +51,25 @@ public struct ContainerizationError: Swift.Error, Sendable {
|
||||
self.cause = cause
|
||||
}
|
||||
|
||||
/// Provides a unique hash of the error.
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.code)
|
||||
hasher.combine(self.message)
|
||||
}
|
||||
|
||||
/// Equality operator for the error. Uses the code and message.
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.code == rhs.code && lhs.message == rhs.message
|
||||
}
|
||||
|
||||
/// Checks if the given error has the provided code.
|
||||
public func isCode(_ code: Code) -> Bool {
|
||||
self.code == code
|
||||
}
|
||||
}
|
||||
|
||||
extension ContainerizationError: CustomStringConvertible {
|
||||
/// Description of the error.
|
||||
public var description: String {
|
||||
guard let cause = self.cause else {
|
||||
return "\(self.code): \"\(self.message)\""
|
||||
@@ -72,6 +79,7 @@ extension ContainerizationError: CustomStringConvertible {
|
||||
}
|
||||
|
||||
extension ContainerizationError {
|
||||
/// Codes for a `ContainerizationError`.
|
||||
public struct Code: Sendable, Hashable {
|
||||
private enum Value: Hashable, Sendable, CaseIterable {
|
||||
case unknown
|
||||
|
||||
@@ -32,6 +32,7 @@ public protocol AddressAllocator<AddressType>: Sendable {
|
||||
func disableAllocator() -> Bool
|
||||
}
|
||||
|
||||
/// Errors that a type implementing AddressAllocator should throw.
|
||||
public enum AllocatorError: Swift.Error, CustomStringConvertible, Equatable {
|
||||
case allocatorDisabled
|
||||
case allocatorFull
|
||||
|
||||
@@ -31,6 +31,7 @@ public actor AsyncLock {
|
||||
|
||||
public init() {}
|
||||
|
||||
/// withLock provides a scoped locking API to run a function while holding the lock.
|
||||
public func withLock<T: Sendable>(_ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T {
|
||||
while self.busy {
|
||||
await withCheckedContinuation { cc in
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
/// Returns a unique temporary directory to use.
|
||||
public func uniqueTemporaryDirectory(create: Bool = true) -> URL {
|
||||
let tempDirectoryURL = temporaryDirectory
|
||||
let uniqueDirectoryURL = tempDirectoryURL.appendingPathComponent(UUID().uuidString)
|
||||
|
||||
@@ -96,6 +96,7 @@ extension UInt32 {
|
||||
extension Character {
|
||||
private static let deviceLetters = Array("abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
/// Creates an allocator for block device tags, or any character values.
|
||||
public static func blockDeviceTagAllocator() -> any AddressAllocator<Character> {
|
||||
IndexedAddressAllocator(
|
||||
size: Self.deviceLetters.count,
|
||||
|
||||
@@ -56,6 +56,8 @@ public class ReadStream {
|
||||
self._data = data
|
||||
}
|
||||
|
||||
/// Resets the read stream. This either reassigns
|
||||
/// the data buffer or url to a new InputStream internally.
|
||||
public func reset() throws {
|
||||
self._stream.close()
|
||||
if let url = self._url {
|
||||
@@ -69,6 +71,7 @@ public class ReadStream {
|
||||
self._stream = InputStream(data: data)
|
||||
}
|
||||
|
||||
/// Get access to an `AsyncStream` of `ByteBuffer`'s from the input source.
|
||||
public var stream: AsyncStream<ByteBuffer> {
|
||||
AsyncStream { cont in
|
||||
self._stream.open()
|
||||
@@ -91,6 +94,7 @@ public class ReadStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get access to an `AsyncStream` of `Data` objects from the input source.
|
||||
public var dataStream: AsyncStream<Data> {
|
||||
AsyncStream { cont in
|
||||
self._stream.open()
|
||||
@@ -113,11 +117,12 @@ public class ReadStream {
|
||||
}
|
||||
|
||||
extension ReadStream {
|
||||
enum Error: Swift.Error, CustomStringConvertible {
|
||||
/// Errors that can be encountered while using a `ReadStream`.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case failedToCreateStream
|
||||
case noSuchFileOrDirectory(_ p: URL)
|
||||
|
||||
var description: String {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .failedToCreateStream:
|
||||
return "failed to create stream"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// AnnotationKeys contains a subset of "dictionary keys" for commonly used annotaions in a OCI Image Descriptor
|
||||
/// AnnotationKeys contains a subset of "dictionary keys" for commonly used annotaions in an OCI Image Descriptor
|
||||
/// https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||
public struct AnnotationKeys: Codable, Sendable {
|
||||
public static let containerizationImageName = "com.apple.containerization.image.name"
|
||||
|
||||
@@ -31,24 +31,43 @@ private let _umount = Glibc.umount2
|
||||
/// `Bundle` represents an OCI runtime spec bundle for running
|
||||
/// a container.
|
||||
public struct Bundle: Sendable {
|
||||
/// The path to the bundle.
|
||||
public let path: URL
|
||||
|
||||
/// The path to the OCI runtime spec config.json file.
|
||||
public var configPath: URL {
|
||||
self.path.appending(path: "config.json")
|
||||
}
|
||||
|
||||
/// The path to a rootfs mount inside the bundle.
|
||||
public var rootfsPath: URL {
|
||||
self.path.appending(path: "rootfs")
|
||||
}
|
||||
|
||||
/// Create the OCI bundle.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: A URL pointing to where to create the bundle on the filesystem.
|
||||
/// - spec: A data blob that should contain an OCI runtime spec. This will be written
|
||||
/// to the bundle as a "config.json" file.
|
||||
public static func create(path: URL, spec: Data) throws -> Bundle {
|
||||
try self.init(path: path, spec: spec)
|
||||
}
|
||||
|
||||
/// Create the OCI bundle.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: A URL pointing to where to create the bundle on the filesystem.
|
||||
/// - spec: An OCI runtime spec that will be written to the bundle as a "config.json"
|
||||
/// file.
|
||||
public static func create(path: URL, spec: ContainerizationOCI.Spec) throws -> Bundle {
|
||||
try self.init(path: path, spec: spec)
|
||||
}
|
||||
|
||||
/// Load an OCI bundle from the provided path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: A URL pointing to where to load the bundle from on the filesystem.
|
||||
public static func load(path: URL) throws -> Bundle {
|
||||
try self.init(path: path)
|
||||
}
|
||||
@@ -88,6 +107,7 @@ public struct Bundle: Sendable {
|
||||
try specData.write(to: self.configPath)
|
||||
}
|
||||
|
||||
/// Delete the OCI bundle from the filesystem.
|
||||
public func delete() throws {
|
||||
// Unmount, and then blow away the dir.
|
||||
#if os(Linux)
|
||||
@@ -101,6 +121,7 @@ public struct Bundle: Sendable {
|
||||
try fm.removeItem(at: self.path)
|
||||
}
|
||||
|
||||
/// Load and return the OCI runtime spec written to the bundle.
|
||||
public func loadConfig() throws -> ContainerizationOCI.Spec {
|
||||
let data = try Data(contentsOf: self.configPath)
|
||||
return try JSONDecoder().decode(ContainerizationOCI.Spec.self, from: data)
|
||||
|
||||
@@ -34,6 +34,8 @@ public struct BasicAuthentication: Authentication {
|
||||
self.password = password
|
||||
}
|
||||
|
||||
/// Get a token using the provided username and password. This will be a
|
||||
/// base64 encoded string of the username and password delimited by a colon.
|
||||
public func token() async throws -> String {
|
||||
let credentials = "\(username):\(password)"
|
||||
if let authenticationData = credentials.data(using: .utf8)?.base64EncodedString() {
|
||||
@@ -42,6 +44,7 @@ public struct BasicAuthentication: Authentication {
|
||||
throw Error.invalidCredentials
|
||||
}
|
||||
|
||||
/// `BasicAuthentication` errors.
|
||||
public enum Error: Swift.Error {
|
||||
case invalidCredentials
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ public struct KeychainHelper: Sendable {
|
||||
}
|
||||
|
||||
extension KeychainHelper {
|
||||
/// `KeychainHelper` errors.
|
||||
public enum Error: Swift.Error {
|
||||
case keyNotFound
|
||||
case invalidInput
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
import NIOHTTP1
|
||||
|
||||
extension RegistryClient {
|
||||
/// `RegistryClient` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case invalidStatus(url: String, HTTPResponseStatus)
|
||||
|
||||
/// Description of the errors.
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .invalidStatus(let u, let response):
|
||||
|
||||
@@ -27,10 +27,15 @@ import NIOHTTP1
|
||||
import Network
|
||||
#endif
|
||||
|
||||
/// Data used to control retry behavior for `RegistryClient`.
|
||||
public struct RetryOptions: Sendable {
|
||||
let maxRetries: Int
|
||||
let retryInterval: UInt64
|
||||
let shouldRetry: (@Sendable (HTTPClientResponse) -> Bool)?
|
||||
/// The maximum number of retries to attempt before failing.
|
||||
public var maxRetries: Int
|
||||
/// The retry interval in nanoseconds.
|
||||
public var retryInterval: UInt64
|
||||
/// A provided closure to handle if a given HTTP response should be
|
||||
/// retried.
|
||||
public var shouldRetry: (@Sendable (HTTPClientResponse) -> Bool)?
|
||||
|
||||
public init(maxRetries: Int, retryInterval: UInt64, shouldRetry: (@Sendable (HTTPClientResponse) -> Bool)? = nil) {
|
||||
self.maxRetries = maxRetries
|
||||
@@ -39,6 +44,7 @@ public struct RetryOptions: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// A client for interacting with OCI compliant container registries.
|
||||
public final class RegistryClient: ContentClient {
|
||||
private static let defaultRetryOptions = RetryOptions(
|
||||
maxRetries: 3,
|
||||
|
||||
@@ -15,43 +15,43 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
public actor AsyncStore<T> {
|
||||
package actor AsyncStore<T> {
|
||||
private var _value: T?
|
||||
|
||||
public init(_ value: T? = nil) {
|
||||
package init(_ value: T? = nil) {
|
||||
self._value = value
|
||||
}
|
||||
|
||||
public func get() -> T? {
|
||||
package func get() -> T? {
|
||||
self._value
|
||||
}
|
||||
|
||||
public func set(_ value: T) {
|
||||
package func set(_ value: T) {
|
||||
self._value = value
|
||||
}
|
||||
}
|
||||
|
||||
public actor AsyncSet<T: Hashable> {
|
||||
package actor AsyncSet<T: Hashable> {
|
||||
private var buffer: Set<T>
|
||||
|
||||
public init<S: Sequence>(_ elements: S) where S.Element == T {
|
||||
package init<S: Sequence>(_ elements: S) where S.Element == T {
|
||||
buffer = Set(elements)
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
package var count: Int {
|
||||
buffer.count
|
||||
}
|
||||
|
||||
public func insert(_ element: T) {
|
||||
package func insert(_ element: T) {
|
||||
buffer.insert(element)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func remove(_ element: T) -> T? {
|
||||
package func remove(_ element: T) -> T? {
|
||||
buffer.remove(element)
|
||||
}
|
||||
|
||||
public func contains(_ element: T) -> Bool {
|
||||
package func contains(_ element: T) -> Bool {
|
||||
buffer.contains(element)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public protocol Content: Sendable {
|
||||
/// sha256 of content
|
||||
func digest() throws -> SHA256.Digest
|
||||
|
||||
/// size of content
|
||||
/// Size of content
|
||||
func size() throws -> UInt64
|
||||
|
||||
/// Data represenatation of entire content
|
||||
|
||||
@@ -25,8 +25,11 @@ public class ContentWriter {
|
||||
private let base: URL
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
private var done: Bool = false
|
||||
|
||||
/// Create a new ContentWriter.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - for: The URL to write content to. If this is not a directory a
|
||||
/// ContainerizationError will be thrown with a code of .internalError.
|
||||
public init(for base: URL) throws {
|
||||
self.base = base
|
||||
var isDirectory = ObjCBool(true)
|
||||
@@ -37,6 +40,9 @@ public class ContentWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the data blob to the base URL provided in the constructor.
|
||||
/// - Parameters:
|
||||
/// - data: The data blob to write to a file under the base path.
|
||||
@discardableResult
|
||||
public func write(_ data: Data) throws -> (size: Int64, digest: SHA256.Digest) {
|
||||
let digest = SHA256.hash(data: data)
|
||||
@@ -45,12 +51,18 @@ public class ContentWriter {
|
||||
return (Int64(data.count), digest)
|
||||
}
|
||||
|
||||
/// Reads the data present in the passed in URL and writes it to the base path.
|
||||
/// - Parameters:
|
||||
/// - from: The URL to read the data from.
|
||||
@discardableResult
|
||||
public func create(from u: URL) throws -> (size: Int64, digest: SHA256.Digest) {
|
||||
let data = try Data(contentsOf: u)
|
||||
return try self.write(data)
|
||||
}
|
||||
|
||||
/// Encodes the passed in type as a JSON blob and writes it to the base path.
|
||||
/// - Parameters:
|
||||
/// - from: The type to convert to JSON.
|
||||
@discardableResult
|
||||
public func create<T: Encodable>(from content: T) throws -> (size: Int64, digest: SHA256.Digest) {
|
||||
let data = try self.encoder.encode(content)
|
||||
|
||||
@@ -22,6 +22,7 @@ import ContainerizationExtras
|
||||
import Crypto
|
||||
import Foundation
|
||||
|
||||
/// A `ContentStore` implementation that stores content on the local filesystem.
|
||||
public actor LocalContentStore: ContentStore {
|
||||
private static let encoder = JSONEncoder()
|
||||
|
||||
@@ -32,6 +33,10 @@ public actor LocalContentStore: ContentStore {
|
||||
|
||||
private var activeIngestSessions: AsyncSet<String> = AsyncSet([])
|
||||
|
||||
/// Create a new `LocalContentStore`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The path where content should be written under.
|
||||
public init(path: URL) throws {
|
||||
let ingestPath = path.appendingPathComponent("ingest")
|
||||
let blobPath = path.appendingPathComponent("blobs/sha256")
|
||||
@@ -47,6 +52,11 @@ public actor LocalContentStore: ContentStore {
|
||||
Self.encoder.outputFormatting = .sortedKeys
|
||||
}
|
||||
|
||||
/// Get a piece of content from the store. Returns nil if not
|
||||
/// found.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - digest: The string digest of the content.
|
||||
public func get(digest: String) throws -> Content? {
|
||||
let d = digest.trimmingDigestPrefix
|
||||
let path = self._blobPath.appendingPathComponent(d)
|
||||
@@ -62,6 +72,11 @@ public actor LocalContentStore: ContentStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a piece of content from the store and return the decoded version of
|
||||
/// it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - digest: The string digest of the content.
|
||||
public func get<T: Decodable & Sendable>(digest: String) throws -> T? {
|
||||
guard let content: Content = try self.get(digest: digest) else {
|
||||
return nil
|
||||
@@ -69,6 +84,10 @@ public actor LocalContentStore: ContentStore {
|
||||
return try content.decode()
|
||||
}
|
||||
|
||||
/// Delete all content besides a set provided.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keeping: The set of string digests to keep.
|
||||
public func delete(keeping: [String]) async throws -> ([String], UInt64) {
|
||||
let fileManager = FileManager.default
|
||||
let all = try fileManager.contentsOfDirectory(at: self._blobPath, includingPropertiesForKeys: nil)
|
||||
@@ -77,6 +96,10 @@ public actor LocalContentStore: ContentStore {
|
||||
return try await self.delete(digests: Array(toDelete))
|
||||
}
|
||||
|
||||
/// Delete a specific set of content.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - digests: Array of strings denoting the digests of the content to delete.
|
||||
@discardableResult
|
||||
public func delete(digests: [String]) async throws -> ([String], UInt64) {
|
||||
let store = AsyncStore<([String], UInt64)>()
|
||||
@@ -98,6 +121,13 @@ public actor LocalContentStore: ContentStore {
|
||||
return await store.get() ?? ([], 0)
|
||||
}
|
||||
|
||||
/// Creates a transactional write to the content store.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - body: Closure that is given a temporary `URL` of the base directory which all contents should be written to.
|
||||
/// This is a transaction write where any failed operation in the closure (caught exception) will result in all contents written
|
||||
/// in the closure to be deleted. If the closure succeeds, then all the content that have been written to the temporary `URL`
|
||||
/// will be moved into the actual blobs path of the content store.
|
||||
@discardableResult
|
||||
public func ingest(_ body: @Sendable @escaping (URL) async throws -> Void) async throws -> [String] {
|
||||
let (id, tempPath) = try await self.newIngestSession()
|
||||
@@ -105,6 +135,9 @@ public actor LocalContentStore: ContentStore {
|
||||
return try await self.completeIngestSession(id)
|
||||
}
|
||||
|
||||
/// Creates a new ingest session and returns the session ID and temporary ingest directory corresponding to the session.
|
||||
/// The contents from the ingest directory are processed and moved into the content store once the session is marked complete.
|
||||
/// This can be done by invoking the `completeIngestSession` method with the returned session ID.
|
||||
public func newIngestSession() async throws -> (id: String, ingestDir: URL) {
|
||||
let id = UUID().uuidString
|
||||
let temporaryPath = self._ingestPath.appendingPathComponent(id)
|
||||
@@ -114,6 +147,11 @@ public actor LocalContentStore: ContentStore {
|
||||
return (id, temporaryPath)
|
||||
}
|
||||
|
||||
/// Completes a previously started ingest session corresponding to `id`. The contents from the ingest
|
||||
/// directory from the session are moved into the content store atomically. Any failure encountered will
|
||||
/// result in a transaction failure causing none of the contents to be ingested into the store.
|
||||
/// - Parameters:
|
||||
/// - id: id of the ingest session to complete.
|
||||
@discardableResult
|
||||
public func completeIngestSession(_ id: String) async throws -> [String] {
|
||||
guard await activeIngestSessions.contains(id) else {
|
||||
@@ -149,6 +187,10 @@ public actor LocalContentStore: ContentStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels a previously started ingest session corresponding to `id`.
|
||||
/// The contents from the ingest directory corresponding to the session are removed.
|
||||
/// - Parameters:
|
||||
/// - id: id of the ingest session to complete.
|
||||
public func cancelIngestSession(_ id: String) async throws {
|
||||
guard let _ = await self.activeIngestSessions.remove(id) else {
|
||||
return
|
||||
|
||||
@@ -19,11 +19,13 @@ import Crypto
|
||||
import Foundation
|
||||
|
||||
extension SHA256.Digest {
|
||||
/// Returns the digest as a string.
|
||||
public var digestString: String {
|
||||
let parts = self.description.split(separator: ": ")
|
||||
return "sha256:\(parts[1])"
|
||||
}
|
||||
|
||||
/// Returns the digest without a 'sha256:' prefix.
|
||||
public var encoded: String {
|
||||
let parts = self.description.split(separator: ": ")
|
||||
return String(parts[1])
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension String {
|
||||
/// Removes any prefix (sha256:) from a digest string.
|
||||
public var trimmingDigestPrefix: String {
|
||||
let split = self.split(separator: ":")
|
||||
if split.count == 2 {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
/// returns the unescaped absolutePath of a URL joined by separator
|
||||
/// Returns the unescaped absolutePath of a URL joined by separator.
|
||||
public func absolutePath() -> String {
|
||||
#if os(macOS)
|
||||
return self.path(percentEncoded: false)
|
||||
@@ -27,6 +27,7 @@ extension URL {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Returns the domain name of a registry.
|
||||
public var domain: String? {
|
||||
guard let host = self.absoluteString.split(separator: ":").first else {
|
||||
return nil
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
import Foundation
|
||||
import Synchronization
|
||||
|
||||
/// Async friendly wrapper around DispatchSourceSignal. Provides an AsyncStream
|
||||
/// Async friendly wrapper around `DispatchSourceSignal`. Provides an `AsyncStream`
|
||||
/// interface to get notified of received signals.
|
||||
public final class AsyncSignalHandler: Sendable {
|
||||
/// An async stream that returns the signal that was caught, if ever
|
||||
/// An async stream that returns the signal that was caught, if ever.
|
||||
public var signals: AsyncStream<Int32> {
|
||||
let (stream, cont) = AsyncStream.makeStream(of: Int32.self)
|
||||
self.state.withLock {
|
||||
|
||||
@@ -19,6 +19,7 @@ import Foundation
|
||||
|
||||
/// Trivial type to discover information about a given file (uid, gid, mode...).
|
||||
public struct File: Sendable {
|
||||
/// `File` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case errno(_ e: Int32)
|
||||
|
||||
@@ -29,10 +30,17 @@ public struct File: Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `FileInfo` struct with information about the file.
|
||||
/// - Parameters:
|
||||
/// - url: The path to the file.
|
||||
public static func info(_ url: URL) throws -> FileInfo {
|
||||
try info(url.path)
|
||||
}
|
||||
|
||||
/// Returns a `FileInfo` struct with information about the file.
|
||||
/// - Parameters:
|
||||
/// - path: The path to the file as a string.
|
||||
public static func info(_ path: String) throws -> FileInfo {
|
||||
var st = stat()
|
||||
guard stat(path, &st) == 0 else {
|
||||
@@ -42,6 +50,8 @@ public struct File: Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// `FileInfo` holds and provides easy access to stat(2) data
|
||||
/// for a file.
|
||||
public struct FileInfo: Sendable {
|
||||
private let _stat_t: Foundation.stat
|
||||
private let _path: String
|
||||
@@ -51,58 +61,72 @@ public struct FileInfo: Sendable {
|
||||
self._stat_t = stat
|
||||
}
|
||||
|
||||
/// mode_t for the file.
|
||||
public var mode: mode_t {
|
||||
self._stat_t.st_mode
|
||||
}
|
||||
|
||||
/// The files uid.
|
||||
public var uid: Int {
|
||||
Int(self._stat_t.st_uid)
|
||||
}
|
||||
|
||||
/// The files gid.
|
||||
public var gid: Int {
|
||||
Int(self._stat_t.st_gid)
|
||||
}
|
||||
|
||||
/// The filesystem ID the file belongs to.
|
||||
public var dev: Int {
|
||||
Int(self._stat_t.st_dev)
|
||||
}
|
||||
|
||||
/// The files inode number.
|
||||
public var ino: Int {
|
||||
Int(self._stat_t.st_ino)
|
||||
}
|
||||
|
||||
/// The size of the file.
|
||||
public var size: Int {
|
||||
Int(self._stat_t.st_size)
|
||||
}
|
||||
|
||||
/// The path to the file.
|
||||
public var path: String {
|
||||
self._path
|
||||
}
|
||||
|
||||
/// Returns if the file is a directory.
|
||||
public var isDirectory: Bool {
|
||||
mode & S_IFMT == S_IFDIR
|
||||
}
|
||||
|
||||
/// Returns if the file is a pipe.
|
||||
public var isPipe: Bool {
|
||||
mode & S_IFMT == S_IFIFO
|
||||
}
|
||||
|
||||
/// Returns if the file is a socket.
|
||||
public var isSocket: Bool {
|
||||
mode & S_IFMT == S_IFSOCK
|
||||
}
|
||||
|
||||
/// Returns if the file is a link.
|
||||
public var isLink: Bool {
|
||||
mode & S_IFMT == S_IFLNK
|
||||
}
|
||||
|
||||
/// Returns if the file is a regular file.
|
||||
public var isRegularFile: Bool {
|
||||
mode & S_IFMT == S_IFREG
|
||||
}
|
||||
|
||||
/// Returns if the file is a block device.
|
||||
public var isBlock: Bool {
|
||||
mode & S_IFMT == S_IFBLK
|
||||
}
|
||||
|
||||
/// Returns if the file is a character device.
|
||||
public var isChar: Bool {
|
||||
mode & S_IFMT == S_IFCHR
|
||||
}
|
||||
|
||||
@@ -25,10 +25,14 @@ import Glibc
|
||||
private let _mount = Glibc.mount
|
||||
#endif
|
||||
|
||||
/// Small utility to mount or create new binfmt_misc entries.
|
||||
/// `Binfmt` is a utlity type that contains static helpers and types for
|
||||
/// mounting the Linux binfmt_misc filesystem, and creating new binfmt entries.
|
||||
public struct Binfmt: Sendable {
|
||||
/// Default mount path for binfmt_misc.
|
||||
public static let path = "/proc/sys/fs/binfmt_misc"
|
||||
|
||||
/// Entry models a binfmt_misc entry.
|
||||
/// https://docs.kernel.org/admin-guide/binfmt-misc.html
|
||||
public struct Entry {
|
||||
public var name: String
|
||||
public var type: String
|
||||
@@ -53,6 +57,7 @@ public struct Binfmt: Sendable {
|
||||
self.flags = flags
|
||||
}
|
||||
|
||||
/// Returns a binfmt `Entry` for amd64 ELF binaries.
|
||||
public static func amd64() -> Self {
|
||||
Binfmt.Entry(
|
||||
name: "x86_64",
|
||||
@@ -62,6 +67,7 @@ public struct Binfmt: Sendable {
|
||||
}
|
||||
|
||||
#if os(Linux)
|
||||
/// Register the passed in `binaryPath` as the interpreter for a new binfmt_misc entry.
|
||||
public func register(binaryPath: String) throws {
|
||||
let registration = ":\(self.name):\(self.type):\(self.offset):\(self.magic):\(self.mask):\(binaryPath):\(self.flags)"
|
||||
|
||||
@@ -72,7 +78,8 @@ public struct Binfmt: Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
public func unregister() throws {
|
||||
/// Deregister the binfmt_misc entry described by the current object.
|
||||
public func deregister() throws {
|
||||
let data = "-1"
|
||||
try data.write(
|
||||
to: URL(fileURLWithPath: Binfmt.path).appendingPathComponent(self.name),
|
||||
@@ -89,6 +96,7 @@ public struct Binfmt: Sendable {
|
||||
FileManager.default.fileExists(atPath: "\(Self.path)/register")
|
||||
}
|
||||
|
||||
/// Mount the binfmt_misc filesystem.
|
||||
public static func mount() throws {
|
||||
guard _mount("binfmt_misc", Self.path, "binfmt_misc", 0, "") == 0 else {
|
||||
throw POSIXError.fromErrno()
|
||||
|
||||
@@ -197,6 +197,7 @@ extension Mount {
|
||||
return mountOpts
|
||||
}
|
||||
|
||||
/// `Mount` errors
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case errno(Int32, String)
|
||||
case validation(String)
|
||||
|
||||
@@ -19,10 +19,13 @@ import Foundation
|
||||
|
||||
/// Helper type with utilities to parse and manipulate unix signals.
|
||||
public struct Signals {
|
||||
/// Returns the numeric values of all known signals.
|
||||
public static func allNumeric() -> [Int32] {
|
||||
Array(Signals.all.values)
|
||||
}
|
||||
|
||||
/// Parses a string representation of a signal (SIGKILL) and returns
|
||||
// the 32 bit integer representation (9).
|
||||
public static func parseSignal(_ signal: String) throws -> Int32 {
|
||||
if let sig = Int32(signal) {
|
||||
if !Signals.all.values.contains(sig) {
|
||||
@@ -38,6 +41,7 @@ public struct Signals {
|
||||
return sig
|
||||
}
|
||||
|
||||
/// Errors that can be encountered for converting signals.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case invalidSignal(String)
|
||||
|
||||
@@ -53,7 +57,7 @@ public struct Signals {
|
||||
#if os(macOS)
|
||||
|
||||
extension Signals {
|
||||
/// all returns all signals for the current platform.
|
||||
/// `all` returns all signals for the current platform.
|
||||
public static let all: [String: Int32] = [
|
||||
"ABRT": SIGABRT,
|
||||
"ALRM": SIGALRM,
|
||||
@@ -95,7 +99,10 @@ extension Signals {
|
||||
#if os(Linux)
|
||||
|
||||
extension Signals {
|
||||
/// all returns all signals for the current platform.
|
||||
/// `all` returns all signals for the current platform.
|
||||
///
|
||||
/// For Linux this isn't actually exhaustive as it excludes
|
||||
/// rtmin/rtmax entries.
|
||||
public static let all: [String: Int32] = [
|
||||
"ABRT": SIGABRT,
|
||||
"ALRM": SIGALRM,
|
||||
|
||||
@@ -27,17 +27,19 @@ import Darwin
|
||||
|
||||
/// Protocol used to describe the family of socket to be created with `Socket`.
|
||||
public protocol SocketType: Sendable, CustomStringConvertible {
|
||||
/// The domain for the socket (AF_UNIX, AF_VSOCK etc.)
|
||||
var domain: Int32 { get }
|
||||
/// The type of socket (SOCK_STREAM).
|
||||
var type: Int32 { get }
|
||||
|
||||
// Different socket types may want to expose things to do
|
||||
// before bind and listen. UDS for example may want to change
|
||||
// the permissions of the socket prior to bind/listen and also
|
||||
// possibly unlink an existing socket before bind.
|
||||
/// Actions to perform before calling bind(2).
|
||||
func beforeBind(fd: Int32) throws
|
||||
/// Actions to perform before calling listen(2).
|
||||
func beforeListen(fd: Int32) throws
|
||||
|
||||
/// Handle accept(2) for an implementation of a socket type.
|
||||
func accept(fd: Int32) throws -> (Int32, SocketType)
|
||||
/// Provide a sockaddr pointer (by casting a socket specific type like sockaddr_un for example).
|
||||
func withSockAddr(_ closure: (_ ptr: UnsafePointer<sockaddr>, _ len: UInt32) throws -> Void) throws
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ public struct UnixType: SocketType, Sendable, CustomStringConvertible {
|
||||
}
|
||||
|
||||
extension UnixType {
|
||||
/// `UnixType` errors.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
case nameTooLong(_: String)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user