mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-01 19:51:10 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8554e56e4b | ||
|
|
3fdcc5c33c | ||
|
|
182ddf0268 | ||
|
|
24375c24f3 | ||
|
|
4ebec53c77 |
@@ -35,3 +35,5 @@ brews:
|
||||
homepage: https://github.com/cirruslabs/tart
|
||||
description: Run macOS VMs on Apple Silicon
|
||||
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.
|
||||
* 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
|
||||
brew install cirruslabs/cli/tart
|
||||
@@ -78,7 +78,7 @@ Please refer to `tart set --help` for additional details.
|
||||
|
||||
### 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:
|
||||
|
||||
```json
|
||||
@@ -160,3 +160,23 @@ tart pull acme.io/remoteorg/name:latest my-local-vm-name
|
||||
Tart is a relatively small project, and it didn't feel right to try to monetize it.
|
||||
Apple did all the heavy lifting with their `Virtualization.Framework`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>How to change VM's disk size?</summary>
|
||||
|
||||
You can choose disk size upon creation of a virtual machine:
|
||||
|
||||
```shell
|
||||
tart create --from-ipsw=latest --disk-size=25 monterey-vanilla
|
||||
```
|
||||
|
||||
For an existing VM please use [Packer Plugin](https://github.com/cirruslabs/packer-plugin-tart) which can increase
|
||||
disk size for new virtual machines. Here is an example of [how to change disk size in a Packer template](https://github.com/cirruslabs/macos-image-templates/blob/fb0bcf68e0b093129136875c050205a66729b596/templates/base.pkr.hcl#L15).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Nested virtualization support?</summary>
|
||||
|
||||
Tart is limited by functionality of Apple's `Virtualization.Framework`. At the moment `Virtualization.Framework`
|
||||
doesn't support nested virtualization.
|
||||
</details>
|
||||
|
||||
@@ -8,11 +8,31 @@ enum RegistryError: Error {
|
||||
}
|
||||
|
||||
struct TokenResponse: Decodable {
|
||||
let creationTime = Date()
|
||||
|
||||
let defaultIssuedAt = Date()
|
||||
let defaultExpiresIn = 60
|
||||
|
||||
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 {
|
||||
get {
|
||||
// Tokens can expire and expire_in field is used to determine when:
|
||||
@@ -22,8 +42,8 @@ struct TokenResponse: Decodable {
|
||||
// >a token should never be returned with less than 60 seconds to live.
|
||||
//
|
||||
// [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))
|
||||
}
|
||||
|
||||
currentAuthToken = try JSONDecoder().decode(TokenResponse.self, from: tokenResponseRaw)
|
||||
currentAuthToken = try TokenResponse.parse(fromData: tokenResponseRaw)
|
||||
}
|
||||
|
||||
private func authAwareRequest(request: URLRequest) async throws -> (Data, HTTPURLResponse) {
|
||||
|
||||
@@ -52,8 +52,10 @@ extension VMDirectory {
|
||||
}
|
||||
|
||||
// Progress
|
||||
defaultLogger.appendNewLine("pulling disk...")
|
||||
let progress = Progress(totalUnitCount: Int64(diskLayers.map{ $0.size }.reduce(0) { $0 + $1 }))
|
||||
let diskCompressedSize: Int64 = Int64(diskLayers.map {$0.size}.reduce(0) {$0 + $1})
|
||||
let prettyDiskSize = String(format: "%.1f", Double(diskCompressedSize) / 1_000_000_000.0)
|
||||
defaultLogger.appendNewLine("pulling disk (\(prettyDiskSize) GB compressed)...")
|
||||
let progress = Progress(totalUnitCount: diskCompressedSize)
|
||||
ProgressObserver(progress).log(defaultLogger)
|
||||
|
||||
for diskLayer in diskLayers {
|
||||
|
||||
@@ -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