Compare commits

..
5 Commits
Author SHA1 Message Date
Nikolay EdigaryevandFedor Korotkov d1bed25023 tart pull: be more liberal when accepting local image as argument (#70)
* tart pull: be more liberal when accepting local image as argument

* Update Sources/tart/Commands/Pull.swift

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>

* Single sentence and consistent capitalization

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
2022-05-15 18:03:15 -04:00
Fedor Korotkov b39c3c9b52 Reworked CI integration documentation (#69)
It was a bit confusing to folks not familiar with Cirrus CLI. Reworked the documentation to better introduce Cirrus CLI.
2022-05-15 13:25:25 +03:00
Nikolay Edigaryev 8554e56e4b Registry: use issued_at field in the token response message (#65)
* Registry: use issued_at field in the token response message

* Configure JSONDecoder instead of the Decodable itself
2022-05-12 22:33:02 +03:00
Nikolay Edigaryev 3fdcc5c33c .goreleaser(brews): depend on macOS Monterey (#64)
* .goreleaser(brews): depend on macOS Monterey

* README.md: mention that macOS Monterey or later is required
2022-05-12 00:42:12 +03:00
Austin Culter 182ddf0268 Minor typo: 'reposiotry' > 'repository' (#61)
On the tin.
2022-05-10 17:16:03 -04:00
5 changed files with 85 additions and 13 deletions
+2
View File
@@ -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
+10 -6
View File
@@ -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
@@ -20,9 +20,13 @@ tart run monterey-base
## CI Integration
[Cirrus CLI](https://github.com/cirruslabs/cirrus-cli) is an open-sourced CI-agnostic tool that can run workloads
inside containers via Docker or Podman and now inside macOS VMs via Tart. Put the following `.cirrus.yml` file
in the root of your repository:
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.
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
task:
@@ -37,7 +41,7 @@ task:
- 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
brew install cirruslabs/cli/cirrus
@@ -78,7 +82,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
+8
View File
@@ -10,6 +10,14 @@ struct Pull: AsyncParsableCommand {
func run() async throws {
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 registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)
+27 -7
View File
@@ -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) {
+38
View File
@@ -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)
}
}