680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
147 lines
4.8 KiB
Swift
147 lines
4.8 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationError
|
|
import Foundation
|
|
|
|
#if canImport(Musl)
|
|
import Musl
|
|
private let _mount = Musl.mount
|
|
private let _umount = Musl.umount2
|
|
#elseif canImport(Glibc)
|
|
import Glibc
|
|
private let _mount = Glibc.mount
|
|
private let _umount = Glibc.umount2
|
|
#endif
|
|
|
|
/// `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)
|
|
}
|
|
|
|
private init(path: URL) throws {
|
|
let fm = FileManager.default
|
|
if !fm.fileExists(atPath: path.path) {
|
|
throw ContainerizationError(.invalidArgument, message: "no bundle at \(path.path)")
|
|
}
|
|
self.path = path
|
|
}
|
|
|
|
// This constructor does not do any validation that data is actually a
|
|
// valid OCI spec.
|
|
private init(path: URL, spec: Data) throws {
|
|
self.path = path
|
|
|
|
let fm = FileManager.default
|
|
try fm.createDirectory(
|
|
atPath: self.path.appending(component: "rootfs").path,
|
|
withIntermediateDirectories: true
|
|
)
|
|
|
|
try spec.write(to: self.configPath)
|
|
}
|
|
|
|
private init(path: URL, spec: ContainerizationOCI.Spec) throws {
|
|
self.path = path
|
|
|
|
let fm = FileManager.default
|
|
try fm.createDirectory(
|
|
atPath: self.path.appending(component: "rootfs").path,
|
|
withIntermediateDirectories: true
|
|
)
|
|
|
|
let specData = try JSONEncoder().encode(spec)
|
|
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)
|
|
let rootfs = self.rootfsPath
|
|
if Self.isMountpoint(rootfs) {
|
|
guard _umount(rootfs.path, 0) == 0 else {
|
|
throw POSIXError.fromErrno()
|
|
}
|
|
}
|
|
#endif
|
|
// removeItem is recursive so should blow away the rootfs dir inside as well.
|
|
let fm = FileManager.default
|
|
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)
|
|
}
|
|
|
|
private static func isMountpoint(_ path: URL) -> Bool {
|
|
var st = stat()
|
|
var parent_st = stat()
|
|
|
|
guard stat(path.path, &st) == 0 else {
|
|
return false
|
|
}
|
|
|
|
let parentPath = path.deletingLastPathComponent()
|
|
guard stat(parentPath.path, &parent_st) == 0 else {
|
|
return false
|
|
}
|
|
|
|
return st.st_dev != parent_st.st_dev
|
|
}
|
|
}
|