Compare commits

...
16 Commits
Author SHA1 Message Date
Nikolay Edigaryev 1e74e268a5 DiskV2: change layer size to 512 * 1024 * 1024 bytes (#866)
Needed to test https://github.com/cirruslabs/tart/pull/864.
2024-07-17 16:37:45 +00:00
Nikolay Edigaryev bff344fb7f tart login: better error when an improperly formatted host is provided (#863)
* tart login: better error when an improperly formatted host is provided

* Revert old behavior w.r.t. URLComponents()
2024-07-15 18:36:09 +04:00
Nikolay Edigaryev ababe8cefc tart pull: choose across multiple VM images to deduplicate against (#862)
This is accomplished by saving the OCI VM image manifests on "tart pull"
in "manifest.json" file and then using them on successive "tart pull"'s
to find the best candidate that results in the most de-duplication,
measured in bytes.
2024-07-15 18:36:01 +04:00
Fedor Korotkov ea5313698e Do not prune running VMs (#861)
Also prevent pushing of a running VM

Fixes #860
2024-07-15 07:10:51 -04:00
Fedor Korotkov 679289d7ab Added Figma as a user (#858)
See https://www.figma.com/open-source/

Plus reworked users section since now we can curate the best representative.
2024-07-10 15:06:01 +00:00
Nikolay Edigaryev 5eccdf7412 Support customizing VM disks and mounting remote VMs in tart run (#847)
* Support remote VM names in --disk command-line argument

* tart set: introduce "--disk" to support replacing VM's disk contents

* Complete the code comment
2024-07-02 18:12:35 +04:00
Nikolay Edigaryev 63e3235d91 tart run: pick up --net-softnet-allow when using --net-host (#853) 2024-07-02 16:39:51 +04:00
Nikolay Edigaryev a760a431c3 Jumping through the hoops: SSH jump host functionality in Orchard (#844) 2024-06-20 22:39:41 +00:00
Tor Arne Vestbø bf5081b3d9 Hook SIGUSR2 to requestStop (#842)
For macOS this brings up a dialog, asking the user if they are sure
they want to shut down, which makes this less useful for automated
graceful shutdowns, but it may behave better on Linux, and there
might be ways to instruct macOS to not ask the user, so it's still
a nice feature, and aligns with the SIGUSR1 for suspend, and SIGINT
for non-graceful shutdown.
2024-06-17 12:11:00 -04:00
Fedor Korotkov 017592075f Drop Monterey Support (#843)
* Drop Monterey Support

People will still be able to run and SSH into Monterey VMs or use VNC but pointing devices/keyboard won't work.

Fixes #841

* Fixed x86 build
2024-06-17 15:20:21 +00:00
Fedor Korotkov 84e1ae2b38 Fixed GoReleaser 2.0.0 (#839)
GoReleaser changes some flags
2024-06-05 19:09:56 +04:00
Fedor Korotkov d50e113300 Rearrange companies
To make the patter pretty
2024-06-05 08:32:38 -04:00
marc fce52f1514 Add Atlassian as Tart user (#838) 2024-06-05 08:31:23 -04:00
Fedor Korotkov 9484b8b2c9 Update Sentry Token (#836)
The latest release has this error:

> error: Project not found. Please check that you entered the project and organization slugs correctly.

Which seems indicating that Auth is broken and we are getting 404? In the Sentry Settings I didn't find any token which I find strange. So I created one and re-encrypted.
2024-06-01 13:11:05 +00:00
Fedor Korotkov dd46033812 Friendly decompression error message (#835)
* Friendly decompression error message

* Wrap FilterError
2024-05-31 14:47:41 +00:00
Fedor Korotkov c655288de7 Fancy Social Cards (#830) 2024-05-22 16:07:08 +04:00
36 changed files with 545 additions and 83 deletions
+2 -2
View File
@@ -90,7 +90,7 @@ task:
- security find-identity -v
- xcodebuild -version
- swift -version
goreleaser_script: goreleaser release --skip-publish --snapshot --clean
goreleaser_script: goreleaser release --skip=publish --snapshot --clean
always:
dist_artifacts:
path: "dist/*"
@@ -111,7 +111,7 @@ task:
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
SENTRY_ORG: cirrus-labs
SENTRY_PROJECT: persistent-workers
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
SENTRY_AUTH_TOKEN: ENCRYPTED[!9eaf2875d51b113e2f68598441ff8e6b2e53242e48fcb93633bd75a373fbe2e7caa900d837cc92f0b142b65579731644!]
setup_script:
- cd $HOME
- echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
+3
View File
@@ -16,3 +16,6 @@ dist/
# mkdocs
.cache
# mkdocs-material
site
+11 -11
View File
@@ -20,20 +20,20 @@ service — a drop-in replacement for the standard GitHub-hosted runners, offeri
Many companies are using Tart in their internal setups. Here are a few of them:
<p align="center">
<a href="https://ahrefs.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/ahrefs.png" height="65"/>
<a href="https://atlassian.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Atlassian.png" height="65"/>
</a>
<a href="https://krisp.ai/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Krisp.png" height="65"/>
<a href="https://www.figma.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Figma.png" height="65"/>
</a>
<a href="https://mullvad.net/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Mullvad.png" height="65"/>
</a>
<a href="https://shape.dk/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/shape.png" height="65"/>
<a href="https://krisp.ai/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Krisp.png" height="65"/>
</a>
<a href="https://suran.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Suran.png" height="65"/>
<a href="https://testingbot.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/TestingBot.png" height="65"/>
</a>
<a href="https://symflower.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Symflower.png" height="65"/>
@@ -41,15 +41,15 @@ Many companies are using Tart in their internal setups. Here are a few of them:
<a href="https://transloadit.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Transloadit.png" height="65"/>
</a>
<a href="https://uphold.com/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Uphold.png" height="65"/>
<a href="https://cirrus-ci.org/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/CirrusCI.png" height="65"/>
</a>
<a href="https://www.pitsdatarecovery.net/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/PITSGlobalDataRecoveryServices.png" height="65"/>
</a>
</p>
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
**Note:** If your company or project is using Tart please consider [sharing with the community](https://github.com/cirruslabs/tart/discussions/857).
<p align="center">
<a href="https://aws.amazon.com/marketplace/pp/prodview-qczco34wlkdws?utm_source=github&utm_medium=referral" target=_blank>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

-4
View File
@@ -1,4 +0,0 @@
If you'd like to highlight your use of Tart, please create a `456px` by `130px` logo and create a PR
that adds it to `README.md` in alphabetical order. Don't forget to include a small description of your usage pattern.
You can refer to `Background.png` as a base for your logo.
+3 -2
View File
@@ -49,9 +49,10 @@ struct Login: AsyncParsableCommand {
])
if !noValidate {
let registry = try Registry(host: host, namespace: "", insecure: insecure,
credentialsProviders: [credentialsProvider])
do {
let registry = try Registry(host: host, namespace: "", insecure: insecure,
credentialsProviders: [credentialsProvider])
try await registry.ping()
} catch {
throw RuntimeError.InvalidCredentials("invalid credentials: \(error)")
+4
View File
@@ -33,6 +33,10 @@ struct Push: AsyncParsableCommand {
func run() async throws {
let ociStorage = VMStorageOCI()
let localVMDir = try VMStorageHelper.open(localName)
let lock = try localVMDir.lock()
if try !lock.trylock() {
throw RuntimeError.VMIsRunning(localName)
}
// Parse remote names supplied by the user
let remoteNames = try remoteNames.map{
+49 -13
View File
@@ -67,12 +67,16 @@ struct Run: AsyncParsableCommand {
var vncExperimental: Bool = false
@Option(help: ArgumentHelp("""
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\" --disk=\"/dev/disk0\" --disk=\"nbd://localhost:10809/myDisk\")
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\" --disk=\"/dev/disk0\" --disk "ghcr.io/cirruslabs/xcode:16.0:ro" --disk=\"nbd://localhost:10809/myDisk\")
""", discussion: """
Can be either a disk image file, a block device like a local SSD on AWS EC2 Mac instances or a Network Block Device (NBD).
The disk attachment can be a:
Learn how to create a disk image using Disk Utility here:
https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
* path to a disk image file
* path to a block device (for example, a local SSD on AWS EC2 Mac instances)
* remote VM name whose disk will be mounted
* Network Block Device (NBD) URL
Learn how to create a disk image using Disk Utility here: https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
To work with block devices, the easiest way is to modify their permissions (e.g. by using "sudo chown $USER /dev/diskX") or to run the Tart binary as root, which affects locating Tart VMs.
@@ -172,7 +176,7 @@ struct Run: AsyncParsableCommand {
if suspendable {
let config = try VMConfig.init(fromURL: vmDir.configURL)
if !(config.platform is PlatformSuspendable) {
if (config.platform is Linux) {
throw ValidationError("You can only suspend macOS VMs")
}
if dir.count > 0 {
@@ -359,6 +363,18 @@ struct Run: AsyncParsableCommand {
}
sigusr1Src.activate()
// Gracefull shutdown support. For macOS this brings up a dialog,
// asking the user if they are sure they want to shut down.
signal(SIGUSR2, SIG_IGN)
let sigusr2Src = DispatchSource.makeSignalSource(signal: SIGUSR2)
sigusr2Src.setEventHandler {
Task {
print("Requesting guest OS to stop...")
try vm!.virtualMachine.requestStop()
}
}
sigusr2Src.activate()
let useVNCWithoutGraphics = (vnc || vncExperimental) && !graphics
if noGraphics || useVNCWithoutGraphics {
// enter the main even loop, without bringing up any UI,
@@ -384,21 +400,22 @@ struct Run: AsyncParsableCommand {
}
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
var softnetExtraArguments: [String] = []
if let netSoftnetAllow = netSoftnetAllow {
softnetExtraArguments += ["--allow", netSoftnetAllow]
}
if netSoftnet {
let config = try VMConfig.init(fromURL: vmDir.configURL)
var extraArguments: [String] = []
if let netSoftnetAllow = netSoftnetAllow {
extraArguments += ["--allow", netSoftnetAllow]
}
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: extraArguments)
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: softnetExtraArguments)
}
if netHost {
let config = try VMConfig.init(fromURL: vmDir.configURL)
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"])
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"] + softnetExtraArguments)
}
if netBridged.count > 0 {
@@ -483,6 +500,25 @@ struct Run: AsyncParsableCommand {
continue
}
// Support remote VM names in --disk command-line argument
if let remoteName = try? RemoteName(diskPath) {
let vmDir = try VMStorageOCI().open(remoteName)
// Unfortunately, VZDiskImageStorageDeviceAttachment does not support
// FileHandle, so we can't easily clone the disk, open it and unlink(2)
// to simplify the garbage collection, so use an intermediate directory.
let clonedDiskURL = try Config().tartTmpDir.appendingPathComponent("run-disk-\(UUID().uuidString)")
try FileManager.default.copyItem(at: vmDir.diskURL, to: clonedDiskURL)
let lock = try FileLock(lockURL: clonedDiskURL)
try lock.lock()
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: clonedDiskURL, readOnly: diskReadOnly)
result.append(VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment))
continue
}
// Error out if the disk is locked by the host (e.g. it was mounted in Finder),
// see https://github.com/cirruslabs/tart/issues/323 for more details.
if try !diskReadOnly && !FileLock(lockURL: diskFileURL).trylock() {
+11
View File
@@ -25,6 +25,9 @@ struct Set: AsyncParsableCommand {
#endif
var randomSerial: Bool = false
@Option(help: ArgumentHelp("Replace the VM's disk contents with the disk contents at path.", valueName: "path"))
var disk: String?
@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: """
Disk resizing works on most cloud-ready Linux distributions out-of-the box (e.g. Ubuntu Cloud Images
@@ -73,6 +76,14 @@ struct Set: AsyncParsableCommand {
try vmConfig.save(toURL: vmDir.configURL)
if let disk = disk {
let temporaryDiskURL = try Config().tartTmpDir.appendingPathComponent("set-disk-\(UUID().uuidString)")
try FileManager.default.copyItem(atPath: disk, toPath: temporaryDiskURL.path())
_ = try FileManager.default.replaceItemAt(vmDir.diskURL, withItemAt: temporaryDiskURL)
}
if diskSize != nil {
try vmDir.resizeDisk(diskSize!)
}
+1 -1
View File
@@ -3,7 +3,7 @@ import Compression
class DiskV2: Disk {
private static let bufferSizeBytes = 4 * 1024 * 1024
private static let layerLimitBytes = 500 * 1000 * 1000
private static let layerLimitBytes = 512 * 1024 * 1024
static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, progress: Progress) async throws -> [OCIManifestLayer] {
var pushedLayers: [OCIManifestLayer] = []
+9 -1
View File
@@ -78,7 +78,7 @@ struct OCIManifestConfig: Codable, Equatable {
var digest: String
}
struct OCIManifestLayer: Codable, Equatable {
struct OCIManifestLayer: Codable, Equatable, Hashable {
var mediaType: String
var size: Int
var digest: String
@@ -113,6 +113,14 @@ struct OCIManifestLayer: Codable, Equatable {
func uncompressedContentDigest() -> String? {
annotations?[uncompressedContentDigestAnnotation]
}
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.digest == rhs.digest
}
func hash(into hasher: inout Hasher) {
hasher.combine(digest)
}
}
struct Descriptor: Equatable {
+13 -3
View File
@@ -112,11 +112,11 @@ class Registry {
return host
}
init(urlComponents: URLComponents,
init(baseURL: URL,
namespace: String,
credentialsProviders: [CredentialsProvider] = [EnvironmentCredentialsProvider(), DockerConfigCredentialsProvider(), KeychainCredentialsProvider()]
) throws {
baseURL = urlComponents.url!
self.baseURL = baseURL
self.namespace = namespace
self.credentialsProviders = credentialsProviders
}
@@ -130,7 +130,17 @@ class Registry {
let proto = insecure ? "http" : "https"
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProviders: credentialsProviders)
guard let baseURL = baseURLComponents.url else {
var hint = ""
if host.hasPrefix("http://") || host.hasPrefix("https://") {
hint += ", make sure that it doesn't start with http:// or https://"
}
throw RuntimeError.ImproperlyFormattedHost(host, hint)
}
try self.init(baseURL: baseURL, namespace: namespace, credentialsProviders: credentialsProviders)
}
func ping() async throws {
+4 -18
View File
@@ -8,7 +8,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
#if arch(arm64)
struct Darwin: PlatformSuspendable {
struct Darwin: Platform {
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel
@@ -103,32 +103,18 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
func keyboards() -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) {
// Mac keyboard is only supported by guests starting with macOS Ventura
return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
return [VZMacKeyboardConfiguration()]
} else {
return [VZUSBKeyboardConfiguration()]
}
}
func keyboardsSuspendable() -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) {
return [VZMacKeyboardConfiguration()]
} else {
// fallback to the regular configuration
return keyboards()
}
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
// Trackpad is only supported by guests starting with macOS Ventura
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
}
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
if #available(macOS 14, *) {
if #available(macOS 13, *) {
return [VZMacTrackpadConfiguration()]
} else {
// fallback to the regular configuration
return pointingDevices()
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
}
}
-5
View File
@@ -8,8 +8,3 @@ protocol Platform: Codable {
func keyboards() -> [VZKeyboardConfiguration]
func pointingDevices() -> [VZPointingDeviceConfiguration]
}
protocol PlatformSuspendable: Platform {
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration]
func keyboardsSuspendable() -> [VZKeyboardConfiguration]
}
+2 -7
View File
@@ -323,13 +323,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
// Keyboard and mouse
if suspendable, let platformSuspendable = vmConfig.platform.self as? PlatformSuspendable {
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
} else {
configuration.keyboards = vmConfig.platform.keyboards()
configuration.pointingDevices = vmConfig.platform.pointingDevices()
}
configuration.keyboards = vmConfig.platform.keyboards()
configuration.pointingDevices = vmConfig.platform.pointingDevices()
// Networking
configuration.networkDevices = network.attachments().map {
+11 -2
View File
@@ -1,3 +1,4 @@
import Compression
import Foundation
import Sentry
@@ -53,8 +54,13 @@ extension VMDirectory {
let progress = Progress(totalUnitCount: diskCompressedSize)
ProgressObserver(progress).log(defaultLogger)
try await diskImplType.pull(registry: registry, diskLayers: layers, diskURL: diskURL, concurrency: concurrency, progress: progress,
localLayerCache: localLayerCache)
do {
try await diskImplType.pull(registry: registry, diskLayers: layers, diskURL: diskURL,
concurrency: concurrency, progress: progress,
localLayerCache: localLayerCache)
} catch let error where error is FilterError {
throw RuntimeError.PullFailed("failed to decompress disk: \(error.localizedDescription)")
}
// Pull VM's NVRAM file layer and store it in an NVRAM file
defaultLogger.appendNewLine("pulling NVRAM...")
@@ -73,6 +79,9 @@ extension VMDirectory {
nvram.write(data)
}
try nvram.close()
// Serialize VM's manifest to enable better de-duplication on subsequent "tart pull"'s
try manifest.toJSON().write(to: manifestURL)
}
func pushToRegistry(registry: Registry, references: [String], chunkSizeMb: Int, diskFormat: String) async throws -> RemoteName {
+3
View File
@@ -23,6 +23,9 @@ struct VMDirectory: Prunable {
var stateURL: URL {
baseURL.appendingPathComponent("state.vzvmsave")
}
var manifestURL: URL {
baseURL.appendingPathComponent("manifest.json")
}
var explicitlyPulledMark: URL {
baseURL.appendingPathComponent(".explicitly-pulled")
+6
View File
@@ -61,6 +61,7 @@ enum RuntimeError : Error {
case PIDLockFailed(_ message: String)
case FailedToParseRemoteName(_ message: String)
case VMTerminationFailed(_ message: String)
case ImproperlyFormattedHost(_ host: String, _ hint: String)
case InvalidCredentials(_ message: String)
case VMDirectoryAlreadyInitialized(_ message: String)
case ExportFailed(_ message: String)
@@ -69,6 +70,7 @@ enum RuntimeError : Error {
case OCIStorageError(_ message: String)
case OCIUnsupportedDiskFormat(_ format: String)
case SuspendFailed(_ message: String)
case PullFailed(_ message: String)
}
protocol HasExitCode {
@@ -106,6 +108,8 @@ extension RuntimeError : CustomStringConvertible {
return "failed to parse remote name: \(cause)"
case .VMTerminationFailed(let message):
return message
case .ImproperlyFormattedHost(let host, let hint):
return "improperly formatted host \"\(host)\" was provided\(hint)"
case .InvalidCredentials(let message):
return message
case .VMDirectoryAlreadyInitialized(let message):
@@ -122,6 +126,8 @@ extension RuntimeError : CustomStringConvertible {
return "OCI disk format \(format) is not supported by this version of Tart"
case .SuspendFailed(let message):
return "Failed to suspend the VM: \(message)"
case .PullFailed(let message):
return message
}
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ class VMStorageLocal: PrunableStorage {
}
func prunables() throws -> [Prunable] {
try list().map { (_, vmDir) in vmDir }
try list().map { (_, vmDir) in vmDir }.filter { try !$0.running() }
}
func hasVMsWithMACAddress(macAddress: String) throws -> Bool {
+53 -8
View File
@@ -197,14 +197,8 @@ class VMStorageOCI: PrunableStorage {
try await withTaskCancellationHandler(operation: {
try await retry(maxAttempts: 5, backoff: .exponentialWithFullJitter(baseDelay: .seconds(5), maxDelay: .seconds(60))) {
var localLayerCache: LocalLayerCache? = nil
if name.reference.type == .Tag,
let vmDir = try? open(name),
let digest = try? digest(name),
let (manifest, _) = try? await registry.pullManifest(reference: digest) {
localLayerCache = try LocalLayerCache(vmDir.diskURL, manifest)
}
// Choose the best base image which has the most deduplication ratio
let localLayerCache = try await chooseLocalLayerCache(name, manifest, registry)
try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache)
} recoverFromFailure: { error in
@@ -249,6 +243,57 @@ class VMStorageOCI: PrunableStorage {
try gc()
}
func chooseLocalLayerCache(_ name: RemoteName, _ manifest: OCIManifest, _ registry: Registry) async throws -> LocalLayerCache? {
// Establish a closure that will calculate how much bytes
// we'll de-duplicate if we re-use the given manifest
let target = Swift.Set(manifest.layers)
let calculateDeduplicatedBytes = { (manifest: OCIManifest) -> Int in
target.intersection(manifest.layers).map({ $0.size }).reduce(0, +)
}
// Load OCI VM images and their manifests (if present)
var candidates: [(name: String, vmDir: VMDirectory, manifest: OCIManifest, deduplicatedBytes: Int)] = []
for (name, vmDir, isSymlink) in try list() {
if isSymlink {
continue
}
guard let manifestJSON = try? Data(contentsOf: vmDir.manifestURL) else {
continue
}
guard let manifest = try? OCIManifest(fromJSON: manifestJSON) else {
continue
}
candidates.append((name, vmDir, manifest, calculateDeduplicatedBytes(manifest)))
}
// Previously we haven't stored the OCI VM image manifests, but still fetched the VM image manifest if
// what the user was trying to pull was a tagged image, and we already had that image in the OCI VM cache
//
// Keep supporting this behavior for backwards comaptibility, but only communicate
// with the registry if we haven't already retrieved the manifest for that OCI VM image.
if name.reference.type == .Tag,
let vmDir = try? open(name),
let digest = try? digest(name),
try !candidates.contains(where: {try $0.manifest.digest() == digest}),
let (manifest, _) = try? await registry.pullManifest(reference: digest) {
candidates.append((name.description, vmDir, manifest, calculateDeduplicatedBytes(manifest)))
}
// Now, find the best match based on how many bytes we'll de-duplicate
let choosen = candidates.max { left, right in
return left.deduplicatedBytes < right.deduplicatedBytes
}
return try choosen.flatMap({ choosen in
try LocalLayerCache(choosen.vmDir.diskURL, choosen.manifest)
})
}
}
extension URL {
+1 -1
View File
@@ -39,7 +39,7 @@ class RegistryRunner {
let port = try Self.dockerCmd("inspect", containerID, "--format", "{{(index (index .NetworkSettings.Ports \"5000/tcp\") 0).HostPort}}")
.trimmingCharacters(in: CharacterSet.newlines)
registry = try Registry(urlComponents: URLComponents(string: "http://127.0.0.1:\(port)/v2/")!,
registry = try Registry(baseURL: URL(string: "http://127.0.0.1:\(port)/v2/")!,
namespace: "vm-image")
// Wait for the Docker Registry to start
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -0,0 +1,62 @@
---
draft: false
date: 2024-06-20
search:
exclude: true
authors:
- edigaryev
categories:
- orchard
---
# Jumping through the hoops: SSH jump host functionality in Orchard
Almost a year ago, when we started building [Orchard](https://github.com/cirruslabs/orchard), an orchestration system for Tart, we quickly realized that most worker machines will be in a private network, and that VMs will be only reachable from the worker machines themselves. Thus, one of our goals became to simplify accessing the compute resources in a cluster through a centralized controller host.
This effort resulted in commands like `orchard port-forward` and `orchard ssh`, which were later improved to support connecting not just to the VMs, but to the worker machines themselves.
Today, we’re making an even further step in this effort: with a trivial configuration, an Orchard controller can act as an SSH jump host to allow connecting to the VMs using just the `ssh` command like `ssh -J <service account name>@orchard-controller.example.com <VM name>`!
<!-- more -->
## Implementation
In a typical cluster there’s one controller, to which workers connect by calling various REST API endpoints to synchronize the worker & VMs state. Each worker also maintains a persistent bi-directional gRPC connection with the controller, with the goal of improving the overall reactivity and making the port-forwarding work.
The gRPC service definition that the controller offers is pretty minimalistic:
```protobuf
service Controller {
rpc Watch(google.protobuf.Empty) returns (stream WatchInstruction);
rpc PortForward(stream PortForwardData) returns (stream PortForwardData);
}
```
Each watch instruction corresponds a single action to be done by the worker, which can either be a request for establishing a port-forwarding stream or a request for VMs re-syncing:
```protobuf
oneof action {
PortForward port_forward_action = 1;
SyncVMs sync_vms_action = 2;
}
```
Now, when the user invokes `orchard port-forward` or `orchard ssh`, controller effectively becomes a rendezvous point by accepting the WebSocket connection from the user, and then asking the worker associated with the requested VM to establish a port-forwarding stream, and finally proxying the two streams together.
![An illustration showing the Orchard controller and worker proxying the SSH connection](../images/jumping-through-the-hoops.png)
SSH protocol works the same way, multiplexing multiple channels in a single transport connection, where each channel can be upgraded either to an interactive session (that’s what you get when you `ssh` to the server) or X11 channel (for X11 forwarding using `-X`), direct or forward TCP/IP channels (these are used for local and remote port-forwarding when using `-L` and `-R` options correspondingly) and so on.
In fact, `ssh -J` jump host functionality also uses the direct TCP/IP channel, which is [just a single port-forwarding request](https://datatracker.ietf.org/doc/html/rfc4254#section-7.2) that needs to be implemented. We’ve used [Golang's SSH library](https://pkg.go.dev/golang.org/x/crypto/ssh) as the most mature choice for this task, and it’s been pleasant to work with so far.
The support for `ssh -J` has landed in Orchard version 0.19.0. To configure the SSH jump host, simply add the `--listen-ssh` command-line argument to your `orchard controller run` invocation.
Once running, you can connect to any VM in the cluster using the `ssh -J <service account name>@orchard-controller.example.com <VM name>`. The password for the jump host is the corresponding service account’s token.
## Future plans
First of all, we’d like to thank our paid clients, without which this feature wouldn’t be possible. [Become one now](../../licensing.md) and get the benefit of higher Tart VMs and Orchard workers allowances and making sure that the roadmap for Tart and Orchard is aligned with your company's needs.
In the near future we plan to implement a mechanism similar to `authorized_keys` file that will allow attaching public SSH keys to the Orchard controller’s service accounts, and thus avoid the need to type the passwords.
Stay tuned and don’t hesitate to send us your feedback on [GitHub](https://github.com/cirruslabs/orchard) and [Twitter](https://x.com/cirrus_labs)!
+2
View File
@@ -1,6 +1,8 @@
---
hide:
- navigation
title: Frequently Asked Questions
description: Advanced configuration and troubleshooting tips for advanced configurations.
---
## VM location on disk
+2 -1
View File
@@ -1,4 +1,5 @@
---
template: overrides/home.html
title: Tart
title: Toolset to build, run and manage macOS and Linux VMs
description: Native performance. Remote storage for Virtual Machines. Many integrations including GitHub, GitLab and more.
---
+5
View File
@@ -1,3 +1,8 @@
---
title: Buildkite Integration
description: Run pipeline steps in isolated ephemeral Tart Virtual Machines.
---
# Buildkite
It is possible to run [Buildkite](https://buildkite.com/) pipeline steps in isolated ephemeral Tart Virtual Machines with the help of [Tart Buildkite Plugin](https://github.com/cirruslabs/tart-buildkite-plugin):
+5
View File
@@ -1,3 +1,8 @@
---
title: Cirrus CLI
description: Tool for running isolated tasks reproducibly in any environment with a simple YAML configuration.
---
# Cirrus CLI
Tart itself is only responsible for managing virtual machines, but we've built Tart support into a tool called Cirrus CLI
+5
View File
@@ -1,3 +1,8 @@
---
title: GitLab Runner Executor
description: Run jobs in isolated ephemeral Tart Virtual Machines.
---
# GitLab Runner Executor
It is possible to run GitLab jobs in isolated ephemeral Tart Virtual Machines via [Tart Executor](https://github.com/cirruslabs/gitlab-tart-executor).
+5
View File
@@ -1,3 +1,8 @@
---
title: Managing Virtual Machine
description: Use Packer to build custom VM images, configure VMs and work with remote OCI registries.
---
# Managing Virtual Machine
## Creating from scratch
+260
View File
@@ -0,0 +1,260 @@
# Copyright (c) 2016-2024 Martin Donath <martin.donath@squidfunk.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------
# Definitions
definitions:
# Background image
- &background_image >-
{{ layout.background_image | x }}
# Background color (default: indigo)
- &background_color >-
{%- if layout.background_color -%}
{{ layout.background_color }}
{%- else -%}
{%- set palette = config.theme.palette or {} -%}
{%- if not palette is mapping -%}
{%- set list = palette | selectattr("accent") | list + palette -%}
{%- set palette = list | first -%}
{%- endif -%}
{%- set accent = palette.get("accent", "indigo") -%}
{%- set accent = accent.replace(" ", "-") -%}
{{ {
"red": "#ff1a47",
"pink": "#f50056",
"purple": "#df41fb",
"deep-purple": "#7c4dff",
"indigo": "#526cfe",
"blue": "#4287ff",
"light-blue": "#0091eb",
"cyan": "#00bad6",
"teal": "#00bda4",
"green": "#00c753",
"light-green": "#63de17",
"lime": "#b0eb00",
"yellow": "#ffd500",
"amber": "#ffaa00",
"orange": "#ff9100",
"deep-orange": "#ff6e42"
}[accent] or "#4051b5" }}
{%- endif -%}
# Text color (default: white)
- &color >-
{%- if layout.color -%}
{{ layout.color }}
{%- else -%}
{%- set palette = config.theme.palette or {} -%}
{%- if not palette is mapping -%}
{%- set list = palette | selectattr("accent") | list + palette -%}
{%- set palette = list | first -%}
{%- endif -%}
{%- set accent = palette.get("accent", "indigo") -%}
{%- set accent = accent.replace(" ", "-") -%}
{{ {
"red": "#ffffff",
"pink": "#ffffff",
"purple": "#ffffff",
"deep-purple": "#ffffff",
"indigo": "#ffffff",
"blue": "#ffffff",
"light-blue": "#ffffff",
"cyan": "#ffffff",
"teal": "#ffffff",
"green": "#ffffff",
"light-green": "#ffffff",
"lime": "#000000",
"yellow": "#000000",
"amber": "#000000",
"orange": "#000000",
"deep-orange": "#ffffff"
}[accent] or "#ffffff" }}
{%- endif -%}
# Font family (default: Roboto)
- &font_family >-
{%- if layout.font_family -%}
{{ layout.font_family }}
{%- elif config.theme.font != false -%}
{{ config.theme.font.get("text", "Roboto") }}
{%- else -%}
Roboto
{%- endif -%}
# Font variant
- &font_variant >-
{%- if layout.font_variant -%}
{{ layout.font_variant }}
{%- endif -%}
# Site name
- &site_name >-
{{ config.site_name }}
# Page title
- &page_title >-
{%- if page.meta.no_title_in_card -%}
{# do not show anything #}
{%- elif layout.title -%}
{{ layout.title }}
{%- else -%}
{{ page.meta.get("title", page.title) }}
{%- endif -%}
# Page title with site name
- &page_title_with_site_name >-
{%- if not page.is_homepage -%}
{{ page.meta.get("title", page.title) }} - {{ config.site_name }}
{%- else -%}
{{ page.meta.get("title", page.title) }}
{%- endif -%}
# Page description
- &page_description >-
{%- if layout.description -%}
{{ layout.description }}
{%- else -%}
{{ page.meta.get("description", config.site_description) | x }}
{%- endif -%}
# Page description for social card
- &page_description_social_card >-
{%- if layout.description -%}
{{ layout.description }}
{%- else -%}
{{ page.meta.get("description", config.site_description_social_card) | x }}
{%- endif -%}
# Logo
- &logo >-
{%- if layout.logo -%}
{{ layout.logo }}
{%- elif config.theme.logo -%}
{{ config.docs_dir }}/{{ config.theme.logo }}
{%- endif -%}
# Logo (icon)
- &logo_icon >-
{%- if not layout.logo -%}
{{ config.theme.icon.logo | x }}
{%- endif -%}
# Meta tags
tags:
# Open Graph
og:type: website
og:title: *page_title_with_site_name
og:description: *page_description
og:image: "{{ image.url }}"
og:image:type: "{{ image.type }}"
og:image:width: "{{ image.width }}"
og:image:height: "{{ image.height }}"
og:url: "{{ page.canonical_url }}"
# Twitter
twitter:card: summary_large_image
twitter:title: *page_title_with_site_name
twitter:description: *page_description
twitter:image: "{{ image.url }}"
# -----------------------------------------------------------------------------
# Specification
# -----------------------------------------------------------------------------
# Card size and layers
size: { width: 1200, height: 630 }
layers:
# Background
- background:
image: *background_image
color: *background_color
# Logo
- size: { width: 170, height: 192 }
offset: { x: 966, y: 64 }
background:
image: *logo
icon:
value: *logo_icon
color: *color
# Site name
- size: { width: 832, height: 42 }
offset: { x: 64, y: 64 }
typography:
content: *site_name
align: start center
color: *color
font:
family: *font_family
variant: *font_variant
style: Bold
# Motto
- size: { width: 832, height: 150 }
offset: { x: 64, y: 106 }
typography:
content: "{{ config.motto }}"
align: start center
color: *color
line:
amount: 2.5
height: 1.25
font:
family: *font_family
variant: *font_variant
style: Bold
# Page title
- size: { width: 1072, height: 256 }
offset: { x: 64, y: 256 }
typography:
content: *page_title
align: start center
color: *color
line:
amount: 3
height: 1.25
font:
family: *font_family
variant: *font_variant
style: Bold
# Page description
- size: { width: 832, height: 64 }
offset: { x: 64, y: 512 }
typography:
content: *page_description_social_card
align: start center
color: *color
line:
amount: 2
height: 1.5
font:
family: *font_family
variant: *font_variant
style: Regular
+2
View File
@@ -1,6 +1,8 @@
---
hide:
- navigation
title: Licensing and Support
description: Free Tier with 100 CPU core limit. Very affordable Tiers for larger enterprises.
---
Both [Tart Virtualization](https://github.com/cirruslabs/tart) and [Orchard Orchestration](https://github.com/cirruslabs/orchard)
+2
View File
@@ -1,6 +1,8 @@
---
hide:
- navigation
title: Quick Start
description: Install Tart and run your first virtual machine on Apple Silicon in minutes.
---
Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura) or later (will download a 25 GB image):
+5 -3
View File
@@ -2,12 +2,11 @@ repo_url: https://github.com/cirruslabs/tart/
site_url: https://tart.run/
edit_uri: blob/main/docs/
site_name: Tart
site_name: Tart Virtualization
site_author: Cirrus Labs
copyright: © Cirrus Labs 2017-present
site_description: >
Tart is a virtualization toolset to build, run and manage macOS and Linux virtual machines (VMs) on Apple Silicon.
Built by CI engineers for your automation needs.
remote_branch: main
@@ -47,7 +46,10 @@ plugins:
match_path: blog/posts/.*
date_from_meta:
as_creation: date
- social
- social:
cards_layout_dir: docs/layouts
cards_layout: custom
debug: true
- search
- minify
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
docker run --pull=always --rm -it -p 8000:8000 -v ${PWD}:/docs ghcr.io/cirruslabs/mkdocs-material-insiders:latest build