Compare commits

..
6 Commits
Author SHA1 Message Date
Nikolay Edigaryev fb954b7cc1 tart list: support -q (or --quiet) for automation purposes (#293) 2022-11-02 11:49:45 -04:00
Fedor KorotkovandNikolay Edigaryev 8cbcd2285b Document Cirrus Runners (#288)
* Document Cirrus Runners

Fixes #237

* Update README.md

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

* Remove redundant mention

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2022-10-27 10:17:13 -04:00
Pete Goldsmith 3d0d889c99 Remove whitespace (#285) 2022-10-22 08:17:42 -04:00
Nikolay Edigaryev 0e77f14dd7 OCI: always read channel until end (#284) 2022-10-18 11:44:14 -04:00
Nikolay Edigaryev 39e1b84423 Use URLSession.dataTask() with delegate instead of URLSession.bytes() (#282)
* Use URLSession.dataTask() with delegate instead of URLSession.bytes()

* Use URLSession.shared instead of creating a new one each time
2022-10-18 08:54:28 -04:00
Fedor Korotkov af7530ee50 Relax status code acceptance (#281)
* Relax status code acceptance

Fixes #273

* added a comment
2022-10-17 21:42:25 +04:00
8 changed files with 119 additions and 34 deletions
+33 -5
View File
@@ -28,14 +28,42 @@ Try running a Tart VM on your Apple Silicon device running macOS Monterey or lat
```shell
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-monterey-base:latest monterey-base
tart run monterey-base
tart clone ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
tart run ventura-base
```
![tart VM view app](Resources/TartScreenshot.png)
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartScreenshot.png"/>
## CI Integration
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.
Please read down below about currently supported services.
### Managed runners for your CI-as-a-service
At the moment Cirrus Runners only supports GitHub Actions, but we are actively working on adding more options.
Please [email us](mailto:hello@cirruslabs.org) if you are interested in a particular one.
#### GitHub Actions
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`:
```yaml
name: Test Suite
jobs:
test:
runs-on: ghcr.io/cirruslabs/macos-ventura-xcode:latest
```
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`.
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartGHARunners.png"/>
### Self-hosted CI
Tart itself is only responsible for managing virtual machines, but we've built Tart support into a tool called Cirrus CLI
also developed by Cirrus Labs. [Cirrus CLI](https://github.com/cirruslabs/cirrus-cli) is a command line tool with
one configuration format to execute common CI steps (run a script, cache a folder, etc.) locally or in any CI system.
@@ -64,14 +92,14 @@ brew install cirruslabs/cli/cirrus
cirrus run
```
![Cirrus CLI Run](Resources/TartCirrusCLI.gif)
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/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.
**Note:** Cirrus CI only allows [images managed and regularly updated by us](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos).
### Retrieving artifacts from within Tart VMs
#### Retrieving artifacts from within Tart VMs
In many cases there is a need to retrieve particular files or a folder from within a Tart virtual machine.
For example, the below `.cirrus.yml` configuration defines a single task that builds a `tart` binary and
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23728bb5438c88b3d0826170a5fa4b7aaad0fbd94a4769c8d492c9f75b57ba81
size 155885
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a43f541b1ab0b57ae2060d371cba5dbb1f5c80b89c76434b7154d8144f66e61
size 205325
oid sha256:7a4929ca4e02d4968904749028ada7072704d2a52cccb69335e1596452c822c7
size 1359834
+11 -2
View File
@@ -5,9 +5,14 @@ import SwiftUI
struct List: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "List created VMs")
@Flag(name: [.short, .long], help: ArgumentHelp("Only display VM names"))
var quiet: Bool = false
func run() async throws {
do {
print("Source\tName")
if !quiet {
print("Source\tName")
}
displayTable("local", try VMStorageLocal().list())
displayTable("oci", try VMStorageOCI().list().map { (name, vmDir, _) in (name, vmDir) })
@@ -22,7 +27,11 @@ struct List: AsyncParsableCommand {
private func displayTable(_ source: String, _ vms: [(String, VMDirectory)]) {
for (name, _) in vms.sorted(by: { left, right in left.0 < right.0 }) {
print("\(source)\t\(name)")
if quiet {
print(name)
} else {
print("\(source)\t\(name)")
}
}
}
}
+46
View File
@@ -0,0 +1,46 @@
import Foundation
import AsyncAlgorithms
class Fetcher: NSObject, URLSessionTaskDelegate, URLSessionDelegate, URLSessionDataDelegate {
let responseCh = AsyncThrowingChannel<URLResponse, Error>()
let dataCh = AsyncThrowingChannel<Data, Error>()
func fetch(_ request: URLRequest) async throws -> (AsyncThrowingChannel<Data, Error>, URLResponse) {
let task = URLSession.shared.dataTask(with: request)
task.delegate = self
task.resume()
// Wait for the response and only then return
var iter = responseCh.makeAsyncIterator()
let response = try await iter.next()!
return (dataCh, response)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
await responseCh.send(response)
return .allow
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let sema = DispatchSemaphore(value: 0)
Task {
await dataCh.send(data)
sema.signal()
}
sema.wait()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Premature termination
responseCh.fail(error)
dataCh.fail(error)
} else {
dataCh.finish()
}
}
}
+18 -19
View File
@@ -2,8 +2,6 @@ import Foundation
import Algorithms
import AsyncAlgorithms
let chunkSizeBytes = 1 * 1024 * 1024
enum RegistryError: Error {
case UnexpectedHTTPStatusCode(when: String, code: Int, details: String = "")
case MissingLocationHeader
@@ -31,11 +29,11 @@ extension Data {
}
}
extension URLSession.AsyncBytes {
extension AsyncThrowingChannel<Data, Error> {
func asData() async throws -> Data {
var result = Data()
for try await chunk in chunks(ofCount: chunkSizeBytes) {
for try await chunk in self {
result += chunk
}
@@ -214,8 +212,8 @@ class Registry {
parameters: lastChunk ? ["digest": digest] : [:],
body: chunk
)
let expectedStatus = lastChunk ? HTTPCode.Created.rawValue : HTTPCode.Accepted.rawValue
if response.statusCode != expectedStatus {
// always accept both statuses since AWS ECR is not following specification
if response.statusCode != HTTPCode.Created.rawValue && response.statusCode != HTTPCode.Accepted.rawValue {
throw RegistryError.UnexpectedHTTPStatusCode(when: "streaming blob to \(uploadLocation)",
code: response.statusCode, details: data.asText())
}
@@ -228,14 +226,14 @@ class Registry {
}
public func pullBlob(_ digest: String, handler: (Data) throws -> Void) async throws {
let (bytes, response) = try await bytesRequest(.GET, endpointURL("\(namespace)/blobs/\(digest)"))
let (channel, response) = try await channelRequest(.GET, endpointURL("\(namespace)/blobs/\(digest)"))
if response.statusCode != HTTPCode.Ok.rawValue {
let body = try await bytes.asData().asText()
let body = try await channel.asData().asText()
throw RegistryError.UnexpectedHTTPStatusCode(when: "pulling blob", code: response.statusCode,
details: body)
}
for try await part in bytes.chunks(ofCount: chunkSizeBytes) {
for try await part in channel {
try Task.checkCancellation()
try handler(Data(part))
@@ -256,20 +254,20 @@ class Registry {
body: Data? = nil,
doAuth: Bool = true
) async throws -> (Data, HTTPURLResponse) {
let (bytes, response) = try await bytesRequest(method, urlComponents,
let (channel, response) = try await channelRequest(method, urlComponents,
headers: headers, parameters: parameters, body: body, doAuth: doAuth)
return (try await bytes.asData(), response)
return (try await channel.asData(), response)
}
private func bytesRequest(
private func channelRequest(
_ method: HTTPMethod,
_ urlComponents: URLComponents,
headers: Dictionary<String, String> = Dictionary(),
parameters: Dictionary<String, String> = Dictionary(),
body: Data? = nil,
doAuth: Bool = true
) async throws -> (URLSession.AsyncBytes, HTTPURLResponse) {
) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var urlComponents = urlComponents
if urlComponents.queryItems == nil && !parameters.isEmpty {
@@ -294,14 +292,15 @@ class Registry {
currentAuthToken = nil
}
var (bytes, response) = try await authAwareRequest(request: request)
var (channel, response) = try await authAwareRequest(request: request)
if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue {
_ = try await channel.asData()
try await auth(response: response)
(bytes, response) = try await authAwareRequest(request: request)
(channel, response) = try await authAwareRequest(request: request)
}
return (bytes, response)
return (channel, response)
}
private func auth(response: HTTPURLResponse) async throws {
@@ -373,7 +372,7 @@ class Registry {
return nil
}
private func authAwareRequest(request: URLRequest) async throws -> (URLSession.AsyncBytes, HTTPURLResponse) {
private func authAwareRequest(request: URLRequest) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var request = request
if let token = currentAuthToken {
@@ -381,8 +380,8 @@ class Registry {
request.addValue(value, forHTTPHeaderField: name)
}
let (bytes, response) = try await URLSession.shared.bytes(for: request)
let (channel, response) = try await Fetcher().fetch(request)
return (bytes, response as! HTTPURLResponse)
return (channel, response as! HTTPURLResponse)
}
}
+4 -4
View File
@@ -24,7 +24,7 @@ let passphrases = [
"act",
"action",
"actor",
" actress",
"actress",
"actual",
"adapt",
"add",
@@ -829,7 +829,7 @@ let passphrases = [
"grow",
"grunt",
"guard",
" guess",
"guess",
"guide",
"guilt",
"guitar",
@@ -1476,7 +1476,7 @@ let passphrases = [
"retire",
"retreat",
"return",
" reunion",
"reunion",
"reveal",
"review",
"reward",
@@ -1764,7 +1764,7 @@ let passphrases = [
"swim",
"swing",
"switch",
" sword",
"sword",
"symbol",
"symptom",
"syrup",
+2 -2
View File
@@ -63,7 +63,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
static func retrieveIPSW(remoteURL: URL) async throws -> URL {
// Check if we already have this IPSW in cache
let (bytes, response) = try await URLSession.shared.bytes(from: remoteURL)
let (channel, response) = try await Fetcher().fetch(URLRequest(url: remoteURL))
if let hash = (response as! HTTPURLResponse).value(forHTTPHeaderField: "x-amz-meta-digest-sha256") {
let ipswLocation = try IPSWCache().locationFor(fileName: "sha256:\(hash).ipsw")
@@ -90,7 +90,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
let fileHandle = try FileHandle(forWritingTo: temporaryLocation)
let digest = Digest()
for try await chunk in bytes.chunks(ofCount: chunkSizeBytes) {
for try await chunk in channel {
let chunkAsData = Data(chunk)
fileHandle.write(chunkAsData)
digest.update(chunkAsData)