mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-12 08:55:34 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d8b9b1f01 | ||
|
|
36bf7116c7 |
@@ -21,7 +21,5 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Access to OCI registries on the local network</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -31,6 +31,15 @@ struct Clone: AsyncParsableCommand {
|
||||
@Flag(help: .hidden)
|
||||
var deduplicate: Bool = false
|
||||
|
||||
@Option(help: .hidden)
|
||||
var proxy: String?
|
||||
|
||||
@Option(help: .hidden)
|
||||
var caCert: String?
|
||||
|
||||
@Option(help: .hidden)
|
||||
var maxRetries: UInt = 5
|
||||
|
||||
func validate() throws {
|
||||
if newName.contains("/") {
|
||||
throw ValidationError("<new-name> should be a local name")
|
||||
@@ -47,8 +56,8 @@ struct Clone: AsyncParsableCommand {
|
||||
|
||||
if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) {
|
||||
// Pull the VM in case it's OCI-based and doesn't exist locally yet
|
||||
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure)
|
||||
try await ociStorage.pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate)
|
||||
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure, proxy: proxy, caCert: caCert)
|
||||
try await ociStorage.pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate, maxRetries: maxRetries)
|
||||
}
|
||||
|
||||
let sourceVM = try VMStorageHelper.open(sourceName)
|
||||
|
||||
@@ -26,6 +26,15 @@ struct Pull: AsyncParsableCommand {
|
||||
@Flag(help: .hidden)
|
||||
var deduplicate: Bool = false
|
||||
|
||||
@Option(help: .hidden)
|
||||
var proxy: String?
|
||||
|
||||
@Option(help: .hidden)
|
||||
var caCert: String?
|
||||
|
||||
@Option(help: .hidden)
|
||||
var maxRetries: UInt = 5
|
||||
|
||||
func validate() throws {
|
||||
if concurrency < 1 {
|
||||
throw ValidationError("network concurrency cannot be less than 1")
|
||||
@@ -42,10 +51,10 @@ struct Pull: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
let remoteName = try RemoteName(remoteName)
|
||||
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure)
|
||||
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure, proxy: proxy, caCert: caCert)
|
||||
|
||||
defaultLogger.appendNewLine("pulling \(remoteName)...")
|
||||
|
||||
try await VMStorageOCI().pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate)
|
||||
try await VMStorageOCI().pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate, maxRetries: maxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,6 @@ struct Push: AsyncParsableCommand {
|
||||
"""))
|
||||
var chunkSize: Int = 0
|
||||
|
||||
|
||||
@Option(name: [.customLong("label")], help: ArgumentHelp("additional metadata to attach to the OCI image configuration in key=value format",
|
||||
discussion: "Can be specified multiple times to attach multiple labels."))
|
||||
var labels: [String] = []
|
||||
|
||||
@Option(help: .hidden)
|
||||
var diskFormat: String = "v2"
|
||||
|
||||
@@ -86,8 +81,7 @@ struct Push: AsyncParsableCommand {
|
||||
references: references,
|
||||
chunkSizeMb: chunkSize,
|
||||
diskFormat: diskFormat,
|
||||
concurrency: concurrency,
|
||||
labels: parseLabels()
|
||||
concurrency: concurrency
|
||||
)
|
||||
// Populate the local cache (if requested)
|
||||
if populateCache {
|
||||
@@ -121,28 +115,6 @@ struct Push: AsyncParsableCommand {
|
||||
return RemoteName(host: registry.host!, namespace: registry.namespace,
|
||||
reference: Reference(digest: digest))
|
||||
}
|
||||
|
||||
// Helper method to convert labels array to dictionary
|
||||
func parseLabels() -> [String: String] {
|
||||
var result = [String: String]()
|
||||
|
||||
for label in labels {
|
||||
let parts = label.trimmingCharacters(in: .whitespaces).split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
|
||||
|
||||
let key = parts.count > 0 ? String(parts[0]) : ""
|
||||
let value = parts.count > 1 ? String(parts[1]) : ""
|
||||
|
||||
// It sometimes makes sense to provide an empty value,
|
||||
// but not an empty key
|
||||
if key.isEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Element == RemoteName {
|
||||
|
||||
@@ -260,11 +260,6 @@ struct Run: AsyncParsableCommand {
|
||||
#endif
|
||||
var captureSystemKeys: Bool = false
|
||||
|
||||
#if arch(arm64)
|
||||
@Flag(help: ArgumentHelp("Don't add trackpad as a pointing device on macOS VMs"))
|
||||
#endif
|
||||
var noTrackpad: Bool = false
|
||||
|
||||
mutating func validate() throws {
|
||||
if vnc && vncExperimental {
|
||||
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
||||
@@ -295,7 +290,7 @@ struct Run: AsyncParsableCommand {
|
||||
|
||||
if nested {
|
||||
if #unavailable(macOS 15) {
|
||||
throw ValidationError("Nested virtualization is supported on hosts starting with macOS 15 (Sequoia), and later.")
|
||||
throw ValidationError("Nested virtualization is supported on hosts starting with macOS 15 (Sequia), and later.")
|
||||
} else if !VZGenericPlatformConfiguration.isNestedVirtualizationSupported {
|
||||
throw ValidationError("Nested virtualization is available for Mac with the M3 chip, and later.")
|
||||
}
|
||||
@@ -315,17 +310,6 @@ struct Run: AsyncParsableCommand {
|
||||
if dir.count > 0 {
|
||||
throw ValidationError("Suspending VMs with shared directories is not supported")
|
||||
}
|
||||
|
||||
if noTrackpad {
|
||||
throw ValidationError("--no-trackpad cannot be used with --suspendable")
|
||||
}
|
||||
}
|
||||
|
||||
if noTrackpad {
|
||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
if config.os != .darwin {
|
||||
throw ValidationError("--no-trackpad can only be used with macOS VMs")
|
||||
}
|
||||
}
|
||||
|
||||
for disk in disk {
|
||||
@@ -389,8 +373,7 @@ struct Run: AsyncParsableCommand {
|
||||
audio: !noAudio,
|
||||
clipboard: !noClipboard,
|
||||
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
|
||||
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
|
||||
noTrackpad: noTrackpad
|
||||
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw)
|
||||
)
|
||||
|
||||
let vncImpl: VNC? = try {
|
||||
|
||||
@@ -33,7 +33,15 @@ struct Set: AsyncParsableCommand {
|
||||
|
||||
@Option(help: ArgumentHelp("Resize the VMs disk to the specified size in GB (note that the disk size can only be increased to avoid losing data)",
|
||||
discussion: """
|
||||
See https://tart.run/faq/#disk-resizing for more details.
|
||||
Disk resizing works on most cloud-ready Linux distributions out-of-the box (e.g. Ubuntu Cloud Images
|
||||
have the \"cloud-initramfs-growroot\" package installed that runs on boot) and on the rest of the
|
||||
distributions by running the \"growpart\" or \"resize2fs\" commands.
|
||||
|
||||
For macOS, however, things are a bit more complicated: you need to remove the recovery partition
|
||||
first and then run various \"diskutil\" commands, see Tart's packer plugin source code for more
|
||||
details[1].
|
||||
|
||||
[1]: https://github.com/cirruslabs/packer-plugin-tart/blob/main/builder/tart/step_disk_resize.go
|
||||
"""))
|
||||
var diskSize: UInt16?
|
||||
|
||||
|
||||
+118
-20
@@ -1,26 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
fileprivate var urlSession: URLSession = {
|
||||
let config = URLSessionConfiguration.default
|
||||
|
||||
// Harbor expects a CSRF token to be present if the HTTP client
|
||||
// carries a session cookie between its requests[1] and fails if
|
||||
// it was not present[2].
|
||||
//
|
||||
// To fix that, we disable the automatic cookies carry in URLSession.
|
||||
//
|
||||
// [1]: https://github.com/goharbor/harbor/blob/a4c577f9ec4f18396207a5e686433a6ba203d4ef/src/server/middleware/csrf/csrf.go#L78
|
||||
// [2]: https://github.com/cirruslabs/tart/issues/295
|
||||
config.httpShouldSetCookies = false
|
||||
|
||||
return URLSession(configuration: config)
|
||||
}()
|
||||
|
||||
class Fetcher {
|
||||
static func fetch(_ request: URLRequest, viaFile: Bool = false) async throws -> (AsyncThrowingStream<Data, Error>, HTTPURLResponse) {
|
||||
let task = urlSession.dataTask(with: request)
|
||||
let urlSession: URLSession
|
||||
let caCert: SecCertificate?
|
||||
|
||||
let delegate = Delegate()
|
||||
init(proxy: String? = nil, caCert: String? = nil) throws {
|
||||
// Configure URLSession
|
||||
let config = URLSessionConfiguration.default
|
||||
|
||||
// Harbor expects a CSRF token to be present if the HTTP client
|
||||
// carries a session cookie between its requests[1] and fails if
|
||||
// it was not present[2].
|
||||
//
|
||||
// To fix that, we disable the automatic cookies carry in URLSession.
|
||||
//
|
||||
// [1]: https://github.com/goharbor/harbor/blob/a4c577f9ec4f18396207a5e686433a6ba203d4ef/src/server/middleware/csrf/csrf.go#L78
|
||||
// [2]: https://github.com/cirruslabs/tart/issues/295
|
||||
config.httpShouldSetCookies = false
|
||||
|
||||
if let proxy {
|
||||
let (host, port) = try Self.parseProxy(proxy)
|
||||
|
||||
config.connectionProxyDictionary = [
|
||||
kCFNetworkProxiesHTTPEnable: true,
|
||||
kCFNetworkProxiesHTTPProxy: host,
|
||||
kCFNetworkProxiesHTTPPort: port,
|
||||
|
||||
kCFNetworkProxiesHTTPSEnable: true,
|
||||
kCFNetworkProxiesHTTPSProxy: host,
|
||||
kCFNetworkProxiesHTTPSPort: port,
|
||||
]
|
||||
}
|
||||
|
||||
self.urlSession = URLSession(configuration: config)
|
||||
|
||||
// Load CA certificate, if any
|
||||
if let caCert {
|
||||
let caCertString = try String(contentsOf: URL(filePath: caCert), encoding:. utf8)
|
||||
|
||||
let caCertBase64Lines = caCertString.components(separatedBy: .newlines).filter { line in
|
||||
!line.hasPrefix("-----BEGIN") && !line.hasPrefix("-----END")
|
||||
}
|
||||
|
||||
guard let caCertData = Data(base64Encoded: caCertBase64Lines.joined()) else {
|
||||
throw RuntimeError.FailedToLoadCACertificate("failed to parse Base64-encoded PEM data")
|
||||
}
|
||||
|
||||
self.caCert = SecCertificateCreateWithData(nil, caCertData as CFData)!
|
||||
} else {
|
||||
self.caCert = nil
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(_ request: URLRequest, viaFile: Bool = false) async throws -> (AsyncThrowingStream<Data, Error>, HTTPURLResponse) {
|
||||
let task = self.urlSession.dataTask(with: request)
|
||||
|
||||
let delegate = Delegate(caCert: self.caCert)
|
||||
task.delegate = delegate
|
||||
|
||||
let stream = AsyncThrowingStream<Data, Error> { continuation in
|
||||
@@ -34,15 +69,78 @@ class Fetcher {
|
||||
|
||||
return (stream, response as! HTTPURLResponse)
|
||||
}
|
||||
|
||||
private static func parseProxy(_ proxy: String) throws -> (String, Int) {
|
||||
// Assume that the scheme is specified
|
||||
var url = URL(string: proxy)
|
||||
|
||||
// Fall back to HTTP scheme when not specified
|
||||
if url?.scheme == nil {
|
||||
url = URL(string: "http://\(proxy)")
|
||||
}
|
||||
|
||||
guard let url else {
|
||||
throw RuntimeError.InvalidProxyString
|
||||
}
|
||||
|
||||
guard let host = url.host() else {
|
||||
throw RuntimeError.InvalidProxyString
|
||||
}
|
||||
|
||||
guard let port = url.port else {
|
||||
throw RuntimeError.InvalidProxyString
|
||||
}
|
||||
|
||||
return (host, port)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class Delegate: NSObject, URLSessionDataDelegate {
|
||||
fileprivate class Delegate: NSObject, URLSessionDelegate, URLSessionDataDelegate {
|
||||
let caCert: SecCertificate?
|
||||
var responseContinuation: CheckedContinuation<URLResponse, Error>?
|
||||
var streamContinuation: AsyncThrowingStream<Data, Error>.Continuation?
|
||||
|
||||
private var buffer: Data = Data()
|
||||
private let bufferFlushSize = 16 * 1024 * 1024
|
||||
|
||||
init(caCert: SecCertificate?) {
|
||||
self.caCert = caCert
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
) {
|
||||
if let caCert {
|
||||
// Ensure that we're performing server trust authentication
|
||||
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
||||
let serverTrust = challenge.protectionSpace.serverTrust else {
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Set the provided CA certificate as the only anchor
|
||||
if SecTrustSetAnchorCertificates(serverTrust, [caCert] as CFArray) != errSecSuccess {
|
||||
completionHandler(.cancelAuthenticationChallenge, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate the trust
|
||||
if SecTrustEvaluateWithError(serverTrust, nil) {
|
||||
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
||||
} else {
|
||||
completionHandler(.rejectProtectionSpace, nil)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
|
||||
@@ -2,5 +2,5 @@ import Foundation
|
||||
|
||||
protocol Disk {
|
||||
static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer]
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache?, deduplicate: Bool, maxRetries: UInt) async throws
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DiskV1: Disk {
|
||||
return pushedLayers
|
||||
}
|
||||
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false) async throws {
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false, maxRetries: UInt = 5) async throws {
|
||||
if !FileManager.default.createFile(atPath: diskURL.path, contents: nil) {
|
||||
throw OCIError.FailedToCreateVmFile
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class DiskV2: Disk {
|
||||
}
|
||||
}
|
||||
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false) async throws {
|
||||
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false, maxRetries: UInt = 5) async throws {
|
||||
// Support resumable pulls
|
||||
let pullResumed = FileManager.default.fileExists(atPath: diskURL.path)
|
||||
|
||||
@@ -210,7 +210,7 @@ class DiskV2: Disk {
|
||||
|
||||
var rangeStart: Int64 = 0
|
||||
|
||||
try await retry(maxAttempts: 5) {
|
||||
try await retry(maxAttempts: Int(maxRetries)) {
|
||||
try await registry.pullBlob(diskLayer.digest, rangeStart: rangeStart) { data in
|
||||
try filter.write(data)
|
||||
|
||||
|
||||
@@ -66,11 +66,6 @@ struct OCIManifest: Codable, Equatable {
|
||||
struct OCIConfig: Codable {
|
||||
var architecture: Architecture = .arm64
|
||||
var os: OS = .darwin
|
||||
var config: ConfigContainer?
|
||||
|
||||
struct ConfigContainer: Codable {
|
||||
var Labels: [String: String]?
|
||||
}
|
||||
|
||||
func toJSON() throws -> Data {
|
||||
try Config.jsonEncoder().encode(self)
|
||||
|
||||
@@ -115,6 +115,7 @@ class Registry {
|
||||
let namespace: String
|
||||
let credentialsProviders: [CredentialsProvider]
|
||||
let authenticationKeeper = AuthenticationKeeper()
|
||||
let fetcher: Fetcher
|
||||
|
||||
var host: String? {
|
||||
guard let host = baseURL.host else { return nil }
|
||||
@@ -128,17 +129,22 @@ class Registry {
|
||||
|
||||
init(baseURL: URL,
|
||||
namespace: String,
|
||||
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()]
|
||||
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()],
|
||||
proxy: String? = nil,
|
||||
caCert: String? = nil
|
||||
) throws {
|
||||
self.baseURL = baseURL
|
||||
self.namespace = namespace
|
||||
self.credentialsProviders = credentialsProviders
|
||||
self.fetcher = try Fetcher(proxy: proxy, caCert: caCert)
|
||||
}
|
||||
|
||||
convenience init(
|
||||
host: String,
|
||||
namespace: String,
|
||||
insecure: Bool = false,
|
||||
proxy: String? = nil,
|
||||
caCert: String? = nil,
|
||||
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()]
|
||||
) throws {
|
||||
let proto = insecure ? "http" : "https"
|
||||
@@ -154,7 +160,7 @@ class Registry {
|
||||
throw RuntimeError.ImproperlyFormattedHost(host, hint)
|
||||
}
|
||||
|
||||
try self.init(baseURL: baseURL, namespace: namespace, credentialsProviders: credentialsProviders)
|
||||
try self.init(baseURL: baseURL, namespace: namespace, credentialsProviders: credentialsProviders, proxy: proxy, caCert: caCert)
|
||||
}
|
||||
|
||||
func ping() async throws {
|
||||
@@ -448,6 +454,6 @@ class Registry {
|
||||
request.setValue("Tart/\(CI.version) (\(DeviceInfo.os); \(DeviceInfo.model))",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
|
||||
return try await Fetcher.fetch(request, viaFile: viaFile)
|
||||
return try await self.fetcher.fetch(request, viaFile: viaFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +127,6 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
|
||||
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
|
||||
}
|
||||
|
||||
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
|
||||
// Only include the USB pointing device, not the trackpad
|
||||
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
||||
}
|
||||
|
||||
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
|
||||
if #available(macOS 14, *) {
|
||||
return [VZMacTrackpadConfiguration()]
|
||||
|
||||
@@ -42,9 +42,4 @@ struct Linux: Platform {
|
||||
func pointingDevices() -> [VZPointingDeviceConfiguration] {
|
||||
[VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
||||
}
|
||||
|
||||
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
|
||||
// Linux doesn't support trackpad, so just return the regular pointing devices
|
||||
return pointingDevices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ protocol Platform: Codable {
|
||||
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
|
||||
func keyboards() -> [VZKeyboardConfiguration]
|
||||
func pointingDevices() -> [VZPointingDeviceConfiguration]
|
||||
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration]
|
||||
}
|
||||
|
||||
protocol PlatformSuspendable: Platform {
|
||||
|
||||
+7
-14
@@ -50,8 +50,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
audio: Bool = true,
|
||||
clipboard: Bool = true,
|
||||
sync: VZDiskImageSynchronizationMode = .full,
|
||||
caching: VZDiskImageCachingMode? = nil,
|
||||
noTrackpad: Bool = false
|
||||
caching: VZDiskImageCachingMode? = nil
|
||||
) throws {
|
||||
name = vmDir.name
|
||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||
@@ -72,8 +71,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
audio: audio,
|
||||
clipboard: clipboard,
|
||||
sync: sync,
|
||||
caching: caching,
|
||||
noTrackpad: noTrackpad
|
||||
caching: caching
|
||||
)
|
||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||
|
||||
@@ -85,7 +83,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
// Check if we already have this IPSW in cache
|
||||
var headRequest = URLRequest(url: remoteURL)
|
||||
headRequest.httpMethod = "HEAD"
|
||||
let (_, headResponse) = try await Fetcher.fetch(headRequest, viaFile: false)
|
||||
let (_, headResponse) = try await Fetcher().fetch(headRequest, viaFile: false)
|
||||
|
||||
if let hash = headResponse.value(forHTTPHeaderField: "x-amz-meta-digest-sha256") {
|
||||
let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw")
|
||||
@@ -102,7 +100,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
defaultLogger.appendNewLine("Fetching \(remoteURL.lastPathComponent)...")
|
||||
|
||||
let request = URLRequest(url: remoteURL)
|
||||
let (channel, response) = try await Fetcher.fetch(request, viaFile: true)
|
||||
let (channel, response) = try await Fetcher().fetch(request, viaFile: true)
|
||||
|
||||
let temporaryLocation = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString + ".ipsw")
|
||||
|
||||
@@ -300,8 +298,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
audio: Bool = true,
|
||||
clipboard: Bool = true,
|
||||
sync: VZDiskImageSynchronizationMode = .full,
|
||||
caching: VZDiskImageCachingMode? = nil,
|
||||
noTrackpad: Bool = false
|
||||
caching: VZDiskImageCachingMode? = nil
|
||||
) throws -> VZVirtualMachineConfiguration {
|
||||
let configuration = VZVirtualMachineConfiguration()
|
||||
|
||||
@@ -342,11 +339,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
|
||||
} else {
|
||||
configuration.keyboards = vmConfig.platform.keyboards()
|
||||
if noTrackpad {
|
||||
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
|
||||
} else {
|
||||
configuration.pointingDevices = vmConfig.platform.pointingDevices()
|
||||
}
|
||||
configuration.pointingDevices = vmConfig.platform.pointingDevices()
|
||||
}
|
||||
|
||||
// Networking
|
||||
@@ -368,7 +361,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
// Storage
|
||||
var attachment = try VZDiskImageStorageDeviceAttachment(
|
||||
let attachment: VZDiskImageStorageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(
|
||||
url: diskURL,
|
||||
readOnly: false,
|
||||
// When not specified, use "cached" caching mode for Linux VMs to prevent file-system corruption[1]
|
||||
|
||||
@@ -11,7 +11,14 @@ enum OCIError: Error {
|
||||
}
|
||||
|
||||
extension VMDirectory {
|
||||
func pullFromRegistry(registry: Registry, manifest: OCIManifest, concurrency: UInt, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws {
|
||||
func pullFromRegistry(
|
||||
registry: Registry,
|
||||
manifest: OCIManifest,
|
||||
concurrency: UInt,
|
||||
localLayerCache: LocalLayerCache?,
|
||||
deduplicate: Bool,
|
||||
maxRetries: UInt
|
||||
) async throws {
|
||||
// Pull VM's config file layer and re-serialize it into a config file
|
||||
let configLayers = manifest.layers.filter {
|
||||
$0.mediaType == configMediaType
|
||||
@@ -55,7 +62,8 @@ extension VMDirectory {
|
||||
try await diskImplType.pull(registry: registry, diskLayers: layers, diskURL: diskURL,
|
||||
concurrency: concurrency, progress: progress,
|
||||
localLayerCache: localLayerCache,
|
||||
deduplicate: deduplicate)
|
||||
deduplicate: deduplicate,
|
||||
maxRetries: maxRetries)
|
||||
} catch let error where error is FilterError {
|
||||
throw RuntimeError.PullFailed("failed to decompress disk: \(error.localizedDescription)")
|
||||
}
|
||||
@@ -87,7 +95,7 @@ extension VMDirectory {
|
||||
try manifest.toJSON().write(to: manifestURL)
|
||||
}
|
||||
|
||||
func pushToRegistry(registry: Registry, references: [String], chunkSizeMb: Int, diskFormat: String, concurrency: UInt, labels: [String: String] = [:]) async throws -> RemoteName {
|
||||
func pushToRegistry(registry: Registry, references: [String], chunkSizeMb: Int, diskFormat: String, concurrency: UInt) async throws -> RemoteName {
|
||||
var layers = Array<OCIManifestLayer>()
|
||||
|
||||
// Read VM's config and push it as blob
|
||||
@@ -121,8 +129,7 @@ extension VMDirectory {
|
||||
layers.append(OCIManifestLayer(mediaType: nvramMediaType, size: nvram.count, digest: nvramDigest))
|
||||
|
||||
// Craft a stub OCI config for Docker Hub compatibility
|
||||
let ociConfigContainer = OCIConfig.ConfigContainer(Labels: labels)
|
||||
let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os, config: ociConfigContainer).toJSON()
|
||||
let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os).toJSON()
|
||||
let ociConfigDigest = try await registry.pushBlob(fromData: ociConfigJSON, chunkSizeMb: chunkSizeMb)
|
||||
let manifest = OCIManifest(
|
||||
config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest),
|
||||
|
||||
@@ -75,6 +75,8 @@ enum RuntimeError : Error {
|
||||
case SuspendFailed(_ message: String)
|
||||
case PullFailed(_ message: String)
|
||||
case VirtualMachineLimitExceeded(_ hint: String)
|
||||
case InvalidProxyString
|
||||
case FailedToLoadCACertificate(_ message: String)
|
||||
}
|
||||
|
||||
protocol HasExitCode {
|
||||
@@ -136,6 +138,10 @@ extension RuntimeError : CustomStringConvertible {
|
||||
return message
|
||||
case .VirtualMachineLimitExceeded(let hint):
|
||||
return "The number of VMs exceeds the system limit\(hint)"
|
||||
case .InvalidProxyString:
|
||||
return "Invalid proxy string, should be in the form of host:port"
|
||||
case .FailedToLoadCACertificate(let message):
|
||||
return "Failed to load CA certificate: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ class VMStorageOCI: PrunableStorage {
|
||||
try list().filter { (_, _, isSymlink) in !isSymlink }.map { (_, vmDir, _) in vmDir }
|
||||
}
|
||||
|
||||
func pull(_ name: RemoteName, registry: Registry, concurrency: UInt, deduplicate: Bool) async throws {
|
||||
func pull(_ name: RemoteName, registry: Registry, concurrency: UInt, deduplicate: Bool, maxRetries: UInt) async throws {
|
||||
SentrySDK.configureScope { scope in
|
||||
scope.setContext(value: ["imageName": name.description], key: "OCI")
|
||||
}
|
||||
@@ -196,7 +196,7 @@ class VMStorageOCI: PrunableStorage {
|
||||
}
|
||||
|
||||
try await withTaskCancellationHandler(operation: {
|
||||
try await retry(maxAttempts: 5) {
|
||||
try await retry(maxAttempts: Int(maxRetries)) {
|
||||
// Choose the best base image which has the most deduplication ratio
|
||||
let localLayerCache = try await chooseLocalLayerCache(name, manifest, registry)
|
||||
|
||||
@@ -210,7 +210,7 @@ class VMStorageOCI: PrunableStorage {
|
||||
}
|
||||
}
|
||||
|
||||
try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache, deduplicate: deduplicate)
|
||||
try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache, deduplicate: deduplicate, maxRetries: maxRetries)
|
||||
} recoverFromFailure: { error in
|
||||
if error is URLError {
|
||||
print("Error pulling image: \"\(error.localizedDescription)\", attempting to re-try...")
|
||||
|
||||
+28
-12
@@ -13,7 +13,7 @@ brew install go
|
||||
Finally, run the following command from this (`benchmark/`) directory:
|
||||
|
||||
```shell
|
||||
go run cmd/main.go fio --image ghcr.io/cirruslabs/macos-sequoia-base:latest --prepare 'sudo purge && sync'
|
||||
go run cmd/main.go fio --image ghcr.io/cirruslabs/macos-sonoma-base:latest --prepare 'sudo purge && sync'
|
||||
```
|
||||
|
||||
You can also enable the debugging output to diagnose issues:
|
||||
@@ -186,25 +186,41 @@ sync test Tart (--root-disk-opts="caching=cached"
|
||||
sync test Tart (--root-disk-opts="sync=none,caching=cached") 0 B/s 17 MB/s 0 IOPS 7.39 kIOPS 0s ± 0s 21.23µs ± 81.749µs 113.239µs ± 191.266µs
|
||||
```
|
||||
|
||||
### March 23, 2025
|
||||
### Jan 16, 2025
|
||||
|
||||
Host:
|
||||
|
||||
* Hardware: Mac mini (Apple M2 Pro, 8 performance and 4 efficiency cores, 32 GB RAM, `Mac14,12`)
|
||||
* OS: macOS Sequoia 15.3.2
|
||||
* Xcode: 16.2
|
||||
* OS: macOS Sequoia 15.2
|
||||
|
||||
Guest:
|
||||
|
||||
* Hardware: [Virtualization.Framework](https://developer.apple.com/documentation/virtualization)
|
||||
* OS: macOS Sonoma 15.3.2
|
||||
* Xcode: 16.2
|
||||
* OS: macOS Sonoma 14.6
|
||||
|
||||
```
|
||||
Name Executor Time
|
||||
XcodeBenchmark (d869315) local 2m19s
|
||||
XcodeBenchmark (d869315) Tart 3m59s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none") 3m48s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="caching=cached") 3m35s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none,caching=cached") 3m14s
|
||||
Name Executor Time
|
||||
XcodeBenchmark (d869315) local 2m15s
|
||||
XcodeBenchmark (d869315) Tart 4m22s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none") 4m21s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="caching=cached") 4m15s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none,caching=cached") 4m16s
|
||||
```
|
||||
|
||||
```
|
||||
Name Executor Time
|
||||
XcodeBenchmark (d869315) local 2m7s
|
||||
XcodeBenchmark (d869315) Tart 4m37s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none") 4m35s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="caching=cached") 4m19s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none,caching=cached") 4m16s
|
||||
```
|
||||
|
||||
```
|
||||
Name Executor Time
|
||||
XcodeBenchmark (d869315) local 2m6s
|
||||
XcodeBenchmark (d869315) Tart 4m24s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none") 4m22s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="caching=cached") 4m18s
|
||||
XcodeBenchmark (d869315) Tart (--root-disk-opts="sync=none,caching=cached") 4m17s
|
||||
```
|
||||
|
||||
+2
-6
@@ -1,24 +1,21 @@
|
||||
module github.com/cirruslabs/tart/benchmark
|
||||
|
||||
go 1.22.1
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/avast/retry-go/v4 v4.5.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -26,8 +23,7 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+6
-13
@@ -7,8 +7,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
|
||||
@@ -28,31 +26,26 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -8,6 +8,6 @@ type Benchmark struct {
|
||||
var benchmarks = []Benchmark{
|
||||
{
|
||||
Name: "XcodeBenchmark (d869315)",
|
||||
Command: "git clone https://github.com/devMEremenko/XcodeBenchmark.git && cd XcodeBenchmark && git reset --hard d86931529ada1df2a1c6646dd85958c360954065 && xcrun simctl list && sh benchmark.sh",
|
||||
Command: "git clone https://github.com/devMEremenko/XcodeBenchmark.git && cd XcodeBenchmark && git reset --hard d86931529ada1df2a1c6646dd85958c360954065 && sh benchmark.sh",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&debug, "debug", false, "enable debug logging")
|
||||
cmd.Flags().StringVar(&image, "image", "ghcr.io/cirruslabs/macos-sequoia-xcode:latest", "image to use for testing")
|
||||
cmd.Flags().StringVar(&image, "image", "ghcr.io/cirruslabs/macos-sonoma-xcode:latest", "image to use for testing")
|
||||
cmd.Flags().StringVar(&prepare, "prepare", "", "command to run before running each benchmark")
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -7,14 +7,11 @@ import (
|
||||
"fmt"
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/google/uuid"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapio"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -40,23 +37,6 @@ func New(ctx context.Context, image string, runArgsExtra []string, logger *zap.L
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vmStat, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cpus := strconv.Itoa(runtime.NumCPU())
|
||||
memory := strconv.FormatUint(vmStat.Total/1024/1024, 10)
|
||||
logger.Info("Setting resources", zap.String("cpus", cpus), zap.String("memory", memory))
|
||||
setResourcesArguments := []string{
|
||||
"set", tart.vmName,
|
||||
"--cpu", cpus,
|
||||
"--memory", memory,
|
||||
}
|
||||
if err := Cmd(ctx, tart.logger, setResourcesArguments...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vmRunCtx, vmRunCancel := context.WithCancel(ctx)
|
||||
tart.vmRunCancel = vmRunCancel
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB |
-67
@@ -177,70 +177,3 @@ export TART_NO_AUTO_PRUNE=
|
||||
```shell
|
||||
TART_NO_AUTO_PRUNE= tart pull ...
|
||||
```
|
||||
|
||||
## Disk resizing
|
||||
|
||||
Disk resizing works on most cloud-ready Linux distributions out-of-the box (e.g. Ubuntu Cloud Images have the `cloud-initramfs-growroot` package installed that runs on boot) and on the rest of the distributions by running the `growpart` or `resize2fs` commands.
|
||||
|
||||
For macOS, however, things are a bit more complicated, and you generally have two options: automated and manual resizing.
|
||||
|
||||
For the automated option, you can use [Packer](https://www.packer.io/) with the [Packer builder for Tart VMs](https://developer.hashicorp.com/packer/integrations/cirruslabs/tart/latest/components/builder/tart). The latter has two has configuration directives related to the disk resizing behavior:
|
||||
|
||||
* [`disk_size_gb`](https://developer.hashicorp.com/packer/integrations/cirruslabs/tart/latest/components/builder/tart#configuration-reference) — controls the target disk size in gigabytes
|
||||
* [`recovery_partition`](https://developer.hashicorp.com/packer/integrations/cirruslabs/tart/latest/components/builder/tart#configuration-reference) — controls what to do with the recovery partition when resizing the disk
|
||||
* you can either keep, delete or relocate it to the end of the disk
|
||||
|
||||
For the manual approach, you have to remove the recovery partition first, repair the disk and the resize the APFS container.
|
||||
|
||||
To do this, first we'll need to identify the primary disk and the APFS containers by running the command below from within a VM:
|
||||
|
||||
```shell
|
||||
diskutil list physical
|
||||
```
|
||||
|
||||
For example, the output might look like this:
|
||||
|
||||
```plain
|
||||
/dev/disk0 (internal, physical):
|
||||
#: TYPE NAME SIZE IDENTIFIER
|
||||
0: GUID_partition_scheme *100.0 GB disk0
|
||||
1: Apple_APFS_ISC Container disk1 524.3 MB disk0s1
|
||||
2: Apple_APFS Container disk3 44.1 GB disk0s2
|
||||
3: Apple_APFS_Recovery Container disk2 5.4 GB disk0s3
|
||||
(free space) 50.0 GB -
|
||||
```
|
||||
|
||||
In the output, you'll normally see:
|
||||
|
||||
* a single physical disk (`disk0`)
|
||||
* APFS container with the system partition which we're going to resize (`disk0s2`)
|
||||
* APFS container with the recovery partition which we're going to delete (`disk0s3`)
|
||||
* `(free space)` which we'll put to use
|
||||
|
||||
To proceed, boot the VM in recovery mode using `tart run --recovery` and choose the "Options" item:
|
||||
|
||||
{width="640" .center}
|
||||
|
||||
When the recovery OS boots, open the Terminal app:
|
||||
|
||||
{width="720" .center}
|
||||
|
||||
In Terminal app, invoke the command below to remove the recovery partition:
|
||||
|
||||
```shell
|
||||
diskutil eraseVolume free free disk0s3
|
||||
```
|
||||
|
||||
Now, repair the disk:
|
||||
|
||||
```shell
|
||||
yes | diskutil repairDisk disk0
|
||||
```
|
||||
|
||||
Finally, resize the system APFS container to take all the remaining space:
|
||||
|
||||
```shell
|
||||
diskutil apfs resizeContainer disk0s2 0
|
||||
```
|
||||
|
||||
Now, you can shut down and `tart run` as you'd normally do.
|
||||
|
||||
@@ -51,6 +51,8 @@ Then, create a launchd job definition in `/Library/LaunchDaemons/org.cirruslabs.
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.cirruslabs.orchard.worker</string>
|
||||
<key>UserName</key>
|
||||
<string>admin</string>
|
||||
<key>Program</key>
|
||||
<string>/opt/homebrew/bin/orchard</string>
|
||||
<key>ProgramArguments</key>
|
||||
@@ -58,8 +60,6 @@ Then, create a launchd job definition in `/Library/LaunchDaemons/org.cirruslabs.
|
||||
<string>/opt/homebrew/bin/orchard</string>
|
||||
<string>worker</string>
|
||||
<string>run</string>
|
||||
<string>--user</string>
|
||||
<string>admin</string>
|
||||
<string>--bootstrap-token</string>
|
||||
<string>${BOOTSTRAP_TOKEN}</string>
|
||||
<string>orchard.example.com</string>
|
||||
|
||||
Reference in New Issue
Block a user