Compare commits

...
Author SHA1 Message Date
Nikolay Edigaryev 902b1a6c9c Fix integration tests (#1149)
* Use ghcr.io/cirruslabs/macos-tahoe-base:latest

* CI: "Test on Sequoia" can be named just "Test"

* integration-test: can use latest requests now that the bug is fixed
2025-10-09 18:55:59 -04:00
Eric Kolve 90d9500133 chore: adding no-keyboard, no-pointer options for run (#1091) 2025-10-09 15:29:28 -04:00
Nikolay Edigaryev b05c731510 FAQ: document creation and unlocking of the keychain headless machines (#1148)
* FAQ: document creation and unlocking of the keychain headless machines

* Remove extra spaces

* Fix typo: this commands → this command
2025-10-08 22:19:02 +04:00
Nikolay Edigaryev d762fe6fc1 tart run: do not recommend running "tart run" as root (#1147) 2025-10-08 12:44:05 +00:00
Stefan MitterrutznerandNikolay Edigaryev eff964b62a Avoid duplicate progress updates in CI logs (#1140)
* Avoid duplicate progress updates in CI logs

* Update Sources/tart/Logging/ProgressObserver.swift

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

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2025-09-29 15:57:39 +04:00
fsc-eriker 590e064e35 Update faq.md: Avoid useless use of grep | awk (#1142)
In "Connecting to a service running on host", refactor to a single Awk script in favor of grep | head | awk
2025-09-29 07:43:48 -04:00
fsc-eriker 839c6e7562 Update faq.md: Use question word order in subheading (#1143)
"How Tart is different from Anka" is not a question, and thus should not have a question mark. This PR proposes to change it into a question, but an equally valid fix is to drop the question mark.
2025-09-29 07:43:14 -04:00
Nikolay Edigaryev e3ee2da2fd Validate custom TART_HOME and provide a human-friendly error message (#1138)
* Validate custom TART_HOME and provide a human-friendly error message

* Safer way to calculate "descendingURLs"
2025-09-25 20:44:57 +04:00
Nikolay Edigaryev 84147f29b5 Document automatic resources set by the Orchard Worker (#1134)
* Fix MkDocs warnings w.r.t. absolute instead of relative links

* Document automatic resources set by the Orchard Worker

* .markdownlint.yml: ignore MD051
2025-09-23 00:02:39 +04:00
jxlwqqandjinxiaolong a655edd826 docs: update sshpass command to ignore known hosts file (#1136)
Co-authored-by: jinxiaolong <jinxiaolong@tuhu.cn>
2025-09-22 23:12:45 +04:00
Nikolay Edigaryev df100f1ca2 Improve credential provider errors (#1133) 2025-09-22 22:57:05 +04:00
39 changed files with 194 additions and 64 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
use_compute_credits: true
task:
name: Test on Sequoia
name: Test
alias: test
persistent_worker:
labels:
+2 -2
View File
@@ -45,8 +45,8 @@ struct Clone: AsyncParsableCommand {
}
func run() async throws {
let ociStorage = VMStorageOCI()
let localStorage = VMStorageLocal()
let ociStorage = try VMStorageOCI()
let localStorage = try VMStorageLocal()
if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) {
// Pull the VM in case it's OCI-based and doesn't exist locally yet
+1 -1
View File
@@ -17,7 +17,7 @@ struct Import: AsyncParsableCommand {
}
func run() async throws {
let localStorage = VMStorageLocal()
let localStorage = try VMStorageLocal()
// Create a temporary VM directory to which we will load the export file
let tmpVMDir = try VMDirectory.temporary()
+2
View File
@@ -64,6 +64,8 @@ struct Login: AsyncParsableCommand {
}
fileprivate class DictionaryCredentialsProvider: CredentialsProvider {
let userFriendlyName = "static dictionary credentials provider"
var credentials: Dictionary<String, (String, String)>
init(_ credentials: Dictionary<String, (String, String)>) {
+3 -3
View File
@@ -53,9 +53,9 @@ struct Prune: AsyncParsableCommand {
switch entries {
case "caches":
prunableStorages = [VMStorageOCI(), try IPSWCache()]
prunableStorages = [try VMStorageOCI(), try IPSWCache()]
case "vms":
prunableStorages = [VMStorageLocal()]
prunableStorages = [try VMStorageLocal()]
default:
throw ValidationError("unsupported --entries value, please specify either \"caches\" or \"vms\"")
}
@@ -152,7 +152,7 @@ struct Prune: AsyncParsableCommand {
let transaction = SentrySDK.startTransaction(name: "Pruning cache", operation: "prune", bindToScope: true)
defer { transaction.finish() }
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()]
let prunableStorages: [PrunableStorage] = [try VMStorageOCI(), try IPSWCache()]
let prunables: [Prunable] = try prunableStorages
.flatMap { try $0.prunables() }
.sorted { try $0.accessDate() < $1.accessDate() }
+1 -1
View File
@@ -35,7 +35,7 @@ struct Pull: AsyncParsableCommand {
func run() async throws {
// Be more liberal when accepting local image as argument,
// see https://github.com/cirruslabs/tart/issues/36
if VMStorageLocal().exists(remoteName) {
if try VMStorageLocal().exists(remoteName) {
print("\"\(remoteName)\" is a local image, nothing to pull here!")
return
+1 -1
View File
@@ -39,7 +39,7 @@ struct Push: AsyncParsableCommand {
var populateCache: Bool = false
func run() async throws {
let ociStorage = VMStorageOCI()
let ociStorage = try VMStorageOCI()
let localVMDir = try VMStorageHelper.open(localName)
let lock = try localVMDir.lock()
if try !lock.trylock() {
+1 -1
View File
@@ -17,7 +17,7 @@ struct Rename: AsyncParsableCommand {
}
func run() async throws {
let localStorage = VMStorageLocal()
let localStorage = try VMStorageLocal()
if !localStorage.exists(name) {
throw ValidationError("failed to rename a non-existent local VM: \(name)")
+22 -6
View File
@@ -134,11 +134,12 @@ struct Run: AsyncParsableCommand {
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.
To work with block devices, the easiest way is to modify their permissions to be accessible to the current user:
To work around this pass TART_HOME explicitly:
sudo chown $USER /dev/diskX
tart run sequoia --disk=/dev/diskX
sudo TART_HOME="$HOME/.tart" tart run sequoia --disk=/dev/disk0
Warning: after running the chown command above, all software running under the current user will be able to access /dev/diskX. If that violates your threat model, we recommend avoiding mounting block devices altogether.
""", valueName: "path[:options]"), completion: .file())
var disk: [String] = []
@@ -265,6 +266,12 @@ struct Run: AsyncParsableCommand {
#endif
var noTrackpad: Bool = false
@Flag(help: ArgumentHelp("Disable the pointer"))
var noPointer: Bool = false
@Flag(help: ArgumentHelp("Disable the keyboard"))
var noKeyboard: Bool = false
mutating func validate() throws {
if vnc && vncExperimental {
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
@@ -301,7 +308,7 @@ struct Run: AsyncParsableCommand {
}
}
let localStorage = VMStorageLocal()
let localStorage = try VMStorageLocal()
let vmDir = try localStorage.open(name)
if try vmDir.state() == .Suspended {
suspendable = true
@@ -316,8 +323,15 @@ struct Run: AsyncParsableCommand {
if noTrackpad {
throw ValidationError("--no-trackpad cannot be used with --suspendable")
}
if noKeyboard {
throw ValidationError("--no-keyboard cannot be used with --suspendable")
}
if noPointer {
throw ValidationError("--no-pointer cannot be used with --suspendable")
}
}
if noTrackpad {
let config = try VMConfig.init(fromURL: vmDir.configURL)
if config.os != .darwin {
@@ -334,7 +348,7 @@ struct Run: AsyncParsableCommand {
@MainActor
func run() async throws {
let localStorage = VMStorageLocal()
let localStorage = try VMStorageLocal()
let vmDir = try localStorage.open(name)
// Validate disk format support
@@ -393,7 +407,9 @@ struct Run: AsyncParsableCommand {
clipboard: !noClipboard,
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
noTrackpad: noTrackpad
noTrackpad: noTrackpad,
noPointer: noPointer,
noKeyboard: noKeyboard
)
let vncImpl: VNC? = try {
+22 -1
View File
@@ -9,7 +9,8 @@ struct Config {
var tartHomeDir: URL
if let customTartHome = ProcessInfo.processInfo.environment["TART_HOME"] {
tartHomeDir = URL(fileURLWithPath: customTartHome)
tartHomeDir = URL(fileURLWithPath: customTartHome, isDirectory: true)
try Self.validateTartHome(url: tartHomeDir)
} else {
tartHomeDir = FileManager.default
.homeDirectoryForCurrentUser
@@ -49,4 +50,24 @@ struct Config {
static func jsonDecoder() -> JSONDecoder {
JSONDecoder()
}
private static func validateTartHome(url: URL) throws {
let urlComponents = url.pathComponents
let descendingURLs = urlComponents.indices.map { i in
URL(fileURLWithPath: urlComponents[0...i].joined(separator: "/"))
}
for descendingURL in descendingURLs {
if FileManager.default.fileExists(atPath: descendingURL.path) {
continue
}
do {
try FileManager.default.createDirectory(at: descendingURL, withIntermediateDirectories: false)
} catch {
throw RuntimeError.Generic("TART_HOME is invalid: \(descendingURL.path) does not exist, yet we can't create it: \(error.localizedDescription)")
}
}
}
}
@@ -5,6 +5,7 @@ enum CredentialsProviderError: Error {
}
protocol CredentialsProvider {
var userFriendlyName: String { get }
func retrieve(host: String) throws -> (String, String)?
func store(host: String, user: String, password: String) throws
}
@@ -1,6 +1,8 @@
import Foundation
class DockerConfigCredentialsProvider: CredentialsProvider {
let userFriendlyName = "Docker configuration credentials provider"
func retrieve(host: String) throws -> (String, String)? {
let dockerConfigURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
if !FileManager.default.fileExists(atPath: dockerConfigURL.path) {
@@ -1,6 +1,8 @@
import Foundation
class EnvironmentCredentialsProvider: CredentialsProvider {
let userFriendlyName = "environment variable credentials provider"
func retrieve(host: String) throws -> (String, String)? {
if let tartRegistryHostname = ProcessInfo.processInfo.environment["TART_REGISTRY_HOSTNAME"],
tartRegistryHostname != host {
@@ -1,6 +1,8 @@
import Foundation
class KeychainCredentialsProvider: CredentialsProvider {
let userFriendlyName = "Keychain credentials provider"
func retrieve(host: String) throws -> (String, String)? {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
@@ -6,6 +6,8 @@ enum StdinCredentialsError: Error {
}
class StdinCredentials {
let userFriendlyName = "standard input credentials provider"
static func retrieve() throws -> (String, String) {
let user = try readStdinCredential(name: "username", prompt: "User: ", isSensitive: false)
let password = try readStdinCredential(name: "password", prompt: "Password: ", isSensitive: true)
+12 -2
View File
@@ -4,18 +4,28 @@ public class ProgressObserver: NSObject {
@objc var progressToObserve: Progress
var observation: NSKeyValueObservation?
var lastTimeUpdated = Date.now
private var lastRenderedLine: String?
public init(_ progress: Progress) {
progressToObserve = progress
}
func log(_ renderer: Logger) {
renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
let initialLine = ProgressObserver.lineToRender(progressToObserve)
renderer.appendNewLine(initialLine)
lastRenderedLine = initialLine
observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
let currentTime = Date.now
if self.progressToObserve.isFinished || currentTime.timeIntervalSince(self.lastTimeUpdated) >= 1.0 {
self.lastTimeUpdated = currentTime
renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
let line = ProgressObserver.lineToRender(self.progressToObserve)
// Skip identical renders so non-interactive logs only see new percent values.
if line == self.lastRenderedLine {
return
}
self.lastRenderedLine = line
renderer.updateLastLine(line)
}
}
}
+6 -2
View File
@@ -429,8 +429,12 @@ class Registry {
}
for provider in credentialsProviders {
if let (user, password) = try provider.retrieve(host: host) {
return (user, password)
do {
if let (user, password) = try provider.retrieve(host: host) {
return (user, password)
}
} catch (let e) {
print("Failed to retrieve credentials using \(provider.userFriendlyName), authentication may fail: \(e)")
}
}
return nil
+1 -1
View File
@@ -92,7 +92,7 @@ struct Root: AsyncParsableCommand {
do {
try Config().gc()
} catch {
fputs("Failed to perform garbage collection!\n\(error)\n", stderr)
fputs("Failed to perform garbage collection: \(error)\n", stderr)
}
}
+19 -5
View File
@@ -51,7 +51,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
clipboard: Bool = true,
sync: VZDiskImageSynchronizationMode = .full,
caching: VZDiskImageCachingMode? = nil,
noTrackpad: Bool = false
noTrackpad: Bool = false,
noPointer: Bool = false,
noKeyboard: Bool = false
) throws {
name = vmDir.name
config = try VMConfig.init(fromURL: vmDir.configURL)
@@ -73,7 +75,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
clipboard: clipboard,
sync: sync,
caching: caching,
noTrackpad: noTrackpad
noTrackpad: noTrackpad,
noPointer: noPointer,
noKeyboard: noKeyboard
)
virtualMachine = VZVirtualMachine(configuration: configuration)
@@ -316,7 +320,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
clipboard: Bool = true,
sync: VZDiskImageSynchronizationMode = .full,
caching: VZDiskImageCachingMode? = nil,
noTrackpad: Bool = false
noTrackpad: Bool = false,
noPointer: Bool = false,
noKeyboard: Bool = false
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
@@ -356,8 +362,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
} else {
configuration.keyboards = vmConfig.platform.keyboards()
if noTrackpad {
if noKeyboard {
configuration.keyboards = []
} else {
configuration.keyboards = vmConfig.platform.keyboards()
}
if noPointer {
configuration.pointingDevices = []
} else if noTrackpad {
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
} else {
configuration.pointingDevices = vmConfig.platform.pointingDevices()
+5 -1
View File
@@ -1,7 +1,11 @@
import Foundation
class VMStorageLocal: PrunableStorage {
let baseURL: URL = try! Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true)
let baseURL: URL
init() throws {
baseURL = try Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true)
}
private func vmURL(_ name: String) -> URL {
baseURL.appendingPathComponent(name, isDirectory: true)
+5 -1
View File
@@ -3,7 +3,11 @@ import Sentry
import Retry
class VMStorageOCI: PrunableStorage {
let baseURL = try! Config().tartCacheDir.appendingPathComponent("OCIs", isDirectory: true)
let baseURL: URL
init() throws {
baseURL = try Config().tartCacheDir.appendingPathComponent("OCIs", isDirectory: true)
}
private func vmURL(_ name: RemoteName) -> URL {
baseURL.appendingRemoteName(name)
+1
View File
@@ -11,3 +11,4 @@
"MD045": false # OK not to have a description for an image
"MD046": false # Code block style [Expected: fenced; Actual: indented]
"MD059": false # It's OK to have "here" links
"MD051": false # MkDocs generates "#-no-pki" anchors, but markdownlint expects "#--no-pki" anchors
@@ -60,9 +60,9 @@ device without a physical display connected. For example, a Mac Mini with a HDMI
but a Mac Mini on a desk with a connected physical display is considered a personal computer. **Usage on personal computers
and before reaching the 100 CPU cores limit is royalty-free and does not have the viral properties of AGPL.**
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold Tier License](/licensing#license-tiers),
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](/licensing#license-tiers)
(\$3000 per month) will be required, and for organizations that exceed 3000 CPU cores, a custom [Diamond Tier License](/licensing#license-tiers)
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold Tier License](../../licensing.md#license-tiers),
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](../../licensing.md#license-tiers)
(\$3000 per month) will be required, and for organizations that exceed 3000 CPU cores, a custom [Diamond Tier License](../../licensing.md#license-tiers)
(\$1 per core per month) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.**
## Have we considered alternatives?
+1 -1
View File
@@ -89,6 +89,6 @@ orchard dev
This will launch a development cluster with a single worker on your machine. Refer to [Orchard documentation](https://github.com/cirruslabs/orchard#creating-virtual-machines)
on how to create your first virtual machine and access it.
In a [separate blog post](/blog/2023/04/28/ssh-over-grpc-or-how-orchard-simplifies-accessing-vms-in-private-networks/)
In a [separate blog post](2023-04-28-orchard-ssh-over-grpc.md)
we’ll cover how Orchard implements seamless SSH access over a gRPC connection. Stay tuned and please don’t hesitate to
[reach out](https://github.com/cirruslabs/orchard/discussions/landing)!
@@ -64,7 +64,7 @@ We’ve also initially considered using [Yamux](https://github.com/hashicorp/yam
First of all, we’ve made the new port-forwarding functionality available for integrations via the Orchard’s REST API:
![OpenAPI documentation for Orchard's port-forwarding endpoint](/assets/images/orchard-port-forwarding-api.png)
![OpenAPI documentation for Orchard's port-forwarding endpoint](../../assets/images/orchard-port-forwarding-api.png)
All you need is to use a WebSocket client when accessing this endpoint to make it work.
+1 -1
View File
@@ -43,7 +43,7 @@ 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/).
-[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)).
+1 -1
View File
@@ -17,7 +17,7 @@ with preconfigured Tart installation that is optimized to work within AWS infras
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)
![EC2 M2 Pro](../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.
@@ -34,7 +34,7 @@ than recently announced Apple Silicon GitHub-manged runners that cost $0.16 per
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)
![Cirrus Runners Dashboard](../images/runners-price-performance-2.png)
As you can see above Cirrus Runners Dashboard focuses on 4 core metrics:
@@ -50,7 +50,7 @@ we can see that the downside of such great price performance is that jobs are wa
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)
![Cirrus Runners Dashboard](../images/runners-price-performance-3.png)
## Conclusion
+41 -5
View File
@@ -5,6 +5,42 @@ title: Frequently Asked Questions
description: Advanced configuration and troubleshooting tips for advanced configurations.
---
## Headless machines
Starting from macOS 15 (Sequoia), there's an undocumented requirement from [Virtualization.Framework](https://developer.apple.com/documentation/virtualization) (which Tart uses) to have an unlocked `login.keychain` available at the times when running a VM.
Without an existing and unlocked `login.keychain`, the VM won't start with errors like:
* `SecKeyCreateRandomKey_ios failed`
* `Failed to generate keypair`
* `Interaction is not allowed with the Security Server`
Below you'll find a couple of workarounds for this behavior.
### Log in via GUI at least once
Connect to the headless machine via [Screen Sharing](https://support.apple.com/guide/mac-help/share-the-screen-of-another-mac-mh14066/mac) and log in to a Mac user account. If you haven't done already, you can enable Screen Sharing [via the terminal](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-to-mac-instance.html#mac-instance-vnc).
Logging in graphically will automatically create the `login.keychain`. Afterward, you have two options:
* configure [automatic log in to a Mac user account](https://support.apple.com/en-us/102316)
* this will maintain a running user session (GUI) even after the machine reboots
* moreover, you can still lock the screen (either manually [or automatically](https://support.apple.com/guide/mac-help/change-lock-screen-settings-on-mac-mh11784/mac)), however, the security benefit of this is questionable
* use `security unlock-keychain login.keychain` to unlock the login keychain via the terminal
* this command also supports the `-p` command-line argument, which allows you to supply a password and unlock non-interactively
### Create and unlock the login keychain via the terminal
Compared to the previous approach, this one is fully automated, but might stop working at some point in the future:
```shell
security create-keychain -p '' login.keychain
security unlock-keychain -p '' login.keychain
security login-keychain -s login.keychain
```
Note that this will create a `login.keychain` with an empty password. Consider supplying a different value to `-p` or omitting the `-p` to enter the password interactively.
## Troubleshooting crashes
If you experience a crash or encounter another error while using the tart executable, you can collect debug information to assist with troubleshooting. Run the following command in a separate terminal window to gather logs from the Tart process and the macOS Virtualization subsystem:
@@ -34,7 +70,7 @@ Then from within a virtual machine you can access the service using the router's
or by running the following command in the Terminal:
```shell
netstat -nr | grep default | head -n 1 | awk '{print $2}'
netstat -nr | awk '/default/{print $2; exit}'
```
Note: that accessing host is only possible with the default NAT network. If you are running your virtual machines with
@@ -143,14 +179,14 @@ This is because Tart uses [Keychain](https://en.wikipedia.org/wiki/Keychain_(sof
To unlock the Keychain in an SSH session, run the following command, which will ask for your user's password:
```shell
security unlock-keychain
security unlock-keychain login.keychain
```
This command also supports the `-p` command-line argument that allows you to supply the password and unlock non-interactively, which is great for scripts.
This command also supports the `-p` command-line argument that allows you to supply a password and unlock non-interactively, which is great for scripts.
If that doesn't work for you for some reason, you can pass the credentials via the environment variables, see [Registry Authorization](integrations/vm-management.md#registry-authorization) for more details on how to do that.
Alternatively, you can pass the credentials via the environment variables, see [Registry Authorization](integrations/vm-management.md#registry-authorization) for more details on how to do that.
## How Tart is different from Anka?
## How is Tart different from Anka?
Under the hood Tart is using the same technology as Anka 3.0 so there should be no real difference in performance
or features supported. If there is some feature missing please don't hesitate to [create a feature request](https://github.com/cirruslabs/tart/issues).
+1 -1
View File
@@ -7,7 +7,7 @@ description: Run pipeline steps in isolated ephemeral Tart Virtual Machines.
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):
![](/assets/images/BuildkiteTartPlugin.png)
![](../assets/images/BuildkiteTartPlugin.png)
## Configuration
+1 -1
View File
@@ -33,7 +33,7 @@ brew install cirruslabs/cli/cirrus
cirrus run
```
![](/assets/images/TartCirrusCLI.gif)
![](../assets/images/TartCirrusCLI.gif)
[Cirrus CI](https://cirrus-ci.org/) already leverages Tart to power its macOS cloud infrastructure. The `.cirrus.yml`
config from above will just work in Cirrus CI and your tasks will be executed inside Tart VMs in our cloud.
+4 -4
View File
@@ -4,7 +4,7 @@ Orchard cluster consists of three components:
* Controller — responsible for managing the cluster and scheduling of resources
* Worker — responsible for executing the VMs
* Client — responsible for creating, modifying and removing the resources on the Controller, can either be an [Orchard CLI](/orchard/using-orchard-cli) or [an API consumer](/orchard/integration-guide)
* Client — responsible for creating, modifying and removing the resources on the Controller, can either be an [Orchard CLI](using-orchard-cli.md) or [an API consumer](integration-guide.md)
At the moment, only one Controller instance is currently supported, while you can deploy one or more Workers and run any number of Clients.
@@ -14,7 +14,7 @@ In terms of networking requirements, only Controller needs to be directly access
When an Orchard Client or a Worker connects to the Controller, they need to establish trust and verify that they're talking to the right Controller, so that no [man-in-the-middle attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) is possible.
Similarly to web-browsers (that rely on the [public key infrastructure](https://en.wikipedia.org/wiki/Public_key_infrastructure)) and SSH (which relies on semi-automated fingerprint verification), Orchard combines these two traits in a hybrid approach by defaulting to automatic PKI verification (can be disabled by [`--no-pki`](#--no-pki-override)) and falling-back to a manual verification for self-signed certificates.
Similarly to web-browsers (that rely on the [public key infrastructure](https://en.wikipedia.org/wiki/Public_key_infrastructure)) and SSH (which relies on semi-automated fingerprint verification), Orchard combines these two traits in a hybrid approach by defaulting to automatic PKI verification (can be disabled by [`--no-pki`](#-no-pki-override)) and falling-back to a manual verification for self-signed certificates.
This hybrid approach is needed because the Controller can be configured in two ways:
@@ -29,7 +29,7 @@ Below we'll explain how Orchard client and Worker secure the connection when acc
Client is associated with the Controller using a `orchard context create` command, which works as follows:
* Client attempts to connect to the Controller and validate its certificate using host's root CA set (can be disabled with [`--no-pki`](#--no-pki-override))
* Client attempts to connect to the Controller and validate its certificate using host's root CA set (can be disabled with [`--no-pki`](#-no-pki-override))
* if the Client encounters a *Controller with a publicly valid certificate*, that would be the last step and the association would succeed
* if the Client is dealing with *Controller with a self-signed certificate*, the Client will do another connection attempt to probe the Controller's certificate
* the probed Controller's certificate fingerprint is then presented to the user, and if the user agrees to trust it, the Client then considers that certificate to be trusted for a given context
@@ -53,7 +53,7 @@ The way Worker connects to the Controller using the `orchard worker run` command
* when the Bootstrap Token contains the Controller's certificate:
* the Orchard Worker will try to connect to the Controller with a trusted CA set containing only that certificate
* when the Bootstrap Token has no Controller's certificate:
* the Orchard Worker will try the PKI approach (can be disabled with [`--no-pki`](#--no-pki-override) to effectively prevent the Worker from connecting) and fail if certificate verification using PKI is not possible
* the Orchard Worker will try the PKI approach (can be disabled with [`--no-pki`](#-no-pki-override) to effectively prevent the Worker from connecting) and fail if certificate verification using PKI is not possible
### `--no-pki` override
+1 -1
View File
@@ -53,7 +53,7 @@ Here's other command-line arguments associated with this functionality:
* `--insecure-ssh-no-client-auth` — allow SSH clients to connect to the controller's SSH server without authentication, thus only authenticating on the target worker/VM's SSH server
* useful when you already have strong credentials on your VMs, and you want to share these VMs to others without additionally giving out Orchard Cluster credentials
Check out our [Jumping through the hoops: SSH jump host functionality in Orchard](/blog/2024/06/20/jumping-through-the-hoops-ssh-jump-host-functionality-in-orchard/) blog post for more information.
Check out our [Jumping through the hoops: SSH jump host functionality in Orchard](../blog/posts/2024-06-20-jumping-through-the-hoops.md) blog post for more information.
## Deployment Methods
+1 -1
View File
@@ -2,7 +2,7 @@ Orchard has a REST API that follows [OpenAPI specification](https://swagger.io/s
You can run `orchard dev` locally and navigate to `http://127.0.0.1:6120/v1/` for interactive documentation.
![](/assets/images/orchard/orchard-api-documentation-browser.png)
![](../assets/images/orchard/orchard-api-documentation-browser.png)
## Using the API
+9 -9
View File
@@ -4,7 +4,7 @@ a couple of VMs is not enough anymore for your needs? This is where [Orchard](ht
comes in to play!
It allows you to orchestrate multiple Tart-capable hosts from either an Orchard CLI (which we demonstrate below)
or [through the API](/orchard/integration-guide).
or [through the API](integration-guide.md).
The easiest way to start is to run Orchard in local development mode:
@@ -18,7 +18,7 @@ test both the CLI functionality and the API from a tool like cURL or programming
authenticate requests.
Note that in production deployments, these two components are started separately and enable security by default. Please
refer to [Deploying Controller](/orchard/deploying-controller) and [Deploying Workers](/orchard/deploying-workers) for
refer to [Deploying Controller](deploying-controller.md) and [Deploying Workers](deploying-workers.md) for
more information.
## Creating Virtual Machines
@@ -92,10 +92,10 @@ orchard delete vm sequoia-base
In addition to controlling the Orchard via the CLI arguments, there are environment variables that may be beneficial
both when automating Orchard and in daily use:
| Variable name | Description |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ORCHARD_HOME` | Override Orchard's home directory. Useful when running multiple Orchard instances on the same host and when testing. |
| `ORCHARD_LICENSE_TIER` | The default license limit only allows connecting 4 Orchard Workers to the Orchard Controller. If you've purchased a [Gold Tier License](/licensing/), set this variable to `gold` to increase the limit to 20 Orchard Workers. And if you've purchased a [Platinum Tier License](/licensing/), set this variable to `platinum` to increase the limit to 200 Orchard Workers. |
| `ORCHARD_URL` | Override controller URL on per-command basis. |
| `ORCHARD_SERVICE_ACCOUNT_NAME` | Override service account name (used for controller API auth) on per-command basis. |
| `ORCHARD_SERVICE_ACCOUNT_TOKEN` | Override service account token (used for controller API auth) on per-command basis. |
| Variable name | Description |
|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `ORCHARD_HOME` | Override Orchard's home directory. Useful when running multiple Orchard instances on the same host and when testing. |
| `ORCHARD_LICENSE_TIER` | The default license limit only allows connecting 4 Orchard Workers to the Orchard Controller. If you've purchased a [Gold Tier License](../licensing.md), set this variable to `gold` to increase the limit to 20 Orchard Workers. And if you've purchased a [Platinum Tier License](../licensing.md), set this variable to `platinum` to increase the limit to 200 Orchard Workers. |
| `ORCHARD_URL` | Override controller URL on per-command basis. |
| `ORCHARD_SERVICE_ACCOUNT_NAME` | Override service account name (used for controller API auth) on per-command basis. |
| `ORCHARD_SERVICE_ACCOUNT_TOKEN` | Override service account token (used for controller API auth) on per-command basis. |
+9
View File
@@ -75,3 +75,12 @@ orchard create vm --resources bandwidth-mbps=7500 <NAME>
However, after this VM is scheduled, the 10 Gbps Mac Studio will only be able to accommodate one more VM (due to internal Apple EULA limit for macOS virtualization) with `bandwidth-mbps=2500` or less.
After the VM finishes, the unused resources will be available again.
## Automatic resources
In addition to manually specifying resources when starting a worker, the following resources are discovered and set automatically by the worker for convenience:
* `org.cirruslabs.logical-cores` — number of logical cores on the host
* `org.cirruslabs.memory-mib` — total memory in MiB (mebibytes) on the host
Note that the values for these resources are scraped only once at worker startup.
+2 -2
View File
@@ -95,8 +95,8 @@ ssh admin@$(tart ip sequoia-base)
```bash
brew install cirruslabs/cli/sshpass
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip sequoia-base) "uname -a"
sshpass -p admin ssh -o "StrictHostKeyChecking no" admin@$(tart ip sequoia-base) < script.sh
sshpass -p admin ssh -o "StrictHostKeyChecking no" -o "UserKnownHostsFile=/dev/null" admin@$(tart ip sequoia-base) "uname -a"
sshpass -p admin ssh -o "StrictHostKeyChecking no" -o "UserKnownHostsFile=/dev/null" admin@$(tart ip sequoia-base) < script.sh
```
## Mounting directories
+1 -1
View File
@@ -1,6 +1,6 @@
pytest
testcontainers
requests == 2.31.0 # work around https://github.com/psf/requests/issues/6707
requests
bitmath
pytest-dependency
paramiko
+1 -1
View File
@@ -9,7 +9,7 @@ def test_run(tart, run_opts):
vm_name = f"integration-test-run-{uuid.uuid4()}"
# Instantiate a VM with admin:admin SSH access
tart.run(["clone", "ghcr.io/cirruslabs/macos-sonoma-base:latest", vm_name])
tart.run(["clone", "ghcr.io/cirruslabs/macos-tahoe-base:latest", vm_name])
# Run the VM asynchronously
tart_run_process = tart.run_async(["run", vm_name] + run_opts)