Compare commits

...
14 Commits
Author SHA1 Message Date
Fedor Korotkov b2c923f2fe Properly enter main even loop in headless mode (#651)
Fixes #638
2023-11-09 00:05:00 +04:00
Fedor Korotkov c75009e46f Introduce --capture-system-keys flag (#650)
To allow guest to capture things like Cmd+Tab.

Fixes #636
2023-11-08 20:21:55 +04:00
Riain Condon 43e74ab769 fix docs to specify inside VM for gitlab runner (#649)
just adds specifically VM in the gitlab runner docs to avoid confusion of where the build and cache dirs are
2023-11-08 09:05:19 -05:00
Fedor Korotkov 1338864ed6 Don't install Sentry CLI via brew (#648)
Seems it installas 1.x version instead of 2.x. Sentry's documentation [recommends to use their script](https://docs.sentry.io/product/cli/installation/?original_referrer=https%3A%2F%2Fwww.google.com%2F#automatic-installation).
2023-11-07 16:23:20 +00:00
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
18 changed files with 227 additions and 40 deletions
+2 -1
View File
@@ -83,8 +83,9 @@ task:
- security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password101 build.keychain
- xcrun notarytool store-credentials "notarytool" --apple-id "hello@cirruslabs.org" --team-id "9M2P8L4D89" --password $AC_PASSWORD
install_script:
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
- brew install go goreleaser/tap/goreleaser-pro
- brew install mitchellh/gon/gon
- curl -sL https://sentry.io/get-cli/ | sh
info_script:
- security find-identity -v
- xcodebuild -version
+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>
+23 -8
View File
@@ -108,6 +108,10 @@ struct Run: AsyncParsableCommand {
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
var suspendable: Bool = false
@Flag(help: ArgumentHelp("Whether system hot keys should be sent to the guest instead of the host",
discussion: "If enabled then system hot keys like Cmd+Tab will be sent to the guest instead of the host."))
var captureSystemKeys: Bool = false
mutating func validate() throws {
if vnc && vncExperimental {
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
@@ -121,11 +125,21 @@ struct Run: AsyncParsableCommand {
throw ValidationError("--graphics and --no-graphics are mutually exclusive")
}
if (noGraphics || vnc || vncExperimental) && captureSystemKeys {
throw ValidationError("--captures-system-keys can only be used with the default VM view")
}
let localStorage = VMStorageLocal()
let vmDir = try localStorage.open(name)
if try vmDir.state() == "suspended" {
suspendable = true
}
if suspendable {
if dir.count > 0 {
throw ValidationError("Suspending VMs with shared directories is not supported")
}
}
}
@MainActor
@@ -298,9 +312,10 @@ struct Run: AsyncParsableCommand {
let useVNCWithoutGraphics = (vnc || vncExperimental) && !graphics
if noGraphics || useVNCWithoutGraphics {
dispatchMain()
// enter the main even loop and just wait for the VM to exit
NSApplication.shared.run()
} else {
runUI(suspendable)
runUI(suspendable, captureSystemKeys)
}
}
@@ -461,7 +476,7 @@ struct Run: AsyncParsableCommand {
return [device]
}
private func runUI(_ suspendable: Bool) {
private func runUI(_ suspendable: Bool, _ captureSystemKeys: Bool) {
let nsApp = NSApplication.shared
nsApp.setActivationPolicy(.regular)
nsApp.activate(ignoringOtherApps: true)
@@ -470,13 +485,14 @@ struct Run: AsyncParsableCommand {
struct MainApp: App {
static var disappearSignal: Int32 = SIGINT
static var capturesSystemKeys: Bool = false
@NSApplicationDelegateAdaptor private var appDelegate: MinimalMenuAppDelegate
var body: some Scene {
WindowGroup(vm!.name) {
Group {
VMView(vm: vm!).onAppear {
VMView(vm: vm!, capturesSystemKeys: MainApp.capturesSystemKeys).onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}.onDisappear {
let ret = kill(getpid(), MainApp.disappearSignal)
@@ -526,6 +542,7 @@ struct Run: AsyncParsableCommand {
}
MainApp.disappearSignal = suspendable ? SIGUSR1 : SIGINT
MainApp.capturesSystemKeys = captureSystemKeys
MainApp.main()
}
}
@@ -575,14 +592,12 @@ struct VMView: NSViewRepresentable {
typealias NSViewType = VZVirtualMachineView
@ObservedObject var vm: VM
var capturesSystemKeys: Bool
func makeNSView(context: Context) -> NSViewType {
let machineView = VZVirtualMachineView()
// Do not capture system keys so that shortcuts like
// Shift-Command-4 + Space (capture a screenshot of window)
// work on the host instead of the guest
machineView.capturesSystemKeys = false
machineView.capturesSystemKeys = capturesSystemKeys
// Enable automatic display reconfiguration
// for guests that support it
+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)
}
+1 -1
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
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

+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.
+10 -2
View File
@@ -1,7 +1,7 @@
# Cirrus Runners for GitHub Actions
*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.
Your actions will be executed in clean macOS virtual machines with 4 Apple M2 cores, compared to GitHub's own macOS runners with just 3 cores and only supporting the outdated Apple–Intel architecture.
*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
@@ -84,6 +84,14 @@ Note that Cirrus Runners will get added to the default runner group.
![](/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
+1 -1
View File
@@ -19,7 +19,7 @@ concurrent = 2
[[runners]]
# ...
executor = "custom"
builds_dir = "/Users/admin/builds" # directory inside the
builds_dir = "/Users/admin/builds" # directory inside the VM
cache_dir = "/Users/admin/cache"
[runners.feature_flags]
FF_RESOLVE_FULL_TLS_CHAIN = false
+5
View File
@@ -56,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).
+5 -8
View File
@@ -1,11 +1,5 @@
{% extends "base.html" %}
{% block announce %}
<a href="/blog/2023/09/20/tart-200-and-community-updates/">
🚀🚀🚀&nbsp&nbspAnnouncing <strong>Tart 2.0.0</strong>&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>