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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ let package = Package(
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "SystemPackage", package: "swift-system"),
"ContainerAPIClient",
"ContainerImagesServiceClient",
"ContainerLog",
Expand Down
3 changes: 2 additions & 1 deletion Sources/Plugins/CoreImages/ImagesHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ContainerXPC
import Containerization
import Foundation
import Logging
import SystemPackage

@main
struct ImagesHelper: AsyncParsableCommand {
Expand Down Expand Up @@ -94,7 +95,7 @@ extension ImagesHelper {
let contentStore = RemoteContentStoreClient()
let imageStore = try ImageStore(path: root, contentStore: contentStore)
let unpackStrategy = SnapshotStore.defaultUnpackStrategy(initImage: containerSystemConfig.vminit.image)
let snapshotStore = try SnapshotStore(path: root, unpackStrategy: unpackStrategy, log: log)
let snapshotStore = try SnapshotStore(path: FilePath(root.absolutePath()), unpackStrategy: unpackStrategy, log: log)
let service = try ImagesService(contentStore: contentStore, imageStore: imageStore, snapshotStore: snapshotStore, log: log)
let harness = ImagesServiceHarness(service: service, log: log)

Expand Down
85 changes: 40 additions & 45 deletions Sources/Services/ContainerImagesService/Server/SnapshotStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import Containerization
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Logging
import SystemPackage
import TerminalProgress

public actor SnapshotStore {
Expand All @@ -47,20 +47,20 @@ public actor SnapshotStore {
}
}

let path: URL
let path: FilePath
let fm = FileManager.default
let ingestDir: URL
let ingestDir: FilePath
let unpackStrategy: UnpackStrategy
let log: Logger?

public init(path: URL, unpackStrategy: @escaping UnpackStrategy, log: Logger?) throws {
let root = path.appendingPathComponent("snapshots")
public init(path: FilePath, unpackStrategy: @escaping UnpackStrategy, log: Logger?) throws {
let root = path.appending("snapshots")
self.path = root
self.ingestDir = self.path.appendingPathComponent(Self.ingestDirName)
self.ingestDir = self.path.appending(Self.ingestDirName)
self.unpackStrategy = unpackStrategy
self.log = log
try self.fm.createDirectory(at: root, withIntermediateDirectories: true)
try self.fm.createDirectory(at: self.ingestDir, withIntermediateDirectories: true)
try self.fm.createDirectory(atPath: root.string, withIntermediateDirectories: true, attributes: nil)
try self.fm.createDirectory(atPath: self.ingestDir.string, withIntermediateDirectories: true, attributes: nil)
}

public func unpack(image: Containerization.Image, platform: Platform? = nil, progressUpdate: ProgressUpdateHandler?) async throws {
Expand All @@ -78,7 +78,7 @@ public actor SnapshotStore {
for desc in toUnpack {
try Task.checkCancellation()
let snapshotDir = self.snapshotDir(desc)
guard !self.fm.fileExists(atPath: snapshotDir.absolutePath()) else {
guard !self.fm.fileExists(atPath: snapshotDir.string) else {
// We have already unpacked this image + platform. Skip
continue
}
Expand All @@ -100,30 +100,30 @@ public actor SnapshotStore {

let tempDir = try self.tempUnpackDir()

let tempSnapshotPath = tempDir.appendingPathComponent(Self.snapshotFileName, isDirectory: false)
let infoPath = tempDir.appendingPathComponent(Self.snapshotInfoFileName, isDirectory: false)
let tempSnapshotPath = URL(fileURLWithPath: tempDir.appending(Self.snapshotFileName).string, isDirectory: false)
let infoPath = tempDir.appending(Self.snapshotInfoFileName)
do {
let progress = ContainerizationProgressAdapter.handler(from: taskUpdateProgress)
let mount = try await unpacker.unpack(image, for: platform, at: tempSnapshotPath, progress: progress)
let fs = Filesystem.block(
format: mount.type,
source: self.snapshotPath(desc).absolutePath(),
source: self.snapshotPath(desc).string,
destination: mount.destination,
options: mount.options
)
let snapshotInfo = try JSONEncoder().encode(fs)
self.fm.createFile(atPath: infoPath.absolutePath(), contents: snapshotInfo)
self.fm.createFile(atPath: infoPath.string, contents: snapshotInfo)
} catch {
try? self.fm.removeItem(at: tempDir)
try? self.fm.removeItem(atPath: tempDir.string)
throw error
}
do {
try fm.moveItem(at: tempDir, to: snapshotDir)
try fm.moveItem(atPath: tempDir.string, toPath: snapshotDir.string)
} catch let err as NSError {
guard err.code == NSFileWriteFileExistsError else {
throw err
}
try? self.fm.removeItem(at: tempDir)
try? self.fm.removeItem(atPath: tempDir.string)
}
}
await taskManager.finish()
Expand All @@ -139,10 +139,10 @@ public actor SnapshotStore {
}
for desc in toDelete {
let p = self.snapshotDir(desc)
guard self.fm.fileExists(atPath: p.absolutePath()) else {
guard self.fm.fileExists(atPath: p.string) else {
continue
}
try self.fm.removeItem(at: p)
try self.fm.removeItem(atPath: p.string)
}
}

Expand All @@ -151,13 +151,13 @@ public actor SnapshotStore {
let infoPath = snapshotInfoPath(desc)
let fsPath = snapshotPath(desc)

guard self.fm.fileExists(atPath: infoPath.absolutePath()),
self.fm.fileExists(atPath: fsPath.absolutePath())
guard self.fm.fileExists(atPath: infoPath.string),
self.fm.fileExists(atPath: fsPath.string)
else {
throw ContainerizationError(.notFound, message: "image snapshot for \(image.reference) with platform \(platform.description)")
}
let decoder = JSONDecoder()
let data = try Data(contentsOf: infoPath)
let data = try Data(contentsOf: URL(filePath: infoPath.string))
let fs = try decoder.decode(Filesystem.self, from: data)
return fs
}
Expand All @@ -173,63 +173,58 @@ public actor SnapshotStore {
toKeep.append(desc.digest.trimmingDigestPrefix)
}
}
let all = try self.fm.contentsOfDirectory(at: self.path, includingPropertiesForKeys: [.totalFileAllocatedSizeKey]).map {
$0.lastPathComponent
}
let all = try self.fm.contentsOfDirectory(atPath: self.path.string)
let delete = Set(all).subtracting(Set(toKeep))
var deletedBytes: UInt64 = 0
for dir in delete {
let unpackedPath = self.path.appending(path: dir, directoryHint: .isDirectory)
guard self.fm.fileExists(atPath: unpackedPath.absolutePath()) else {
let unpackedPath = self.path.appending(dir)
guard self.fm.fileExists(atPath: unpackedPath.string) else {
continue
}
deletedBytes += (try? self.fm.directorySize(dir: unpackedPath)) ?? 0
try self.fm.removeItem(at: unpackedPath)
try self.fm.removeItem(atPath: unpackedPath.string)
}
return deletedBytes
}

private func snapshotDir(_ desc: Descriptor) -> URL {
let p = self.path.appendingPathComponent(desc.digest.trimmingDigestPrefix, isDirectory: true)
return p
private func snapshotDir(_ desc: Descriptor) -> FilePath {
self.path.appending(desc.digest.trimmingDigestPrefix)
}

private func snapshotPath(_ desc: Descriptor) -> URL {
let p = self.snapshotDir(desc)
.appendingPathComponent(Self.snapshotFileName, isDirectory: false)
return p
private func snapshotPath(_ desc: Descriptor) -> FilePath {
self.snapshotDir(desc).appending(Self.snapshotFileName)
}

private func snapshotInfoPath(_ desc: Descriptor) -> URL {
let p = self.snapshotDir(desc)
.appendingPathComponent(Self.snapshotInfoFileName, isDirectory: false)
return p
private func snapshotInfoPath(_ desc: Descriptor) -> FilePath {
self.snapshotDir(desc).appending(Self.snapshotInfoFileName)
}

private func tempUnpackDir() throws -> URL {
let uniqueDirectoryURL = ingestDir.appendingPathComponent(UUID().uuidString, isDirectory: true)
try self.fm.createDirectory(at: uniqueDirectoryURL, withIntermediateDirectories: true, attributes: nil)
return uniqueDirectoryURL
private func tempUnpackDir() throws -> FilePath {
let uniqueDir = ingestDir.appending(UUID().uuidString)
try self.fm.createDirectory(atPath: uniqueDir.string, withIntermediateDirectories: true, attributes: nil)
return uniqueDir
}

/// Get the disk size for a specific snapshot descriptor
public func getSnapshotSize(descriptor: Descriptor) throws -> UInt64 {
let snapshotPath = self.snapshotDir(descriptor)
guard self.fm.fileExists(atPath: snapshotPath.path) else {
guard self.fm.fileExists(atPath: snapshotPath.string) else {
return 0
}
return try self.fm.directorySize(dir: snapshotPath)
}
}

extension FileManager {
fileprivate func directorySize(dir: URL) throws -> UInt64 {
fileprivate func directorySize(dir: FilePath) throws -> UInt64 {
var size: UInt64 = 0
let resourceKeys: [URLResourceKey] = [.totalFileAllocatedSizeKey]

// URL boundary: required by URLEnumerator API; no FilePath equivalent
let dirURL = URL(fileURLWithPath: dir.string, isDirectory: true)
guard
let enumerator = self.enumerator(
at: dir,
at: dirURL,
includingPropertiesForKeys: resourceKeys,
options: [.skipsHiddenFiles]
)
Expand Down