Compare commits

...
22 Commits
Author SHA1 Message Date
Nikolay Edigaryev 70040b633c Introduce AuthenticationKeeper actor to serialize authn modification (#647) 2023-11-06 14:58:23 -05:00
Nikolay Edigaryev f4bc02d175 DiskV2.push(): map disk into memory to avoid large allocations (#645) 2023-11-03 17:13:10 +04:00
Fedor Korotkov 6c24aa639a [blog] New dashboard with insights into performance of Cirrus Runners (#644) 2023-11-03 12:17:40 +04:00
Nikolay Edigaryev d8b69de52d Fetcher.fetchViaFile(): use an mmap(2)-ed file, similarly to DiskV1 (#641)
* Fetcher.fetchViaFile(): use an mmap(2)-ed file, similarly to DiskV1

* No need to convert Data to Data
2023-11-01 15:00:48 +04:00
Nikolay Edigaryev c4c2bfeded tart-dev.entitlements: add "com.apple.security.get-task-allow" (#642) 2023-11-01 14:35:55 +04:00
Fedor Korotkov b95585b56b Don't forget to finalize output stream (#640)
There is a suspicion that this might leak memory
2023-11-01 12:19:11 +04:00
Fedor Korotkov 8d5574ed3f Removed usage of deprecated APIs (#628)
See https://developer.apple.com/documentation/virtualization/vzmacauxiliarystorage/3816043-init
2023-10-11 17:05:49 -04:00
Fedor Korotkov 457c2bc7db Adjusted live installation counter (#627) 2023-10-11 20:55:51 +00:00
Fedor Korotkov 4bf9bdd531 Document Tart on AWS (#625)
Fixes #581
2023-10-07 12:06:06 +04:00
Fedor Korotkov 8e9d61d5f5 Validate that a suspendable VM doesn't have shared directories (#623) 2023-10-04 20:21:45 +04:00
Fedor KorotkovandNikolay Edigaryev 71d03226fe Support mounting remote archives (#620)
* Support mounting remote archives

Allow to pass an HTTPS link instead of a local path to `tart run --dir` argument. HTTPS link should point to a gzipped Tar archive aka `*.tar.gz` file.

In this situation Tart will download an archive by the link if necessary, will cache it and will unarchive it into a temporary folder inside `$TART_HOME` to be mounted to the VM.

This use case is useful for mounting something external that updates faster than the VM itself. For example, GitHub Actions Runner installation.

* Don't use async/await APIs to prevent from deadlocks because of the MainActor thing

* Prefer cached data

* Moved comment

* Fix URLCache caching files in memory instead of on-disk (#622)

* Fix URLCache caching files in memory instead of on-disk

* Fix disk capacity typo

* Moved log

* Moved fetching logic to `DirectoryShare#createConfiguration` method

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2023-10-03 23:01:29 +04:00
Nikolay Edigaryev 36dab9878d tart: bump max password characters from 256 to 1024 (#618) 2023-10-02 13:58:55 +00:00
Nikolay Edigaryev f634002813 tart run: disable console device when --suspendable is requested (#615) 2023-09-30 21:13:44 +04:00
Fedor Korotkov 8e79669afb Configure Markdown Linter (#614) 2023-09-29 03:35:17 -04:00
Fedor KorotkovandNikolay Edigaryev 2da8bc0fb5 Document XL Cirrus Runners (#613)
* Document XL Cirrus Runners

Also tried to put everything about Cirrus Runners in one place rather than having the information spreaded between https://tart.run and https://github.com/apps/cirrus-runners.

Plus updated docs to use Sonoma.

* Apply suggestions from code review

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2023-09-28 15:53:13 -04:00
Nikolay Edigaryev 2d984ba194 .cirrus.yml: add an execution_lock: for integration tests (#610)
* .cirrus.yml: add an execution_lock: for integration tests

* Use Persistent Worker's resources instead of grabbing an execution lock
2023-09-22 22:52:52 +04:00
Fedor Korotkov 50ce44c3eb Support block devices on Sonoma (#611)
* Support block devices on Sonoma

* Updated docs

* Removed unused error
2023-09-22 22:30:18 +04:00
Fedor Korotkov c9e49ceb39 Missing user
So it's an even number of them
2023-09-22 12:10:13 -04:00
Fedor Korotkov 6df50e55d8 --suspendable devices fallback on Ventura host (#605)
Fixes #604
2023-09-22 07:38:13 -04:00
Nikolay Edigaryev 1fd710d00d web: fix GitLab Runner integration link (#608)
Resolves https://github.com/cirruslabs/tart/issues/607.
2023-09-22 07:38:02 -04:00
Rui Marinho 4f6c7e79e1 Add Uphold as a Tart user (#606) 2023-09-22 07:37:02 -04:00
fedor 1afb43e85b Updated announcement link 2023-09-20 10:58:17 -04:00
30 changed files with 524 additions and 156 deletions
+15 -11
View File
@@ -4,12 +4,13 @@ env:
XCODE_TAG: 15
task:
name: Test on Ventura
name: Test on Sonoma
alias: test
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
persistent_worker:
labels:
name: dev-mini
resources:
tart-vms: 1
test_script:
- swift test
integration_test_script:
@@ -28,12 +29,19 @@ task:
path: "integration-tests/pytest-junit.xml"
format: junit
task:
name: Markdown Lint
only_if: $CIRRUS_BRANCH != 'gh-pages' && changesInclude('**.md')
container:
image: node:latest
install_script: npm install -g markdownlint-cli
lint_script: markdownlint --config=docs/.markdownlint.yml docs/
task:
name: Lint
alias: lint
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-xcode:$XCODE_TAG
image: ghcr.io/cirruslabs/macos-sonoma-xcode:$XCODE_TAG
lint_script:
- swift package plugin --allow-writing-to-package-directory swiftformat --cache ignore --lint --report swiftformat.json .
always:
@@ -45,9 +53,8 @@ task:
only_if: $CIRRUS_TAG == ''
name: Build
alias: build
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-xcode:$XCODE_TAG
image: ghcr.io/cirruslabs/macos-sonoma-xcode:$XCODE_TAG
build_script: swift build --product tart
sign_script: codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
binary_artifacts:
@@ -59,9 +66,8 @@ task:
depends_on:
- lint
- build
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-xcode:$XCODE_TAG
image: ghcr.io/cirruslabs/macos-sonoma-xcode:$XCODE_TAG
env:
MACOS_CERTIFICATE: ENCRYPTED[552b9d275d1c2bdbc1bff778b104a8f9a53cbd0d59344d4b7f6d0ca3c811a5cefb97bef9ba0ef31c219cb07bdacdd2c2]
AC_PASSWORD: ENCRYPTED[4a761023e7e06fe2eb350c8b6e8e7ca961af193cb9ba47605f25f1d353abc3142606f412e405be48fd897a78787ea8c2]
@@ -95,9 +101,8 @@ task:
- lint
- test
- build
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-xcode:$XCODE_TAG
image: ghcr.io/cirruslabs/macos-sonoma-xcode:$XCODE_TAG
env:
MACOS_CERTIFICATE: ENCRYPTED[552b9d275d1c2bdbc1bff778b104a8f9a53cbd0d59344d4b7f6d0ca3c811a5cefb97bef9ba0ef31c219cb07bdacdd2c2]
AC_PASSWORD: ENCRYPTED[4a761023e7e06fe2eb350c8b6e8e7ca961af193cb9ba47605f25f1d353abc3142606f412e405be48fd897a78787ea8c2]
@@ -142,7 +147,6 @@ task:
task:
name: Deploy Documentation
only_if: $CIRRUS_BRANCH == 'main'
use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true'
container:
image: ghcr.io/cirruslabs/mkdocs-material-insiders:latest
registry_config: ENCRYPTED[!cf1a0f25325aa75bad3ce6ebc890bc53eb0044c02efa70d8cefb83ba9766275a994b4831706c52630a0692b2fa9cfb9e!]
+5 -2
View File
@@ -52,6 +52,9 @@ Many more companies are using Tart in their internal setups. Here are a few of t
<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>
<a href="https://www.pitsdatarecovery.net/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/PITSGlobalDataRecoveryServices.png" height="65"/>
</a>
@@ -65,8 +68,8 @@ Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura)
```bash
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
tart run ventura-base
tart clone ghcr.io/cirruslabs/macos-sonoma-base:latest sonoma-base
tart run sonoma-base
```
Please check the [official documentation](https://tart.run) for more information and/or feel free to use [discussions](https://github.com/cirruslabs/tart/discussions)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+2
View File
@@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.virtualization</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
+146 -52
View File
@@ -1,5 +1,6 @@
import ArgumentParser
import Cocoa
import Darwin
import Dispatch
import SwiftUI
import Virtualization
@@ -51,10 +52,17 @@ 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\")
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\" --disk=\"/dev/disk0\")
""", discussion: """
Can be either a disk image file or a block device like a local SSD on AWS EC2 Mac instances.
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 'tart' binary must be executed as root which affects locating Tart VMs.
To workaround this issue pass TART_HOME explicitly:
sudo TART_HOME="$HOME/.tart" tart run sonoma --disk=/dev/disk0
""", valueName: "path[:ro]"))
var disk: [String] = []
@@ -118,6 +126,12 @@ struct Run: AsyncParsableCommand {
if try vmDir.state() == "suspended" {
suspendable = true
}
if suspendable {
if dir.count > 0 {
throw ValidationError("Suspending VMs with shared directories is not supported")
}
}
}
@MainActor
@@ -146,20 +160,6 @@ struct Run: AsyncParsableCommand {
let additionalDiskAttachments = try additionalDiskAttachments()
// 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.
for additionalDiskAttachment in additionalDiskAttachments {
// Read-only attachments do not seem to acquire the lock
if additionalDiskAttachment.isReadOnly {
continue
}
if try !FileLock(lockURL: additionalDiskAttachment.url).trylock() {
throw RuntimeError.DiskAlreadyInUse("disk \(additionalDiskAttachment.url.path) seems to be already in use, "
+ "unmount it first in Finder")
}
}
var serialPorts: [VZSerialPortConfiguration] = []
if serial {
let tty_fd = createPTY()
@@ -181,7 +181,7 @@ struct Run: AsyncParsableCommand {
vm = try VM(
vmDir: vmDir,
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
additionalDiskAttachments: additionalDiskAttachments,
additionalStorageDevices: additionalDiskAttachments,
directorySharingDevices: directoryShares() + rosettaDirectoryShare(),
serialPorts: serialPorts,
suspendable: suspendable
@@ -331,7 +331,7 @@ struct Run: AsyncParsableCommand {
return try Softnet(vmMACAddress: config.macAddress.string)
}
if netBridged.count > 0 {
if netBridged.count > 0 {
func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface {
let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in
interface.identifier == name || interface.localizedDisplayName == name
@@ -361,22 +361,43 @@ struct Run: AsyncParsableCommand {
}
}
func additionalDiskAttachments() throws -> [VZDiskImageStorageDeviceAttachment] {
var result: [VZDiskImageStorageDeviceAttachment] = []
func additionalDiskAttachments() throws -> [VZStorageDeviceConfiguration] {
var result: [VZStorageDeviceConfiguration] = []
let readOnlySuffix = ":ro"
let expandedDiskPaths = disk.map { NSString(string:$0).expandingTildeInPath }
for rawDisk in expandedDiskPaths {
if rawDisk.hasSuffix(readOnlySuffix) {
result.append(try VZDiskImageStorageDeviceAttachment(
url: URL(fileURLWithPath: String(rawDisk.prefix(rawDisk.count - readOnlySuffix.count))),
readOnly: true
))
let diskReadOnly = rawDisk.hasSuffix(readOnlySuffix)
let diskPath = diskReadOnly ? String(rawDisk.prefix(rawDisk.count - readOnlySuffix.count)) : rawDisk
let diskURL = URL(fileURLWithPath: diskPath)
// check if `diskPath` is a block device or a directory
if pathHasMode(diskPath, mode: S_IFBLK) || pathHasMode(diskPath, mode: S_IFDIR) {
print("Using block device\n")
guard #available(macOS 14, *) else {
throw UnsupportedOSError("attaching block devices", "are")
}
let fileHandle = FileHandle(forUpdatingAtPath: diskPath)
guard fileHandle != nil else {
if ProcessInfo.processInfo.userName != "root" {
throw RuntimeError.VMConfigurationError("need to run as root to work with block devices")
}
throw RuntimeError.VMConfigurationError("block device \(diskURL.url.path) seems to be already in use, unmount it first via 'diskutil unmount'")
}
let attachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: fileHandle!, readOnly: diskReadOnly, synchronizationMode: .full)
result.append(VZVirtioBlockDeviceConfiguration(attachment: attachment))
} else {
result.append(try VZDiskImageStorageDeviceAttachment(
url: URL(fileURLWithPath: rawDisk),
readOnly: false
))
// 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: diskURL).trylock() {
throw RuntimeError.DiskAlreadyInUse("disk \(diskURL.url.path) seems to be already in use, unmount it first in Finder")
}
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(
url: diskURL,
readOnly: diskReadOnly
)
result.append(VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment))
}
}
@@ -408,13 +429,13 @@ struct Run: AsyncParsableCommand {
let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag)
if allNamedShares {
var directories: [String : VZSharedDirectory] = Dictionary()
directoryShares.forEach { directories[$0.name!] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) }
try directoryShares.forEach { directories[$0.name!] = try $0.createConfiguration() }
sharingDevice.share = VZMultipleDirectoryShare(directories: directories)
} else if dir.count > 1 {
throw ValidationError("invalid --dir syntax: for multiple directory shares each one of them should be named")
} else if dir.count == 1 {
let directoryShare = directoryShares.first!
let singleDirectoryShare = VZSingleDirectoryShare(directory: VZSharedDirectory(url: directoryShare.path, readOnly: directoryShare.readOnly))
let singleDirectoryShare = VZSingleDirectoryShare(directory: try directoryShare.createConfiguration())
sharingDevice.share = singleDirectoryShare
}
@@ -589,36 +610,109 @@ struct DirectoryShare {
let readOnly: Bool
init(parseFrom: String) throws {
let splits = parseFrom.split(maxSplits: 2) { $0 == ":" }
let readOnlySuffix = ":ro"
readOnly = parseFrom.hasSuffix(readOnlySuffix)
let maybeNameAndURL = readOnly ? String(parseFrom.dropLast(readOnlySuffix.count)) : parseFrom
if splits.count == 3 {
if splits[2] == "ro" {
readOnly = true
} else {
throw ValidationError("invalid --dir syntax: optional read-only specifier can only be \"ro\"")
}
if maybeNameAndURL.starts(with: "https://") || maybeNameAndURL.starts(with: "http://") {
// just a URL
name = nil
path = URL(string: maybeNameAndURL)!
return
}
let splits = maybeNameAndURL.split(separator: ":", maxSplits: 1)
if splits.count == 2 {
name = String(splits[0])
path = String(splits[1]).toFilePathURL()
} else if splits.count == 2 {
if splits[1] == "ro" {
name = nil
path = String(splits[0]).toFilePathURL()
readOnly = true
} else {
name = String(splits[0])
path = String(splits[1]).toFilePathURL()
readOnly = false
}
path = String(splits[1]).toRemoteOrLocalURL()
} else {
name = nil
path = String(splits[0]).toFilePathURL()
readOnly = false
path = String(splits[0]).toRemoteOrLocalURL()
}
}
func createConfiguration() throws -> VZSharedDirectory {
if (path.isFileURL) {
return VZSharedDirectory(url: path, readOnly: readOnly)
}
let urlCache = URLCache(memoryCapacity: 0, diskCapacity: 1 * 1024 * 1024 * 1024)
let archiveRequest = URLRequest(url: path, cachePolicy: .returnCacheDataElseLoad)
var response: CachedURLResponse? = urlCache.cachedResponse(for: archiveRequest)
if (response == nil) {
print("Downloading \(path)...")
// download and unarchive remote directories if needed here
// use old school API to prevent deadlocks since we are running via MainActor
let downloadSemaphore = DispatchSemaphore(value: 0)
Task {
do {
let (archiveData, archiveResponse) = try await URLSession.shared.data(for: archiveRequest)
urlCache.storeCachedResponse(CachedURLResponse(response: archiveResponse, data: archiveData, storagePolicy: .allowed), for: archiveRequest)
print("Cached for future invocations!")
} catch {
print("Download failed: \(error)")
}
downloadSemaphore.signal()
}
downloadSemaphore.wait()
response = urlCache.cachedResponse(for: archiveRequest)
} else {
print("Using cached archive for \(path)...")
}
if (response == nil) {
throw ValidationError("Failed to fetch a remote archive!")
}
let temporaryLocation = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString + ".volume")
try FileManager.default.createDirectory(atPath: temporaryLocation.path, withIntermediateDirectories: true)
let lock = try FileLock(lockURL: temporaryLocation)
try lock.lock()
guard let executableURL = resolveBinaryPath("tar") else {
throw ValidationError("tar not found in PATH")
}
let process = Process.init()
process.executableURL = executableURL
process.currentDirectoryURL = temporaryLocation
process.arguments = ["-xz"]
let inPipe = Pipe()
process.standardInput = inPipe
process.launch()
inPipe.fileHandleForWriting.write(response!.data)
try inPipe.fileHandleForWriting.close()
process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw ValidationError("Unarchiving failed!")
}
print("Unarchived into a temporary directory!")
return VZSharedDirectory(url: temporaryLocation, readOnly: readOnly)
}
}
extension String {
func toFilePathURL() -> URL {
URL(fileURLWithPath: NSString(string: self).expandingTildeInPath)
func toRemoteOrLocalURL() -> URL {
if (starts(with: "https://") || starts(with: "https://")) {
URL(string: self)!
} else {
URL(fileURLWithPath: NSString(string: self).expandingTildeInPath)
}
}
}
func pathHasMode(_ path: String, mode: mode_t) -> Bool {
var st = stat()
let statRes = stat(path, &st)
guard statRes != -1 else {
return false
}
return (Int32(st.st_mode) & Int32(mode)) == Int32(mode)
}
@@ -13,7 +13,7 @@ class StdinCredentials {
return (user, password)
}
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 255, isSensitive: Bool) throws -> String {
private static func readStdinCredential(name: String, prompt: String, maxCharacters: Int = 1024, isSensitive: Bool) throws -> String {
var buf = [CChar](repeating: 0, count: maxCharacters + 1 /* sentinel */ + 1 /* NUL */)
guard let rawCredential = readpassphrase(prompt, &buf, buf.count, isSensitive ? RPP_ECHO_OFF : RPP_ECHO_ON) else {
throw StdinCredentialsError.CredentialRequired(which: name)
+3 -3
View File
@@ -35,12 +35,12 @@ class Fetcher {
//
// This keeps a working reference to that file, yet we don't
// have to deal with the cleanup any more.
let fh = try FileHandle(forReadingFrom: fileURL)
let mappedFile = try Data(contentsOf: fileURL, options: [.alwaysMapped])
try FileManager.default.removeItem(at: fileURL)
Task {
while let data = try fh.read(upToCount: 64 * 1024 * 1024) {
await dataCh.send(data)
for chunk in (0 ..< mappedFile.count).chunks(ofCount: 64 * 1024 * 1024) {
await dataCh.send(mappedFile.subdata(in: chunk))
}
dataCh.finish()
@@ -0,0 +1,25 @@
import Foundation
actor AuthenticationKeeper {
var authentication: Authentication? = nil
func set(_ authentication: Authentication) {
self.authentication = authentication
}
func header() -> (String, String)? {
if let authentication = authentication {
// Do not suggest any headers if the
// authentication token has expired
if !authentication.isValid() {
return nil
}
return authentication.header()
}
// Do not suggest any headers if the
// authentication token is not set
return nil
}
}
+15 -4
View File
@@ -9,12 +9,16 @@ class DiskV2: Disk {
var pushedLayers: [OCIManifestLayer] = []
// Open the disk file
let disk = try FileHandle(forReadingFrom: diskURL)
var mappedDisk = try Data(contentsOf: diskURL, options: [.alwaysMapped])
// Compress the disk file as multiple individually decompressible streams,
// each equal ``Self.layerLimitBytes`` bytes or slightly larger due to the
// internal compressor's buffer
while let (compressedData, uncompressedSize, uncompressedDigest) = try compressNextLayerOfLimitBytesOrMore(disk: disk) {
var offset: UInt64 = 0
while let (compressedData, uncompressedSize, uncompressedDigest) = try compressNextLayerOfLimitBytesOrMore(mappedDisk: mappedDisk, offset: offset) {
offset += uncompressedSize
let layerDigest = try await registry.pushBlob(fromData: compressedData, chunkSizeMb: chunkSizeMb)
pushedLayers.append(OCIManifestLayer(
@@ -106,6 +110,8 @@ class DiskV2: Disk {
progress.completedUnitCount += Int64(data.count)
}
try filter.finalize()
try disk.close()
}
@@ -114,7 +120,7 @@ class DiskV2: Disk {
}
}
private static func compressNextLayerOfLimitBytesOrMore(disk: FileHandle) throws -> (Data, UInt64, String)? {
private static func compressNextLayerOfLimitBytesOrMore(mappedDisk: Data, offset: UInt64) throws -> (Data, UInt64, String)? {
var compressedData = Data()
var bytesRead: UInt64 = 0
let digest = Digest()
@@ -126,10 +132,15 @@ class DiskV2: Disk {
return nil
}
guard let uncompressedChunk = try disk.read(upToCount: bufferSizeBytes) else {
let readFromByte = Int(offset + bytesRead)
let numBytesToRead = min(mappedDisk.count - readFromByte, bufferSizeBytes)
if numBytesToRead == 0 {
return nil
}
let uncompressedChunk = mappedDisk.subdata(in: readFromByte ..< (readFromByte + numBytesToRead))
bytesRead += UInt64(uncompressedChunk.count)
digest.update(uncompressedChunk)
+5 -12
View File
@@ -102,8 +102,7 @@ class Registry {
private let baseURL: URL
let namespace: String
let credentialsProviders: [CredentialsProvider]
var currentAuthToken: Authentication? = nil
let authenticationKeeper = AuthenticationKeeper()
var host: String? {
guard let host = baseURL.host else { return nil }
@@ -253,7 +252,7 @@ class Registry {
for try await part in channel {
try Task.checkCancellation()
try await handler(Data(part))
try await handler(part)
}
}
@@ -305,11 +304,6 @@ class Registry {
request.httpBody = body
}
// Invalidate token if it has expired
if currentAuthToken?.isValid() == false {
currentAuthToken = nil
}
var (channel, response) = try await authAwareRequest(request: request, viaFile: viaFile)
if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue {
@@ -331,7 +325,7 @@ class Registry {
if wwwAuthenticate.scheme.lowercased() == "basic" {
if let (user, password) = try lookupCredentials() {
currentAuthToken = BasicAuthentication(user: user, password: password)
await authenticationKeeper.set(BasicAuthentication(user: user, password: password))
}
return
@@ -378,7 +372,7 @@ class Registry {
+ "while retrieving an authentication token", details: data.asText())
}
currentAuthToken = try TokenResponse.parse(fromData: data)
await authenticationKeeper.set(try TokenResponse.parse(fromData: data))
}
private func lookupCredentials() throws -> (String, String)? {
@@ -399,8 +393,7 @@ class Registry {
private func authAwareRequest(request: URLRequest, viaFile: Bool = false) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var request = request
if let token = currentAuthToken {
let (name, value) = token.header()
if let (name, value) = await authenticationKeeper.header() {
request.addValue(value, forHTTPHeaderField: name)
}
+9 -3
View File
@@ -60,7 +60,7 @@ struct Darwin: PlatformSuspendable {
let result = VZMacPlatformConfiguration()
result.machineIdentifier = ecid
result.auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: nvramURL)
result.auxiliaryStorage = VZMacAuxiliaryStorage(url: nvramURL)
if !hardwareModel.isSupported {
// At the moment support of M1 chip is not yet dropped in any macOS version
@@ -111,7 +111,8 @@ struct Darwin: PlatformSuspendable {
if #available(macOS 14, *) {
return [VZMacKeyboardConfiguration()]
} else {
return []
// fallback to the regular configuration
return keyboards()
}
}
@@ -121,6 +122,11 @@ struct Darwin: PlatformSuspendable {
}
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
[VZMacTrackpadConfiguration()]
if #available(macOS 14, *) {
return [VZMacTrackpadConfiguration()]
} else {
// fallback to the regular configuration
return pointingDevices()
}
}
}
+17 -15
View File
@@ -42,7 +42,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
init(vmDir: VMDirectory,
network: Network = NetworkShared(),
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
additionalStorageDevices: [VZStorageDeviceConfiguration] = [],
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
serialPorts: [VZSerialPortConfiguration] = [],
suspendable: Bool = false
@@ -58,7 +58,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
self.network = network
configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
nvramURL: vmDir.nvramURL, vmConfig: config,
network: network, additionalDiskAttachments: additionalDiskAttachments,
network: network, additionalStorageDevices: additionalStorageDevices,
directorySharingDevices: directorySharingDevices,
serialPorts: serialPorts,
suspendable: suspendable
@@ -142,7 +142,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
ipswURL: URL,
diskSizeGB: UInt16,
network: Network = NetworkShared(),
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
additionalStorageDevices: [VZStorageDeviceConfiguration] = [],
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
serialPorts: [VZSerialPortConfiguration] = []
) async throws {
@@ -190,7 +190,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
self.network = network
configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
vmConfig: config, network: network,
additionalDiskAttachments: additionalDiskAttachments,
additionalStorageDevices: additionalStorageDevices,
directorySharingDevices: directorySharingDevices,
serialPorts: serialPorts
)
@@ -277,7 +277,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
nvramURL: URL,
vmConfig: VMConfig,
network: Network = NetworkShared(),
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
additionalStorageDevices: [VZStorageDeviceConfiguration],
directorySharingDevices: [VZDirectorySharingDeviceConfiguration],
serialPorts: [VZSerialPortConfiguration],
suspendable: Bool = false
@@ -326,11 +326,11 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
// Storage
var attachments = [try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)]
attachments.append(contentsOf: additionalDiskAttachments)
configuration.storageDevices = attachments.map {
VZVirtioBlockDeviceConfiguration(attachment: $0)
}
var devices: [VZStorageDeviceConfiguration] = [
VZVirtioBlockDeviceConfiguration(attachment: try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false))
]
devices.append(contentsOf: additionalStorageDevices)
configuration.storageDevices = devices
// Entropy
if !suspendable {
@@ -347,13 +347,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
//
// A dummy console device useful for implementing
// host feature checks in the guest agent software.
let consolePort = VZVirtioConsolePortConfiguration()
consolePort.name = "tart-version-\(CI.version)"
if !suspendable {
let consolePort = VZVirtioConsolePortConfiguration()
consolePort.name = "tart-version-\(CI.version)"
let consoleDevice = VZVirtioConsoleDeviceConfiguration()
consoleDevice.ports[0] = consolePort
let consoleDevice = VZVirtioConsoleDeviceConfiguration()
consoleDevice.ports[0] = consolePort
configuration.consoleDevices.append(consoleDevice)
configuration.consoleDevices.append(consoleDevice)
}
try configuration.validate()
+12
View File
@@ -0,0 +1,12 @@
"default": true
"MD002": false # First heading should be a top level heading
"MD007": # Unordered list indentation
indent: 4
"MD009": false # Trailing spaces
"MD013": false # Line length
"MD025": false # Multiple top level headings in the same document
"MD026": false # Trailing punctuation in heading
"MD033": false # Inline HTML
"MD041": false # First line in file should be a top level heading
"MD045": false # OK not to have a description for an image
"MD046": false # Code block style [Expected: fenced; Actual: indented]
Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

@@ -56,7 +56,7 @@ On bootstrap, each Orchard worker establishes a `Watch()` RPC stream and waits f
Once `PortForward` instruction is received, the worker connects to the specified VM and port locally and opens a new `PortForward()` RPC stream with the controller, carrying the unique `session` identifier in the gRPC metadata to help distinguish several port forwarding requests.
We’re using a pretty ingenious Golang package that turns any gRPC stream into a `net.Conn`: https://github.com/mitchellh/go-grpc-net-conn. This allows us to abstract from the gRPC details and simply proxy two `net.Conns`, thus providing the port forwarding functionality.
We’re using a pretty ingenious [Golang package that turns any gRPC stream into a `net.Conn`](https://github.com/mitchellh/go-grpc-net-conn). This allows us to abstract from the gRPC details and simply proxy two `net.Conns`, thus providing the port forwarding functionality.
We’ve also initially considered using [Yamux](https://github.com/hashicorp/yamux) to only keep a single connection with each worker, however, that involves the burden of dealing with flow control and potential implementation bugs associated with it, so we’ve decided to simply open an additional connection for each port forwarding session and let the OS deal with it.
@@ -74,25 +74,25 @@ Secondly, we’ve exposed three commands in the Orchard CLI that all use this en
Opens a TCP port locally and forwards everything sent to it to the specified VM (and vice versa).
For example, `orchard port-forward vm ventura-builder 2222:22` will forward traffic from the local TCP port `2222` to the `ventura-builder` VM’s TCP port `22`.
For example, `orchard port-forward vm sonoma-builder 2222:22` will forward traffic from the local TCP port `2222` to the `ventura-builder` VM’s TCP port `22`.
### `orchard ssh`
Connects to the specified VM on the default SSH port `22`, optionally only launching a command (if specified), similarly to what the official OpenSSH client does.
For example, `orchard ssh vm ventura-builder` will open an interactive session with the `ventura-builder` VM.
For example, `orchard ssh vm sonoma-builder` will open an interactive session with the `ventura-builder` VM.
You can also send local scripts for execution by utilizing redirection:
```shell
orchard ssh vm ventura-builder 'sh -s' < script.sh
orchard ssh vm sonoma-builder 'sh -s' < script.sh
```
### `orchard vnc`
Establishes a port forwarding to the specified VM’s default VNC port `5900` and opens the default macOS Screen Sharing app.
For example, `orchard vnc vm ventura-builder` will establish a port-forwarding to the `ventura-builder` VM's port `5900` under the hood and launch macOS Screen Sharing app.
For example, `orchard vnc vm sonoma-builder` will establish a port-forwarding to the `ventura-builder` VM's port `5900` under the hood and launch macOS Screen Sharing app.
Note that the SSH and VNC commands expect the VM resource to specify credentials in it’s definition (can be done via `orchard create vm`), and will otherwise fall back to the credentials specified by `--username` and `--password`, or if none specified — to de-facto standard of `admin:admin` credentials.
+6 -4
View File
@@ -29,6 +29,8 @@ dozens of companies that we know of are using Tart in their daily workflows. If
- ![](https://github.com/cirruslabs/tart/raw/main/Resources/Users/Suran.png){ height="65" }
- ![](https://github.com/cirruslabs/tart/raw/main/Resources/Users/Symflower.png){ height="65" }
- ![](https://github.com/cirruslabs/tart/raw/main/Resources/Users/Transloadit.png){ height="65" }
- ![](https://github.com/cirruslabs/tart/raw/main/Resources/Users/PITSGlobalDataRecoveryServices.png){ height="65" }
- ![](https://github.com/cirruslabs/tart/raw/main/Resources/Users/Uphold.png){ height="65" }
</div>
@@ -41,10 +43,10 @@ allocate time to continue improving Tart which brings us to the section below.
In the last 7 months we've had 12 feature releases that brought a lot of features requested by the community. Here are just
a few of them to highlight:
* [Custom GitLab Runner Executor](/integrations/gitlab-runner.md).
* [Cluster Management via Orchard](2023-04-25-orchard-ga.md).
* Numerous compatibility improvements for all kinds of OCI-registries.
* Sonoma Support (see details [below](#macos-sonoma-updates)).
-[Custom GitLab Runner Executor](/integrations/gitlab-runner/).
-[Cluster Management via Orchard](2023-04-25-orchard-ga.md).
-Numerous compatibility improvements for all kinds of OCI-registries.
-Sonoma Support (see details [below](#macos-sonoma-updates)).
But one of the most requested features/complaints was around pulling huge Tart images from remote OCI-compatible registries.
With an ideal network conditions `tart pull` worked pretty good but in case of any network issues it was required to
+71
View File
@@ -0,0 +1,71 @@
---
draft: false
date: 2023-10-06
search:
exclude: true
authors:
- fkorotkov
categories:
- announcement
---
# Tart is now available on AWS Marketplace
Announcing [official AMIs for EC2 Mac Instances](https://aws.amazon.com/marketplace/pp/prodview-qczco34wlkdws)
with preconfigured Tart installation that is optimized to work within AWS infrastructure.
EC2 Mac Instances is a gem of engineering powered by AWS Nitro devices. Just imagine there is a physical Mac Mini with
a plugged in Nitro device that can push the physical power button!
![EC2 M2 Pro](/blog/images/ec2-mac2-m2pro.png)
This clever synergy between Apple Hardware and Nitro System allows seamless integration with VPC networking and booting macOS from an EBS volume.
In this blog post we’ll see how a virtualization solution like Tart can compliment and elevate experience with EC2 Mac Instances.
<!-- more -->
Let’s start from the basics, what EC2 Mac Instances allow to do compared to physical Mac Minis seating in offices of
many companies around the world?
First and foremost, EC2 Mac Instances sit inside AWS data centers and can leverage all the goodies of VPC networking
within your company's existing infrastructure. No need to connect your Macs in the office through a VPN and deal
with networking and security.
Additionally, EC2 Mac Instances are booting from EBS volumes which means it is possible to always have reproducible instances
and apply all the best practices of Infrastructure-as-Code. Managing a fleet of physical Macs is a pain and it's very hard
to make them configured in a reproducible and stable way. With booting from identical EBS volumes your team is always sure
about the identical initial state of the fleet.
## Compromises of EC2 Mac Instances
The flexibility of EBS volumes for macOS comes with some compromises that virtualization solutions like Tart can help with.
The initial boot from an EBS volume takes some time and not instant. macOS itself is pretty heavy and a Nitro device needs
to download tens of gigabytes that macOS requires in order to boot. This means that **resetting a EC2 Mac Instance to a clean state
is not instant and usually takes a couple of minutes** when you can’t utilize the precious resources for your workloads.
It is much easier to tailor such EBS volumes with tools like Packer but there is still a **friction to test newly created EBS volumes**
since one needs to start and run a EC2 Mac Instance and it’s not possible to test things locally. Similarly it is even harder
to test beta versions of macOS that require manual interaction with a running instance.
## Solution
Tart can help with all the compromises! Tart virtual machines (VMs) have nearly native performance thanks to utilizing
native `Virtualization.Framework` that was developed along the first Apple Silicon chip. **Tart VMs can be copied/disposed
instantly and booting a fresh Tart VM takes only several seconds**. It is also possible to run two different Tart VMs in parallel
that can have completely different versions of macOS and packages. For example, it is possible to have the latest stable macOS
with the release version of Xcode along with the next version of macOS with the latest beta of Xcode.
Creation of Tart VMs can be automated with [a Packer plugin](https://github.com/cirruslabs/packer-plugin-tart) the same way as
creation of EC2 AMIs with one caveat that **Tart Packer Plugin works locally so you can test the same virtual machine locally
as you would run it in the cloud**.
Lightweight nature of Tart VMs with a focus on an easy-to-integrate Tart CLI compliments any macOS automation and helps to reduce
the feedback cycle and improves reproducibility of macOS environments even further.
## Conclusion
We are excited to bring [official AMIs that include Tart installation optimized to work within AWS](https://aws.amazon.com/marketplace/pp/prodview-qczco34wlkdws).
In the coming weeks when macOS Sonoma will become available on AWS we’ll release another update specifically targeting EC2 Mac Instances.
This update will simplify access to local SSDs of Mac Instances that are slightly faster than EBS volumes. Stay tuned and don’t hesitate
to ask any [questions](https://tart.run/licensing/).
@@ -0,0 +1,59 @@
---
draft: false
date: 2023-11-03
search:
exclude: true
authors:
- fkorotkov
categories:
- announcement
---
# New dashboard with insights into performance of Cirrus Runners
This month we are celebrating one year since launching Cirrus Runners — managed Apple Silicon infrastructure for your
GitHub Actions. During the last 12 months we ran millions of workflows for our customers and now ready to share some insights
into price performance of them for our customers.
One of the key difference with Cirrus Runners is how they are getting billed for. Customers purchase Cirrus Runners via monthly subscription
that costs $150 per each Cirrus Runner. Each runner can be used 24 hours a day 7 days a week to run GitHub Actions workflows
for an organization. If there are more outstanding jobs than available runners then they are queued and executed as soon as
there is a free runner. This is different from how GitHub-managed GitHub Actions are billed for — you pay for each minute of execution time.
The benefit of a fixed price is that you can run as many jobs as you want without worrying about the cost. The downside is that
you need to make sure that you are using your runners efficiently. This is where the new dashboard comes in handy.
<!-- more -->
But first, **let's see theoretically the lowest price per minute** of a Cirrus Runners. If you run 24 hours a day 7 days a week
then you will get 43,200 minutes of execution time per month. This means that the price per minute is $0.0035 if your runners
utilization is 100%. But even if your engineering teams is located in a single time zone and works 8 hours a day 5 days a week
then you will get 9,600 minutes of execution time per month which comes down to $0.015 per-minute. This is still more than 10 times cheaper
than recently announced Apple Silicon GitHub-manged runners that cost $0.16 per minute.
Now lets take a look at the new Cirrus Runners dashboard of a real customers that run their workflows on Cirrus Runners
and **practically pushing the price performance pretty close to the theoretical minimum**.
![Cirrus Runners Dashboard](/blog/images/runners-price-performance-2.png)
As you can see above Cirrus Runners Dashboard focuses on 4 core metrics:
1. **Minutes Used** — overall amount of minutes that Cirrus Runners were executing jobs.
2. **Workflow Runs** — absolute number of workflow runs that were executed on Cirrus Runners.
3. **Queue Size** — number of jobs that were queued and waiting for a free Cirrus Runner.
4. **Queue Time** — average time that jobs were waiting in the queue.
In this particular example price performance of Cirrus Runners is $0.006 per minute which is 2 times more than the theoretical minimum
and **26 times better than GitHub-managed Apple Silicon runners**. But this is a extreme example, looking at queue time and queue size
we can see that the downside of such great price performance is that jobs are waiting in the queue on average around 5 minutes.
Here is another example of Cirrus Runners Dashboard for a different customer that has a slightly higher price performance of $0.017 per minute
but at the same time doesn't experience queue time at all. **Note that $0.017 is still 10 times cheaper than GitHub-managed Apple Silicon runners**.
![Cirrus Runners Dashboard](/blog/images/runners-price-performance-3.png)
## Conclusion
Having a fixed price for Cirrus Runners is a great way to save money on your CI/CD infrastructure and just in general have predictable budged.
But it requires keeping the balance between price per minute and queue time. Cirrus Runners Dashboard helps you to keep an eye on this balance
and make sure that you are getting the most out of your Cirrus Runners.
+2 -2
View File
@@ -13,7 +13,7 @@ task:
name: hello
macos_instance:
# can be a remote or a local virtual machine
image: ghcr.io/cirruslabs/macos-ventura-base:latest
image: ghcr.io/cirruslabs/macos-sonoma-base:latest
hello_script:
- echo "Hello from within a Tart VM!"
- echo "Here is my CPU info:"
@@ -45,7 +45,7 @@ exposes it via [`artifacts` instruction](https://cirrus-ci.org/guide/writing-tas
task:
name: Build
macos_instance:
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
build_script: swift build --product tart
binary_artifacts:
path: .build/debug/tart
+100 -18
View File
@@ -1,33 +1,115 @@
# GitHub Actions
# Cirrus Runners for GitHub Actions
Tart already powers several CI services mentioned above including our own [Cirrus CI](https://cirrus-ci.org/guide/macOS/) which offers unlimited concurrency with per-second billing.
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
*Cirrus Runners* is the fastest and most cost-efficient way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
Your actions will be executed in clean macOS virtual machines with 4 Apple M2 cores.
## Testimonials from customers
Sebastian Jachec, Mobile Engineer at [Daybridge](https://www.daybridge.com/).
> It’s been plain-sailing with the Cirrus Runners — they’ve been great! They’re consistently 60+% faster on workflows that we previously used Github Actions’ macOS runners for.
Max Lapides, Senior Mobile Engineer at [Tonal](https://www.tonal.com/).
Max Lapides, Senior Mobile Engineer at [Tonal](https://www.tonal.com/):
> Previously, we were using the GitHub‑hosted macOS runners and our iOS build took ~30 minutes. Now with Cirrus Runners, the iOS build only takes ~12 minutes. That’s a huge boost to our productivity, and for only $150/month per runner it is much less expensive too.
John A., Software Engineer at [GitKraken](https://www.gitkraken.com/):
> GitHub Actions MacOS-x86 runners have become increasingly unreliable, so we're moving our Mac builds over to arm64 because Cirrus Labs' M1 runners are not only ~3 times faster, they've also been far more stable.
Sebastian Jachec, Mobile Engineer at [Daybridge](https://www.daybridge.com/):
> It’s been plain-sailing with the Cirrus Runners — they’ve been great! They’re consistently 60+% faster on workflows that we previously used Github Actions’ macOS runners for.
## Pricing
Each Cirrus Runner costs $150 a month and there is no limit on the amount of minutes for your actions.
We recommend to purchase several Cirrus Runners depending on your team size, so you can run actions in
parallel. Note that you can change your subscription at any time via [this page](https://billing.stripe.com/p/login/3cs7vNbzo92p7fy3cc)
or by emailing [support@cirruslabs.org](mailto:support@cirruslabs.org).
### Priority Support
Subscriptions of 20 or more Cirrus Runners include access to [Priority Support](../licensing.md#priority-support).
Please contact [sales@cirruslabs.org](mailto:sales@cirruslabs.org) in order to get all the details.
### CPU and Memory resources of Cirrus Runners
By default, a single Cirrus Runner is allocated with 4 M2 cores and 12 GB of unified memory which is enough for most of the workloads.
For workloads that require more resources it is possible to use XL Cirrus Runners which have twice the resources: a full M2 chip with 8 cores
and 24 GB of unified memory. Note that a single XL Cirrus Runner also uses twice the concurrency.
In order to use an XL Cirrus Runner for a job please append `-xl` suffix to your `runs-on` property. More on that down below.
## Installation
Once you configure [Cirrus Runners App](https://github.com/apps/cirrus-runners) for your organization, you'll be redirected
to a checkout page powered by Stripe. During the checkout process you'll be able to configure a subscription for
a desired amount of parallel Cirrus Runners and try it for free for 10 days.
Once configured, please follow instruction below. If you have any questions please contact [support@cirruslabs.org](mailto:support@cirruslabs.org).
Subscriptions with more than 10 runners also include Priority Support
## Configuring Cirrus Runners
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
After successful installation and subscription configuration, use any of [Ventura images managed by us](https://github.com/cirruslabs/macos-image-templates) in `runs-on`:
In order for Cirrus Runners to be used by your GitHub Actions workflow jobs, specify a desired image in the `runs-on` property.
```yaml
name: Test Suite
jobs:
test:
runs-on: ghcr.io/cirruslabs/macos-ventura-xcode:latest
```
=== "Default Cirrus Runner"
```yaml
name: Tests
jobs:
test:
runs-on: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
```
=== "XL Cirrus Runner"
```yaml
name: Integration Tests
jobs:
test:
runs-on: ghcr.io/cirruslabs/macos-sonoma-xcode:latest-xl
```
List of all available images can be found in [this repository](https://github.com/cirruslabs/macos-image-templates).
Note that Tart VM images don't have the same set of pre-installed packages as the official Intel GitHub runners.
If something is missing please [create an issue within this repository](https://github.com/cirruslabs/macos-image-templates/issues/new).
When workflows are executing you'll see Cirrus on-demand runners on your organization's settings page at `https://github.com/organizations/<ORGANIZATION>/settings/actions/runners`.
Note that Cirrus Runners will get added to the default runner group. By default, only private repositories can access runners in a default runner group, but you can override this in your organization's settings.
Note that Cirrus Runners will get added to the default runner group.
!!! tip "Using Cirrus Runners with public repositories"
By default, only private repositories can access runners in a default runner group, but you can override this in your organization's settings:
```https://github.com/organizations/<YOUR ORGANIZATION NAME>/settings/actions/runner-groups/1```
![](/assets/images/TartGHARunners.png)
### Dashboard
You can also see the status of your runners on the [Cirrus Runners Dashboard](https://cirrus-runners.app/). This dashboard
also provides insights into price performance of your Cirrus Runners. Please check out [this blog post](/blog/2023/11/03/new-dashboard-with-insights-into-performance-of-cirrus-runners/)
to learn more about what this dashboard can do for you.
![](/assets/images/RunnersDashboard.png)
## Data handling flow
By design Cirrus Runners service never sees any of your secrets or source code and acts as compute platform with the lastest
Apple Silicon hardware that can quickly allocate CPU/Memory resources for your jobs.
Here is a high-level overview of how Cirrus Runners service manages runners for your organization:
- Cirrus Runner GitHub App is subscribed to [`workflow_job`](https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_job).
- Upon receiving a new event targeting Cirrus Runners via `runs-on` property the following steps take place:
- Non-personal information about your job is saved to perform health checking of Cirrus Runners execution.
- Cirrus Runners GitHub App has only one permission that allows generating temporary registration tokens for
self-hosted GitHub Actions Runners. Note that Cirrus Runners GitHub App itself doesn't have access to contents of
repositories in your organization.
- Cirrus Runners Service creates a new single use Tart VM, generates a temporary registration tokens for self-hosted runners
and passes it without storing inside the VM for the GitHub Actions Runner service to [start a ephemeral runner](https://github.blog/changelog/2021-09-20-github-actions-ephemeral-self-hosted-runners-new-webhooks-for-auto-scaling/).
- Cirrus Runners service continuously monitors health of the Tart VM executing your job to make sure it runs to completion.
- After the job finishes the ephemeral Tart VM is getting destroyed with all the information of the job run.
If you have any questions or concerns please feel free to reach out to [support@cirruslabs.org](mailto:support@cirruslabs.org).
+1 -1
View File
@@ -37,7 +37,7 @@ Now you can use Tart Images in your `.gitlab-ci.yml`:
```yaml
# You can use any remote Tart Image.
# Tart Executor will pull it from the registry and use it for creating ephemeral VMs.
image: ghcr.io/cirruslabs/macos-ventura-base:latest
image: ghcr.io/cirruslabs/macos-sonoma-base:latest
test:
tags:
+6 -4
View File
@@ -11,8 +11,8 @@ Tart can create VMs from `*.ipsw` files. You can download a specific `*.ipsw` fi
use `latest` instead of a path to `*.ipsw` to download the latest available version:
```bash
tart create --from-ipsw=latest ventura-vanilla
tart run ventura-vanilla
tart create --from-ipsw=latest sonoma-vanilla
tart run sonoma-vanilla
```
After the initial booting of the VM you'll need to manually go through the macOS installation process. As a convention we recommend creating an `admin` user with an `admin` password. After the regular installation please do some additional modifications in the VM:
@@ -67,8 +67,8 @@ packer {
}
source "tart-cli" "tart" {
vm_base_name = "ghcr.io/cirruslabs/macos-ventura-base:latest"
vm_name = "my-custom-ventura"
vm_base_name = "ghcr.io/cirruslabs/macos-sonoma-base:latest"
vm_name = "my-custom-sonoma"
cpu_count = 4
memory_gb = 8
disk_size_gb = 70
@@ -92,7 +92,9 @@ Here is a [repository with Packer templates](https://github.com/cirruslabs/macos
## Working with a Remote OCI Container Registry
<!-- markdownlint-disable MD034 -->
For example, let's say you want to push/pull images to a registry hosted at https://acme.io/.
<!-- markdownlint-enable MD034 -->
### Registry Authorization
+5 -1
View File
@@ -12,7 +12,6 @@ will be required to obtain a paid license.
The virtual CPU cores of Tart VMs are not tied to specific physical cores of the host CPU. Instead, for optimal performance
Tart VMs will automatically try to balance compute between all available cores of the host CPU. As a result,
all performance and energy-efficient cores of the host CPU are always counted towards the license usage.
# License Tiers
@@ -57,6 +56,11 @@ If your organization is interested in purchasing one of the license tiers, pleas
You can see a template of a license subscription agreement [here](assets/TartLicenseSubscription.pdf).
!!! info "Running on AWS?"
There are [official AMIs for EC2 Mac Instances](https://aws.amazon.com/marketplace/pp/prodview-qczco34wlkdws)
with preconfigured Tart installation that is optimized to work within AWS infrastructure.
# General Support
The best way to ask general questions about particular use cases is to email our support team at [support@cirruslabs.org](mailto:support@cirruslabs.org).
+7 -8
View File
@@ -7,8 +7,8 @@ Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura)
```bash
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
tart run ventura-base
tart clone ghcr.io/cirruslabs/macos-sonoma-base:latest sonoma-base
tart run sonoma-base
```
??? info "Manual installation from a release archive"
@@ -17,8 +17,8 @@ tart run ventura-base
```bash
curl -LO https://github.com/cirruslabs/tart/releases/latest/download/tart.tar.gz
tar -xzvf tart.tar.gz
./tart.app/Contents/MacOS/tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
./tart.app/Contents/MacOS/tart run ventura-base
./tart.app/Contents/MacOS/tart clone ghcr.io/cirruslabs/macos-sonoma-base:latest sonoma-base
./tart.app/Contents/MacOS/tart run sonoma-base
```
Please note that `./tart.app/Contents/MacOS/tart` binary is required to be used in order to trick macOS
@@ -33,7 +33,7 @@ tart run ventura-base
If the guest VM is running and configured to accept incoming SSH connections you can conveniently connect to it like so:
```bash
ssh admin@$(tart ip ventura-base)
ssh admin@$(tart ip sonoma-base)
```
!!! tip "Running scripts inside Tart virtual machines"
@@ -42,8 +42,8 @@ ssh admin@$(tart ip ventura-base)
```bash
brew install sshpass
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip ventura-base) "uname -a"
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip ventura-base) < script.sh
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip sonoma-base) "uname -a"
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip sonoma-base) < script.sh
```
## Mounting directories
@@ -100,4 +100,3 @@ mount -t virtiofs com.apple.virtio-fs.automount /mnt/shared
```
The directory we've mounted above will be accessible from the `/mnt/shared/project` path inside a guest VM.
+5 -8
View File
@@ -1,11 +1,5 @@
{% extends "base.html" %}
{% block announce %}
<a href="/blog/2023/04/25/announcing-orchard-orchestration-for-managing-macos-virtual-machines-at-scale/">
🚀🚀🚀&nbsp&nbspAnnouncing <strong>Orchard</strong> orchestration for managing macOS virtual machines at scale&nbsp;&nbsp;🚀🚀🚀
</a>
{% endblock %}
<!-- Render landing page under tabs -->
{% block tabs %} {{ super() }}
@@ -242,12 +236,15 @@
}
let counterElement = document.getElementById('installation-counter');
if (counterElement) {
counterElement.textContent = Math.round(allDownloads / 1000) + ",000"
// Live installation count is available starting version 1.0.0
// Prior Tart was installed a little over 14,000 times, let's count them too
let installationPriorV1 = 14
counterElement.textContent = (installationPriorV1 + Math.round(allDownloads / 1000)) + ",000"
}
})
</script>
<h2>
With more than <strong id="installation-counter">10,000</strong> installations to date, Tart has been adopted for various scenarios.
With more than <strong id="installation-counter">25,000</strong> installations to date, Tart has been adopted for various scenarios.
Its applications range from powering CI/CD pipelines and reproducible local development environments,
to helping in the testing of device management systems without actual physical devices.
</h2>
+1 -1
View File
@@ -7,7 +7,7 @@ def test_run(tart):
vm_name = f"integration-test-run-{uuid.uuid4()}"
# Instantiate a VM with admin:admin SSH access
tart.run(["clone", "ghcr.io/cirruslabs/macos-ventura-base:latest", vm_name])
tart.run(["clone", "ghcr.io/cirruslabs/macos-sonoma-base:latest", vm_name])
# Run the VM asynchronously
tart_run_process = tart.run_async(["run", vm_name])
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
# helper script to build and run a signed tart binary
# usage: ./scripts/run-signed.sh run ventura-base
# usage: ./scripts/run-signed.sh run sonoma-base
set -e