diff --git a/Sources/tart/VMDirectory.swift b/Sources/tart/VMDirectory.swift index cdd2ebf..7caeb9b 100644 --- a/Sources/tart/VMDirectory.swift +++ b/Sources/tart/VMDirectory.swift @@ -20,6 +20,10 @@ struct VMDirectory: Prunable { baseURL.appendingPathComponent("nvram.bin") } + var explicitlyPulledMark: URL { + baseURL.appendingPathComponent(".explicitly-pulled") + } + var name: String { baseURL.lastPathComponent } @@ -89,4 +93,12 @@ struct VMDirectory: Prunable { func sizeBytes() throws -> Int { try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes() } + + func markExplicitlyPulled() { + FileManager.default.createFile(atPath: explicitlyPulledMark.path, contents: nil) + } + + func isExplicitlyPulled() -> Bool { + FileManager.default.fileExists(atPath: explicitlyPulledMark.path) + } } diff --git a/Sources/tart/VMStorageOCI.swift b/Sources/tart/VMStorageOCI.swift index 5c0890f..f9f616c 100644 --- a/Sources/tart/VMStorageOCI.swift +++ b/Sources/tart/VMStorageOCI.swift @@ -42,6 +42,44 @@ class VMStorageOCI: PrunableStorage { func delete(_ name: RemoteName) throws { try FileManager.default.removeItem(at: vmURL(name)) + try gc() + } + + func gc() throws { + var refCounts = Dictionary() + + guard let enumerator = FileManager.default.enumerator(at: baseURL, + includingPropertiesForKeys: [.isSymbolicLinkKey]) else { + return + } + + for case let foundURL as URL in enumerator { + let isSymlink = try foundURL.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink! + + // Perform garbage collection for tag-based images + // with broken outgoing references + if isSymlink && foundURL == foundURL.resolvingSymlinksInPath() { + try FileManager.default.removeItem(at: foundURL) + continue + } + + let vmDir = VMDirectory(baseURL: foundURL.resolvingSymlinksInPath()) + if !vmDir.initialized { + continue + } + + refCounts[vmDir.baseURL] = (refCounts[vmDir.baseURL] ?? 0) + (isSymlink ? 1 : 0) + } + + // Perform garbage collection for digest-based images + // with no incoming references + for (baseURL, incRefCount) in refCounts { + let vmDir = VMDirectory(baseURL: baseURL) + + if !vmDir.isExplicitlyPulled() && incRefCount == 0 { + try FileManager.default.removeItem(at: baseURL) + } + } } func list() throws -> [(String, VMDirectory, Bool)] { @@ -82,10 +120,10 @@ class VMStorageOCI: PrunableStorage { func pull(_ name: RemoteName, registry: Registry) async throws { defaultLogger.appendNewLine("pulling manifest...") - let (manifest, _) = try await registry.pullManifest(reference: name.reference.value) + let (manifest, manifestData) = try await registry.pullManifest(reference: name.reference.value) - var digestName = RemoteName(host: name.host, namespace: name.namespace, - reference: Reference(digest: try manifest.digest())) + let digestName = RemoteName(host: name.host, namespace: name.namespace, + reference: Reference(digest: Digest.hash(manifestData))) if !exists(digestName) { let tmpVMDir = try VMDirectory.temporary() @@ -113,8 +151,12 @@ class VMStorageOCI: PrunableStorage { } if name != digestName { - // Overwrite the old symbolic link + // Create new or overwrite the old symbolic link try link(from: digestName, to: name) + } else { + // Ensure that images pulled by content digest + // are excluded from garbage collection + VMDirectory(baseURL: vmURL(name)).markExplicitlyPulled() } }