mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-18 11:55:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1bed25023 | ||
|
|
b39c3c9b52 | ||
|
|
8554e56e4b | ||
|
|
3fdcc5c33c | ||
|
|
182ddf0268 |
@@ -35,3 +35,5 @@ brews:
|
|||||||
homepage: https://github.com/cirruslabs/tart
|
homepage: https://github.com/cirruslabs/tart
|
||||||
description: Run macOS VMs on Apple Silicon
|
description: Run macOS VMs on Apple Silicon
|
||||||
skip_upload: auto
|
skip_upload: auto
|
||||||
|
custom_block: |
|
||||||
|
depends_on :macos => :monterey
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Built by CI engineers for your automation needs. Here are some highlights of Tar
|
|||||||
* Use Tart Packer Plugin to automate VM creation.
|
* Use Tart Packer Plugin to automate VM creation.
|
||||||
* Built-in CI integration.
|
* Built-in CI integration.
|
||||||
|
|
||||||
Try running a Tart VM on your Apple Silicon device (will download a 25 GB image):
|
Try running a Tart VM on your Apple Silicon device running macOS Monterey or later (will download a 25 GB image):
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew install cirruslabs/cli/tart
|
brew install cirruslabs/cli/tart
|
||||||
@@ -20,9 +20,13 @@ tart run monterey-base
|
|||||||
|
|
||||||
## CI Integration
|
## CI Integration
|
||||||
|
|
||||||
[Cirrus CLI](https://github.com/cirruslabs/cirrus-cli) is an open-sourced CI-agnostic tool that can run workloads
|
Tart itself is only responsible for managing virtual machines, but we've built Tart support into a tool called Cirrus CLI
|
||||||
inside containers via Docker or Podman and now inside macOS VMs via Tart. Put the following `.cirrus.yml` file
|
also developed by Cirrus Labs. [Cirrus CLI](https://github.com/cirruslabs/cirrus-cli) is a command line tool with
|
||||||
in the root of your repository:
|
one configuration format to execute common CI steps (run a script, cache a folder, etc.) locally or in any CI system.
|
||||||
|
We built Cirrus CLI to solve "But it works on my machine!" problem.
|
||||||
|
|
||||||
|
Here is an example of a `.cirrus.yml` configuration file which will start a Tart VM, will copy over working directory and
|
||||||
|
will run scripts and [other instructions](https://cirrus-ci.org/guide/writing-tasks/#supported-instructions) inside the virtual machine:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
task:
|
task:
|
||||||
@@ -37,7 +41,7 @@ task:
|
|||||||
- sleep 15
|
- sleep 15
|
||||||
```
|
```
|
||||||
|
|
||||||
Run it locally or in CI with the following command:
|
Put the above `.cirrus.yml` file in the root of your repository and run it with the following command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew install cirruslabs/cli/cirrus
|
brew install cirruslabs/cli/cirrus
|
||||||
@@ -78,7 +82,7 @@ Please refer to `tart set --help` for additional details.
|
|||||||
|
|
||||||
### Building with Packer
|
### Building with Packer
|
||||||
|
|
||||||
Please refer to [Tart Packer Plugin reposiotry](https://github.com/cirruslabs/packer-plugin-tart) for setup instructions.
|
Please refer to [Tart Packer Plugin repository](https://github.com/cirruslabs/packer-plugin-tart) for setup instructions.
|
||||||
Here is an example of a template to build `monterey-base` local image based of a remote image:
|
Here is an example of a template to build `monterey-base` local image based of a remote image:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ struct Pull: AsyncParsableCommand {
|
|||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
do {
|
do {
|
||||||
|
// Be more liberal when accepting local image as argument,
|
||||||
|
// see https://github.com/cirruslabs/tart/issues/36
|
||||||
|
if VMStorageLocal().exists(remoteName) {
|
||||||
|
print("\"\(remoteName)\" is a local image, nothing to pull here!")
|
||||||
|
|
||||||
|
Foundation.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
let remoteName = try RemoteName(remoteName)
|
let remoteName = try RemoteName(remoteName)
|
||||||
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)
|
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,30 @@ enum RegistryError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct TokenResponse: Decodable {
|
struct TokenResponse: Decodable {
|
||||||
let creationTime = Date()
|
let defaultIssuedAt = Date()
|
||||||
|
let defaultExpiresIn = 60
|
||||||
|
|
||||||
var token: String
|
var token: String
|
||||||
var expires_in: Int?
|
var expiresIn: Int?
|
||||||
|
var issuedAt: Date?
|
||||||
|
|
||||||
|
static func parse(fromData: Data) throws -> Self {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
|
// RFC3339 date formatter from Apple's documentation[1]
|
||||||
|
//
|
||||||
|
// [1]: https://developer.apple.com/documentation/foundation/dateformatter
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||||
|
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
|
||||||
|
|
||||||
|
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||||
|
|
||||||
|
return try decoder.decode(TokenResponse.self, from: fromData)
|
||||||
|
}
|
||||||
|
|
||||||
var tokenExpiresAt: Date {
|
var tokenExpiresAt: Date {
|
||||||
get {
|
get {
|
||||||
@@ -23,7 +43,7 @@ struct TokenResponse: Decodable {
|
|||||||
//
|
//
|
||||||
// [1]: https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
|
// [1]: https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
|
||||||
|
|
||||||
creationTime + TimeInterval(expires_in ?? 60)
|
(issuedAt ?? defaultIssuedAt) + TimeInterval(expiresIn ?? defaultExpiresIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +253,7 @@ class Registry {
|
|||||||
+ "while retrieving an authentication token", details: String(decoding: tokenResponseRaw, as: UTF8.self))
|
+ "while retrieving an authentication token", details: String(decoding: tokenResponseRaw, as: UTF8.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAuthToken = try JSONDecoder().decode(TokenResponse.self, from: tokenResponseRaw)
|
currentAuthToken = try TokenResponse.parse(fromData: tokenResponseRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func authAwareRequest(request: URLRequest) async throws -> (Data, HTTPURLResponse) {
|
private func authAwareRequest(request: URLRequest) async throws -> (Data, HTTPURLResponse) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import tart
|
||||||
|
|
||||||
|
final class TokenResponseTests: XCTestCase {
|
||||||
|
func testBasic() throws {
|
||||||
|
let tokenResponseRaw = Data("{\"token\":\"some token\"}".utf8)
|
||||||
|
let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw)
|
||||||
|
|
||||||
|
XCTAssertEqual(tokenResponse.token, "some token")
|
||||||
|
|
||||||
|
let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(60)
|
||||||
|
XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt))
|
||||||
|
|
||||||
|
XCTAssertTrue(tokenResponse.isValid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExpirationBasic() throws {
|
||||||
|
let tokenResponseRaw = Data("{\"token\":\"some token\",\"expires_in\":2}".utf8)
|
||||||
|
let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw)
|
||||||
|
|
||||||
|
XCTAssertEqual(tokenResponse.expiresIn, 2)
|
||||||
|
|
||||||
|
let expectedTokenExpiresAtRange = Date()...Date().addingTimeInterval(2)
|
||||||
|
XCTAssertTrue(expectedTokenExpiresAtRange.contains(tokenResponse.tokenExpiresAt))
|
||||||
|
|
||||||
|
XCTAssertTrue(tokenResponse.isValid)
|
||||||
|
_ = XCTWaiter.wait(for: [expectation(description: "Wait 3 seconds for the token to become invalid")], timeout: 2)
|
||||||
|
XCTAssertFalse(tokenResponse.isValid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExpirationWithIssuedAt() throws {
|
||||||
|
let tokenResponseRaw = Data("{\"token\":\"some token\",\"expires_in\":3600,\"issued_at\":\"1970-01-01T00:00:00Z\"}".utf8)
|
||||||
|
let tokenResponse = try TokenResponse.parse(fromData: tokenResponseRaw)
|
||||||
|
|
||||||
|
XCTAssertEqual(Date(timeIntervalSince1970: 3600), tokenResponse.tokenExpiresAt)
|
||||||
|
XCTAssertFalse(tokenResponse.isValid)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user