diff --git a/README.md b/README.md index 2012f048..bb845dbb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Sources/Containerization/Agent/Vminitd.swift b/Sources/Containerization/Agent/Vminitd.swift index 363ebd19..b3a16508 100644 --- a/Sources/Containerization/Agent/Vminitd.swift +++ b/Sources/Containerization/Agent/Vminitd.swift @@ -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") diff --git a/Sources/Containerization/Container.swift b/Sources/Containerization/Container.swift index e4a49831..53af23a0 100644 --- a/Sources/Containerization/Container.swift +++ b/Sources/Containerization/Container.swift @@ -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 } } diff --git a/Sources/Containerization/DNSConfiguration.swift b/Sources/Containerization/DNSConfiguration.swift index 540175c2..33e56bd6 100644 --- a/Sources/Containerization/DNSConfiguration.swift +++ b/Sources/Containerization/DNSConfiguration.swift @@ -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( diff --git a/Sources/Containerization/Image/Image.swift b/Sources/Containerization/Image/Image.swift index bff95678..c5ee2442 100644 --- a/Sources/Containerization/Image/Image.swift +++ b/Sources/Containerization/Image/Image.swift @@ -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 } diff --git a/Sources/ContainerizationEXT4/EXT4.swift b/Sources/ContainerizationEXT4/EXT4.swift index 5d3de30c..3a825864 100644 --- a/Sources/ContainerizationEXT4/EXT4.swift +++ b/Sources/ContainerizationEXT4/EXT4.swift @@ -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) diff --git a/Sources/ContainerizationError/ContainerizationError.swift b/Sources/ContainerizationError/ContainerizationError.swift index d4aebe5d..c6512de5 100644 --- a/Sources/ContainerizationError/ContainerizationError.swift +++ b/Sources/ContainerizationError/ContainerizationError.swift @@ -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 diff --git a/Sources/ContainerizationExtras/AddressAllocator.swift b/Sources/ContainerizationExtras/AddressAllocator.swift index 597c607c..4bf94581 100644 --- a/Sources/ContainerizationExtras/AddressAllocator.swift +++ b/Sources/ContainerizationExtras/AddressAllocator.swift @@ -32,6 +32,7 @@ public protocol AddressAllocator: Sendable { func disableAllocator() -> Bool } +/// Errors that a type implementing AddressAllocator should throw. public enum AllocatorError: Swift.Error, CustomStringConvertible, Equatable { case allocatorDisabled case allocatorFull diff --git a/Sources/ContainerizationExtras/AsyncLock.swift b/Sources/ContainerizationExtras/AsyncLock.swift index ad913ed4..d52a1da2 100644 --- a/Sources/ContainerizationExtras/AsyncLock.swift +++ b/Sources/ContainerizationExtras/AsyncLock.swift @@ -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(_ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T { while self.busy { await withCheckedContinuation { cc in diff --git a/Sources/ContainerizationExtras/FileManager+Temporary.swift b/Sources/ContainerizationExtras/FileManager+Temporary.swift index b55f0478..48c6a637 100644 --- a/Sources/ContainerizationExtras/FileManager+Temporary.swift +++ b/Sources/ContainerizationExtras/FileManager+Temporary.swift @@ -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) diff --git a/Sources/ContainerizationExtras/NetworkAddress+Allocator.swift b/Sources/ContainerizationExtras/NetworkAddress+Allocator.swift index a9eb28e4..0fe7025b 100644 --- a/Sources/ContainerizationExtras/NetworkAddress+Allocator.swift +++ b/Sources/ContainerizationExtras/NetworkAddress+Allocator.swift @@ -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 { IndexedAddressAllocator( size: Self.deviceLetters.count, diff --git a/Sources/ContainerizationIO/ReadStream.swift b/Sources/ContainerizationIO/ReadStream.swift index 49d7bc3f..232298d4 100644 --- a/Sources/ContainerizationIO/ReadStream.swift +++ b/Sources/ContainerizationIO/ReadStream.swift @@ -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 { 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 { 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" diff --git a/Sources/ContainerizationOCI/AnnotationKeys.swift b/Sources/ContainerizationOCI/AnnotationKeys.swift index 663a7b9d..9239f5aa 100644 --- a/Sources/ContainerizationOCI/AnnotationKeys.swift +++ b/Sources/ContainerizationOCI/AnnotationKeys.swift @@ -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" diff --git a/Sources/ContainerizationOCI/Bundle.swift b/Sources/ContainerizationOCI/Bundle.swift index e1edf9f3..e616f80b 100644 --- a/Sources/ContainerizationOCI/Bundle.swift +++ b/Sources/ContainerizationOCI/Bundle.swift @@ -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) diff --git a/Sources/ContainerizationOCI/Client/Authentication.swift b/Sources/ContainerizationOCI/Client/Authentication.swift index 7864206e..c970ef11 100644 --- a/Sources/ContainerizationOCI/Client/Authentication.swift +++ b/Sources/ContainerizationOCI/Client/Authentication.swift @@ -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 } diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index 9bfbe678..23c8364e 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -90,6 +90,7 @@ public struct KeychainHelper: Sendable { } extension KeychainHelper { + /// `KeychainHelper` errors. public enum Error: Swift.Error { case keyNotFound case invalidInput diff --git a/Sources/ContainerizationOCI/Client/RegistryClient+Error.swift b/Sources/ContainerizationOCI/Client/RegistryClient+Error.swift index 0db38258..e6705a3e 100644 --- a/Sources/ContainerizationOCI/Client/RegistryClient+Error.swift +++ b/Sources/ContainerizationOCI/Client/RegistryClient+Error.swift @@ -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): diff --git a/Sources/ContainerizationOCI/Client/RegistryClient.swift b/Sources/ContainerizationOCI/Client/RegistryClient.swift index a4d38159..876713f1 100644 --- a/Sources/ContainerizationOCI/Client/RegistryClient.swift +++ b/Sources/ContainerizationOCI/Client/RegistryClient.swift @@ -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, diff --git a/Sources/ContainerizationOCI/Content/AsyncTypes.swift b/Sources/ContainerizationOCI/Content/AsyncTypes.swift index cf7d8e6f..63cf3601 100644 --- a/Sources/ContainerizationOCI/Content/AsyncTypes.swift +++ b/Sources/ContainerizationOCI/Content/AsyncTypes.swift @@ -15,43 +15,43 @@ // limitations under the License. //===----------------------------------------------------------------------===// -public actor AsyncStore { +package actor AsyncStore { 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 { +package actor AsyncSet { private var buffer: Set - public init(_ elements: S) where S.Element == T { + package init(_ 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) } } diff --git a/Sources/ContainerizationOCI/Content/Content.swift b/Sources/ContainerizationOCI/Content/Content.swift index ae02193b..acae9846 100644 --- a/Sources/ContainerizationOCI/Content/Content.swift +++ b/Sources/ContainerizationOCI/Content/Content.swift @@ -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 diff --git a/Sources/ContainerizationOCI/Content/ContentWriter.swift b/Sources/ContainerizationOCI/Content/ContentWriter.swift index b99efc27..cfe0dbc7 100644 --- a/Sources/ContainerizationOCI/Content/ContentWriter.swift +++ b/Sources/ContainerizationOCI/Content/ContentWriter.swift @@ -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(from content: T) throws -> (size: Int64, digest: SHA256.Digest) { let data = try self.encoder.encode(content) diff --git a/Sources/ContainerizationOCI/Content/LocalContentStore.swift b/Sources/ContainerizationOCI/Content/LocalContentStore.swift index 7387d442..9f6a7969 100644 --- a/Sources/ContainerizationOCI/Content/LocalContentStore.swift +++ b/Sources/ContainerizationOCI/Content/LocalContentStore.swift @@ -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 = 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(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 diff --git a/Sources/ContainerizationOCI/Content/SHA256+Extensions.swift b/Sources/ContainerizationOCI/Content/SHA256+Extensions.swift index 67f72050..23b35d9c 100644 --- a/Sources/ContainerizationOCI/Content/SHA256+Extensions.swift +++ b/Sources/ContainerizationOCI/Content/SHA256+Extensions.swift @@ -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]) diff --git a/Sources/ContainerizationOCI/Content/String+Extension.swift b/Sources/ContainerizationOCI/Content/String+Extension.swift index 5d12a19c..3710b002 100644 --- a/Sources/ContainerizationOCI/Content/String+Extension.swift +++ b/Sources/ContainerizationOCI/Content/String+Extension.swift @@ -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 { diff --git a/Sources/ContainerizationOCI/Content/URL+Extensions.swift b/Sources/ContainerizationOCI/Content/URL+Extensions.swift index d715cc07..5528fedd 100644 --- a/Sources/ContainerizationOCI/Content/URL+Extensions.swift +++ b/Sources/ContainerizationOCI/Content/URL+Extensions.swift @@ -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 diff --git a/Sources/ContainerizationOS/AsyncSignalHandler.swift b/Sources/ContainerizationOS/AsyncSignalHandler.swift index 2194234d..4ddef04b 100644 --- a/Sources/ContainerizationOS/AsyncSignalHandler.swift +++ b/Sources/ContainerizationOS/AsyncSignalHandler.swift @@ -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 { let (stream, cont) = AsyncStream.makeStream(of: Int32.self) self.state.withLock { diff --git a/Sources/ContainerizationOS/File.swift b/Sources/ContainerizationOS/File.swift index f9f5de3d..5e04e09f 100644 --- a/Sources/ContainerizationOS/File.swift +++ b/Sources/ContainerizationOS/File.swift @@ -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 } diff --git a/Sources/ContainerizationOS/Linux/Binfmt.swift b/Sources/ContainerizationOS/Linux/Binfmt.swift index b278eea5..ab125e5d 100644 --- a/Sources/ContainerizationOS/Linux/Binfmt.swift +++ b/Sources/ContainerizationOS/Linux/Binfmt.swift @@ -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() diff --git a/Sources/ContainerizationOS/Mount/Mount.swift b/Sources/ContainerizationOS/Mount/Mount.swift index a604e652..80a6d25c 100644 --- a/Sources/ContainerizationOS/Mount/Mount.swift +++ b/Sources/ContainerizationOS/Mount/Mount.swift @@ -197,6 +197,7 @@ extension Mount { return mountOpts } + /// `Mount` errors public enum Error: Swift.Error, CustomStringConvertible { case errno(Int32, String) case validation(String) diff --git a/Sources/ContainerizationOS/Signals.swift b/Sources/ContainerizationOS/Signals.swift index c2ed21d0..87bdd052 100644 --- a/Sources/ContainerizationOS/Signals.swift +++ b/Sources/ContainerizationOS/Signals.swift @@ -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, diff --git a/Sources/ContainerizationOS/Socket/SocketType.swift b/Sources/ContainerizationOS/Socket/SocketType.swift index 27ffc817..05211bff 100644 --- a/Sources/ContainerizationOS/Socket/SocketType.swift +++ b/Sources/ContainerizationOS/Socket/SocketType.swift @@ -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, _ len: UInt32) throws -> Void) throws } diff --git a/Sources/ContainerizationOS/Socket/UnixType.swift b/Sources/ContainerizationOS/Socket/UnixType.swift index dcab6295..fdc68354 100644 --- a/Sources/ContainerizationOS/Socket/UnixType.swift +++ b/Sources/ContainerizationOS/Socket/UnixType.swift @@ -147,6 +147,7 @@ public struct UnixType: SocketType, Sendable, CustomStringConvertible { } extension UnixType { + /// `UnixType` errors. public enum Error: Swift.Error, CustomStringConvertible { case nameTooLong(_: String)