Skip to content

Commit d25d488

Browse files
committed
Only make fully extracted and verified VMs available to run. AINFRA-325
1 parent f6a9b1e commit d25d488

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed

Sources/libhostmgr/Platforms/VMManager.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,32 @@ public struct VMManager {
5555
/// Unpack a packaged VM
5656
///
5757
/// This method expects that the packaged VM is located in the `vm-images` directory – referencing it by name
58-
/// will attempt to unpack the VM at that location. If there is no packaged VM at that location, this method will
59-
/// emit an error.
58+
/// will attempt to unpack the VM at that location.
59+
/// - If there is no packaged VM at that location, this method will emit an error.
60+
/// - If an extracted VM already exists at the final destination path, this method will emit an error.
61+
/// - If an error occurs, this method will attempt to clean up any extracted files.
6062
public func unpackVM(name: String) async throws {
63+
let finalDestination = Paths.toVMTemplate(named: name)
64+
65+
guard !FileManager.default.fileExists(atPath: finalDestination.path) else {
66+
throw CocoaError(.fileWriteFileExists)
67+
}
68+
69+
let tempDirectory = FileManager.default.temporaryDirectory
70+
.appendingPathComponent("hostmgr-vm-unpack-\(name)-\(UUID().uuidString)")
71+
72+
defer {
73+
try? FileManager.default.removeItem(at: tempDirectory)
74+
}
75+
6176
try Compressor.decompress(
6277
archiveAt: Paths.toArchivedVM(named: name),
63-
to: Paths.toVMTemplate(named: name)
78+
to: tempDirectory
6479
)
6580

66-
try VMTemplate(at: Paths.toVMTemplate(named: name)).validate()
81+
try VMTemplate(at: tempDirectory).validate()
82+
83+
try FileManager.default.moveItem(at: tempDirectory, to: finalDestination)
6784
}
6885

6986
/// Package a VM for use on other machines
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import XCTest
2+
import Foundation
3+
@testable import libhostmgr
4+
5+
final class VMManagerTests: XCTestCase {
6+
7+
/// Test that unpackVM throws the correct error when a VM already exists at the destination
8+
func testUnpackVMThrowsWhenVMAlreadyExists() async throws {
9+
let vmName = "test-vm"
10+
let expectedVMPath = Paths.toVMTemplate(named: vmName)
11+
12+
// Create the destination directory to simulate an existing VM
13+
try FileManager.default.createDirectory(at: expectedVMPath, withIntermediateDirectories: true)
14+
defer {
15+
try? FileManager.default.removeItem(at: expectedVMPath)
16+
}
17+
18+
let vmManager = VMManager()
19+
20+
do {
21+
try await vmManager.unpackVM(name: vmName)
22+
XCTFail("Expected CocoaError.fileWriteFileExists to be thrown")
23+
} catch let error as CocoaError {
24+
XCTAssertEqual(error.code, CocoaError.fileWriteFileExists)
25+
}
26+
}
27+
28+
/// Test that unpackVM throws an error when the source archive doesn't exist
29+
func testUnpackVMThrowsWhenArchiveDoesNotExist() async throws {
30+
let vmName = "non-existent-vm"
31+
let vmManager = VMManager()
32+
33+
do {
34+
try await vmManager.unpackVM(name: vmName)
35+
XCTFail("Expected error when archive does not exist")
36+
} catch {
37+
XCTAssertTrue(true, "Should throw error for missing archive")
38+
}
39+
}
40+
41+
/// Test that temporary directories are properly cleaned up when unpackVM fails
42+
func testUnpackVMCleansUpTempDirectoryOnFailure() async throws {
43+
let vmName = "test-vm-cleanup"
44+
let vmManager = VMManager()
45+
46+
do {
47+
try await vmManager.unpackVM(name: vmName)
48+
XCTFail("Expected unpack to fail due to missing archive")
49+
} catch {
50+
// Verify that no temporary directories remain after failure
51+
let tempDirs = try FileManager.default.contentsOfDirectory(
52+
at: FileManager.default.temporaryDirectory,
53+
includingPropertiesForKeys: nil
54+
).filter { $0.lastPathComponent.contains("hostmgr-vm-unpack-\(vmName)") }
55+
56+
XCTAssertTrue(tempDirs.isEmpty, "Temporary directories should be cleaned up on failure")
57+
}
58+
}
59+
60+
/// Test that when unpacking an invalid archive fails, no folder is left at the final destination
61+
func testUnpackVMDoesNotLeaveDestinationFolderOnInvalidArchive() async throws {
62+
let vmName = "test-invalid-archive"
63+
let archivePath = Paths.toArchivedVM(named: vmName)
64+
let finalDestination = Paths.toVMTemplate(named: vmName)
65+
66+
// Create an invalid archive (just a text file instead of proper .aar format)
67+
try FileManager.default.createDirectory(
68+
at: archivePath.deletingLastPathComponent(),
69+
withIntermediateDirectories: true
70+
)
71+
try "invalid archive content".write(to: archivePath, atomically: true, encoding: .utf8)
72+
73+
defer {
74+
try? FileManager.default.removeItem(at: archivePath.deletingLastPathComponent())
75+
try? FileManager.default.removeItem(at: finalDestination)
76+
}
77+
78+
let vmManager = VMManager()
79+
80+
// Verify destination doesn't exist before attempting unpack
81+
XCTAssertFalse(FileManager.default.fileExists(atPath: finalDestination.path))
82+
83+
do {
84+
try await vmManager.unpackVM(name: vmName)
85+
XCTFail("Expected unpack to fail with invalid archive")
86+
} catch {
87+
// After failure, verify no folder was left at the final destination
88+
XCTAssertFalse(FileManager.default.fileExists(atPath: finalDestination.path),
89+
"Final destination should not exist after failed unpack")
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)