Compare commits

...
8 Commits
Author SHA1 Message Date
Fedor Korotkov a9e2a19015 Lowercase tart.app (#751)
So signing and `embedded.provisionprofile` work as expected.

Related to #746
2024-03-11 12:23:12 -04:00
Fedor Korotkov 89ff5f6b65 Respect name in case of a single directory mount (#750)
Fixes #748
2024-03-11 15:01:06 +00:00
Nikolay Edigaryev 6bf39e73d0 Prefer USB keyboard and screen coordinate pointing devices (#747) 2024-03-11 13:16:40 +00:00
Tor Arne VestbøandNikolay Edigaryev 0b693f6bc9 Improve macOS app integration (#746)
* Improve macOS app integration

Tart is now a proper application bundle, with the name and icon
declared in the Info.plist, which we were missing.

This also allows us to declare the app as LSBackgroundOnly
as a default, which means that 'tart create' and similar
background commands will not show the application icon in
the dock, while 'tart run' will, thanks to it overriding
the activation policy of the app.

For now the logic of creating the Tart.app bundle is duplicated
between the CI packaging scripts and the run-signed.sh script.
Now that these scripts are growing, it makes sense to look
at whether we can share the logic somehow, e.g. by building
the application bundle directly during build, and packaging
that, instead of creating it as a post install step.

* Integration tests: fix DockerContainer import

To work around the breaking change in 4.0.0,
see https://github.com/testcontainers/testcontainers-python/commit/383b12e9d63b4b105c65e587e87c380355871a36.

* .cirrus.yml(Release (Dry Run)): no need to install Sentry CLI

---------

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2024-03-11 09:05:56 -04:00
Fedor Korotkov 99bbd838a1 Introduce --net-host flag to enforce host-only network (#743)
* Experimental `--net-host-only` option

* Use Softnet's host networking
2024-03-01 13:57:39 +04:00
Fedor Korotkov 5c7743b7cd Show both size and actual size of files (#742)
Right now we show only actual size of files on disk which excludes empty blocks of the recently introduced sparced format in #671. This makes impossible to get info about disk size that we just set via `tart set`.

Here is an example of `tart list` output before the change:

```
Source Name                                                                                                            Size State
local  sonoma-base                                                                                                     22   stopped
local  sonoma-vanilla                                                                                                  18   stopped
local  sonoma-xcode                                                                                                    67   stopped
local  ubuntu                                                                                                          1    stopped
oci    ghcr.io/cirruslabs/macos-sonoma-base:latest                                                                     22   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-base@sha256:16c1593bbaf787b20b3c0bc094c5b6baf71c937d22c2e4596da85ac55c92e6cc    22   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-vanilla:14.3                                                                    17   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-vanilla@sha256:23c4e853d48d00a4333346d66a32b2b5aad900cc0dc10e7ecb9dbe67b6f587f4 17   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-xcode:latest                                                                    67   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-xcode@sha256:d0cb8d01424a68b89e0f16f5371bf2152b2c115bd886341a6ba8da42121d1f41   67   stopped
oci    ghcr.io/cirruslabs/ubuntu:22.04                                                                                 1    stopped
oci    ghcr.io/cirruslabs/ubuntu@sha256:037763feb7a15d6077edeb7a097738c34313637d16036764b4c196d28d8b429c               1    stopped
```

And here is the output after the change:

```
Source Name                                                                                                            Disk Size State
local  sonoma-base                                                                                                     50   22   stopped
local  sonoma-vanilla                                                                                                  50   18   stopped
local  sonoma-xcode                                                                                                    90   67   stopped
local  ubuntu                                                                                                          20   1    stopped
oci    ghcr.io/cirruslabs/macos-sonoma-base:latest                                                                     50   22   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-base@sha256:16c1593bbaf787b20b3c0bc094c5b6baf71c937d22c2e4596da85ac55c92e6cc    50   22   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-vanilla:14.3                                                                    50   17   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-vanilla@sha256:23c4e853d48d00a4333346d66a32b2b5aad900cc0dc10e7ecb9dbe67b6f587f4 50   17   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-xcode:latest                                                                    90   67   stopped
oci    ghcr.io/cirruslabs/macos-sonoma-xcode@sha256:d0cb8d01424a68b89e0f16f5371bf2152b2c115bd886341a6ba8da42121d1f41   90   67   stopped
oci    ghcr.io/cirruslabs/ubuntu:22.04                                                                                 20   1    stopped
oci    ghcr.io/cirruslabs/ubuntu@sha256:037763feb7a15d6077edeb7a097738c34313637d16036764b4c196d28d8b429c               20   1    stopped
```

Additionally, `tart get` will print actual size with a 3 decimal point precision which will help to track growth in disk images for our templates.

`tart get` before:

```
CPU Memory Disk Display  State
4   8192   67   1024x768 stopped
```

`tart get` after:

```
CPU Memory Disk Size   Display  State
4   8192   90   67.333 1024x768 stopped
```
2024-02-27 20:13:38 +04:00
Fedor Korotkov a40e104c03 Update Platinum Tier (#741)
5x jump in price from Gold to Platinum is a bit too high. Most of known large deployments target 200-300 hosts. Let's accommodate such users by lowering Platinum tier.
2024-02-23 15:27:30 +00:00
Nikolay Edigaryev e2d6c13ed0 DHCP MAC-address resolver: handle duplicate leases (#740) 2024-02-23 16:49:06 +04:00
21 changed files with 107 additions and 37 deletions
+2
View File
@@ -7,6 +7,8 @@ export VERSION="${CIRRUS_TAG:-0}"
mkdir -p .ci/pkg/ mkdir -p .ci/pkg/
cp .build/arm64-apple-macosx/release/tart .ci/pkg/tart cp .build/arm64-apple-macosx/release/tart .ci/pkg/tart
cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile
cp Resources/AppIcon.png .ci/pkg/AppIcon.png
cp Resources/Info.plist .ci/pkg/Info.plist
pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \ pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \
--scripts .ci/pkg/scripts \ --scripts .ci/pkg/scripts \
--install-location "/Library/Application Support/Tart" \ --install-location "/Library/Application Support/Tart" \
+3 -1
View File
@@ -3,9 +3,11 @@
set -e set -e
# fix structure # fix structure
mkdir -p "$2/tart.app/Contents/MacOS" mkdir -p "$2/tart.app/Contents/MacOS" "$2/tart.app/Resources"
mv "$2/tart" "$2/tart.app/Contents/MacOS/tart" mv "$2/tart" "$2/tart.app/Contents/MacOS/tart"
mv "$2/embedded.provisionprofile" "$2/tart.app/Contents/embedded.provisionprofile" mv "$2/embedded.provisionprofile" "$2/tart.app/Contents/embedded.provisionprofile"
mv "$2/AppIcon.png" "$2/tart.app/Resources/AppIcon.png"
mv "$2/Info.plist" "$2/tart.app/Contents/Info.plist"
echo "#!/bin/sh" > /usr/local/bin/tart echo "#!/bin/sh" > /usr/local/bin/tart
echo "exec '$2/tart.app/Contents/MacOS/tart' \"\$@\"" >> /usr/local/bin/tart echo "exec '$2/tart.app/Contents/MacOS/tart' \"\$@\"" >> /usr/local/bin/tart
-1
View File
@@ -89,7 +89,6 @@ task:
install_script: install_script:
- brew install go goreleaser/tap/goreleaser-pro - brew install go goreleaser/tap/goreleaser-pro
- brew install mitchellh/gon/gon - brew install mitchellh/gon/gon
- curl -sL https://sentry.io/get-cli/ | sh
info_script: info_script:
- security find-identity -v - security find-identity -v
- xcodebuild -version - xcodebuild -version
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>tart</string>
<key>CFBundleIdentifier</key>
<string>org.cirruslabs.tart</string>
<key>CFBundleExecutable</key>
<string>tart</string>
<key>LSBackgroundOnly</key>
<string>1</string>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon.png</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -73,7 +73,7 @@ struct Clone: AsyncParsableCommand {
// APFS is doing copy-on-write so the above cloning operation (just copying files on disk) // APFS is doing copy-on-write so the above cloning operation (just copying files on disk)
// is not actually claiming new space until the VM is started and it writes something to disk. // is not actually claiming new space until the VM is started and it writes something to disk.
// So once we clone the VM let's try to claim a little bit of space for the VM to run. // So once we clone the VM let's try to claim a little bit of space for the VM to run.
try Prune.reclaimIfNeeded(UInt64(sourceVM.sizeBytes()), sourceVM) try Prune.reclaimIfNeeded(UInt64(sourceVM.allocatedSizeBytes()), sourceVM)
}, onCancel: { }, onCancel: {
try? FileManager.default.removeItem(at: tmpVMDir.baseURL) try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
}) })
+2 -2
View File
@@ -5,6 +5,7 @@ fileprivate struct VMInfo: Encodable {
let CPU: Int let CPU: Int
let Memory: UInt64 let Memory: UInt64
let Disk: Int let Disk: Int
let Size: String
let Display: String let Display: String
let Running: Bool let Running: Bool
let State: String let State: String
@@ -22,10 +23,9 @@ struct Get: AsyncParsableCommand {
func run() async throws { func run() async throws {
let vmDir = try VMStorageLocal().open(name) let vmDir = try VMStorageLocal().open(name)
let vmConfig = try VMConfig(fromURL: vmDir.configURL) let vmConfig = try VMConfig(fromURL: vmDir.configURL)
let diskSizeInGb = try vmDir.sizeGB()
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024 let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb, let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000),
Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state()) Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state())
print(format.renderSingle(info)) print(format.renderSingle(info))
} }
+3 -2
View File
@@ -5,6 +5,7 @@ import SwiftUI
fileprivate struct VMInfo: Encodable { fileprivate struct VMInfo: Encodable {
let Source: String let Source: String
let Name: String let Name: String
let Disk: Int
let Size: Int let Size: Int
let Running: Bool let Running: Bool
let State: String let State: String
@@ -37,13 +38,13 @@ struct List: AsyncParsableCommand {
if source == nil || source == "local" { if source == nil || source == "local" {
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running(), State: vmDir.state()) try VMInfo(Source: "local", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), Running: vmDir.running(), State: vmDir.state())
}) })
} }
if source == nil || source == "oci" { if source == nil || source == "oci" {
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running(), State: vmDir.state()) try VMInfo(Source: "oci", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), Running: vmDir.running(), State: vmDir.state())
}) })
} }
+4 -4
View File
@@ -89,7 +89,7 @@ struct Prune: AsyncParsableCommand {
var prunablesToDelete: [Prunable] = [] var prunablesToDelete: [Prunable] = []
for prunable in prunables { for prunable in prunables {
let prunableSizeBytes = UInt64(try prunable.sizeBytes()) let prunableSizeBytes = UInt64(try prunable.allocatedSizeBytes())
if prunableSizeBytes <= spaceBudgetBytes { if prunableSizeBytes <= spaceBudgetBytes {
// Don't mark for deletion as // Don't mark for deletion as
@@ -158,7 +158,7 @@ struct Prune: AsyncParsableCommand {
.sorted { try $0.accessDate() < $1.accessDate() } .sorted { try $0.accessDate() < $1.accessDate() }
// Does it even make sense to start? // Does it even make sense to start?
let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +) let cacheUsedBytes = try prunables.map { try $0.allocatedSizeBytes() }.reduce(0, +)
if cacheUsedBytes < reclaimBytes { if cacheUsedBytes < reclaimBytes {
return return
} }
@@ -177,9 +177,9 @@ struct Prune: AsyncParsableCommand {
continue continue
} }
try SentrySDK.span?.setData(value: prunable.sizeBytes(), key: prunable.url.path) try SentrySDK.span?.setData(value: prunable.allocatedSizeBytes(), key: prunable.url.path)
cacheReclaimedBytes += try prunable.sizeBytes() cacheReclaimedBytes += try prunable.allocatedSizeBytes()
try prunable.delete() try prunable.delete()
} }
+19 -10
View File
@@ -117,6 +117,9 @@ struct Run: AsyncParsableCommand {
discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet")) discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet"))
var netSoftnet: Bool = false var netSoftnet: Bool = false
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
var netHost: Bool = false
#if arch(arm64) #if arch(arm64)
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\".")) @Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
#endif #endif
@@ -133,8 +136,14 @@ struct Run: AsyncParsableCommand {
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive") throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
} }
if netBridged.count > 0 && netSoftnet { // check that not more than one network option is specified
throw ValidationError("--net-bridged and --net-softnet are mutually exclusive") var netFlags = 0
if netBridged.count > 0 { netFlags += 1 }
if netSoftnet { netFlags += 1 }
if netHost { netFlags += 1 }
if netFlags > 1 {
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
} }
if graphics { if graphics {
@@ -184,7 +193,7 @@ struct Run: AsyncParsableCommand {
} }
} }
if netSoftnet && isInteractiveSession() { if (netSoftnet || netHost) && isInteractiveSession() {
try Softnet.configureSUIDBitIfNeeded() try Softnet.configureSUIDBitIfNeeded()
} }
@@ -340,9 +349,7 @@ struct Run: AsyncParsableCommand {
if noGraphics { if noGraphics {
// enter the main even loop, without bringing up any UI, // enter the main even loop, without bringing up any UI,
// and just wait for the VM to exit. // and just wait for the VM to exit.
let nsApp = NSApplication.shared NSApp.run()
nsApp.setActivationPolicy(.prohibited)
nsApp.run()
} else { } else {
runUI(suspendable, captureSystemKeys) runUI(suspendable, captureSystemKeys)
} }
@@ -365,10 +372,14 @@ struct Run: AsyncParsableCommand {
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? { func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
if netSoftnet { if netSoftnet {
let config = try VMConfig.init(fromURL: vmDir.configURL) let config = try VMConfig.init(fromURL: vmDir.configURL)
return try Softnet(vmMACAddress: config.macAddress.string) return try Softnet(vmMACAddress: config.macAddress.string)
} }
if netHost {
let config = try VMConfig.init(fromURL: vmDir.configURL)
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"])
}
if netBridged.count > 0 { if netBridged.count > 0 {
func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface { func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface {
let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in
@@ -466,7 +477,7 @@ struct Run: AsyncParsableCommand {
allNamedShares = false allNamedShares = false
} }
} }
if directoryShares.count == 1 { if directoryShares.count == 1 && directoryShares.first!.name == nil {
let directoryShare = directoryShares.first! let directoryShare = directoryShares.first!
let singleDirectoryShare = VZSingleDirectoryShare(directory: try directoryShare.createConfiguration()) let singleDirectoryShare = VZSingleDirectoryShare(directory: try directoryShare.createConfiguration())
sharingDevice.share = singleDirectoryShare sharingDevice.share = singleDirectoryShare
@@ -516,8 +527,6 @@ struct Run: AsyncParsableCommand {
nsApp.setActivationPolicy(.regular) nsApp.setActivationPolicy(.regular)
nsApp.activate(ignoringOtherApps: true) nsApp.activate(ignoringOtherApps: true)
nsApp.applicationIconImage = NSImage(data: AppIconData)
struct MainApp: App { struct MainApp: App {
static var disappearSignal: Int32 = SIGINT static var disappearSignal: Int32 = SIGINT
static var capturesSystemKeys: Bool = false static var capturesSystemKeys: Bool = false
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -45,7 +45,10 @@ class Leases {
(lease.mac, lease) (lease.mac, lease)
}) })
self.leases = Dictionary(uniqueKeysWithValues: leases) self.leases = Dictionary(leases) { (left, right) in
// When duplicate lease is found, prefer a newer lease over the older one
(left.expiresAt > right.expiresAt) ? left : right
}
} }
/// Parse leases from the host cache similarly to the PLCache_read() function found in Apple's Open Source releases. /// Parse leases from the host cache similarly to the PLCache_read() function found in Apple's Open Source releases.
+2 -2
View File
@@ -15,7 +15,7 @@ class Softnet: Network {
let vmFD: Int32 let vmFD: Int32
init(vmMACAddress: String) throws { init(vmMACAddress: String, extraArguments: [String] = []) throws {
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2) let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds) let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
@@ -30,7 +30,7 @@ class Softnet: Network {
try setSocketBuffers(softnetFD, 1 * 1024 * 1024); try setSocketBuffers(softnetFD, 1 * 1024 * 1024);
process.executableURL = try Self.softnetExecutableURL() process.executableURL = try Self.softnetExecutableURL()
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] + extraArguments
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false) process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
} }
+2 -2
View File
@@ -103,7 +103,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
func keyboards() -> [VZKeyboardConfiguration] { func keyboards() -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) { if #available(macOS 14, *) {
// Mac keyboard is only supported by guests starting with macOS Ventura // Mac keyboard is only supported by guests starting with macOS Ventura
return [VZMacKeyboardConfiguration(), VZUSBKeyboardConfiguration()] return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
} else { } else {
return [VZUSBKeyboardConfiguration()] return [VZUSBKeyboardConfiguration()]
} }
@@ -120,7 +120,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
func pointingDevices() -> [VZPointingDeviceConfiguration] { func pointingDevices() -> [VZPointingDeviceConfiguration] {
// Trackpad is only supported by guests starting with macOS Ventura // Trackpad is only supported by guests starting with macOS Ventura
[VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()] [VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
} }
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] { func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
+3
View File
@@ -8,5 +8,8 @@ protocol Prunable {
var url: URL { get } var url: URL { get }
func delete() throws func delete() throws
func accessDate() throws -> Date func accessDate() throws -> Date
// size on disk as seen in Finder including empty blocks
func sizeBytes() throws -> Int func sizeBytes() throws -> Int
// actual size on disk without empty blocks
func allocatedSizeBytes() throws -> Int
} }
+5 -1
View File
@@ -9,7 +9,11 @@ extension URL: Prunable {
try FileManager.default.removeItem(at: self) try FileManager.default.removeItem(at: self)
} }
func sizeBytes() throws -> Int { func allocatedSizeBytes() throws -> Int {
try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize! try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize!
} }
func sizeBytes() throws -> Int {
try resourceValues(forKeys: [.totalFileSizeKey]).totalFileSize!
}
} }
+8
View File
@@ -165,6 +165,14 @@ struct VMDirectory: Prunable {
try baseURL.accessDate() try baseURL.accessDate()
} }
func allocatedSizeBytes() throws -> Int {
try configURL.allocatedSizeBytes() + diskURL.allocatedSizeBytes() + nvramURL.allocatedSizeBytes()
}
func allocatedSizeGB() throws -> Int {
try allocatedSizeBytes() / 1000 / 1000 / 1000
}
func sizeBytes() throws -> Int { func sizeBytes() throws -> Int {
try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes() try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes()
} }
+23
View File
@@ -35,4 +35,27 @@ final class LeasesTests: XCTestCase {
XCTAssertEqual(IPv4Address("1.2.3.4"), leases.ResolveMACAddress(macAddress: macAddress)) XCTAssertEqual(IPv4Address("1.2.3.4"), leases.ResolveMACAddress(macAddress: macAddress))
} }
func testDuplicateYetNotExpiredLeases() throws {
let macAddress = MACAddress(fromString: "11:22:33:44:55:66")!
let leases = try Leases("""
{
name=debian
ip_address=192.168.64.1
hw_address=1,\(macAddress)
identifier=1,\(macAddress)
lease=\(Int((Date() + 10.minutes).timeIntervalSince1970).hex)
}
{
name=debian
ip_address=192.168.64.2
hw_address=1,\(macAddress)
identifier=1,\(macAddress)
lease=\(Int((Date() + 5.minutes).timeIntervalSince1970).hex)
}
""")
XCTAssertEqual(IPv4Address("192.168.64.1"), leases.ResolveMACAddress(macAddress: macAddress))
}
} }
@@ -62,7 +62,7 @@ and before reaching the 100 CPU cores limit is royalty-free and does not have th
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold 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#license-tiers),
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum 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)
(\$5000 per month) will be required, and for organizations that exceed 5000 CPU cores, a custom [Diamond 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)
(\$1 per core per month) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.** (\$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? ## Have we considered alternatives?
+2 -2
View File
@@ -40,9 +40,9 @@ Gold Tier license has a 500 CPU core limit for Tart and 20 Orchard Workers limit
### Platinum Tier ### Platinum Tier
If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$5000 per month. If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$3000 per month.
Platinum Tier license has a 5,000 CPU core limit for Tart and 200 Orchard Workers limit for Orchard. Platinum Tier license has a 3,000 CPU core limit for Tart and 200 Orchard Workers limit for Orchard.
### Diamond Tier ### Diamond Tier
+1 -1
View File
@@ -1,7 +1,7 @@
import requests import requests
from testcontainers.core.waiting_utils import wait_container_is_ready from testcontainers.core.waiting_utils import wait_container_is_ready
from testcontainers.general import DockerContainer from testcontainers.core.container import DockerContainer
class DockerRegistry(DockerContainer): class DockerRegistry(DockerContainer):
+4 -1
View File
@@ -8,8 +8,11 @@ set -e
swift build --product tart swift build --product tart
codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
mkdir -p .build/tart.app/Contents/MacOS rm -Rf .build/tart.app/
mkdir -p .build/tart.app/Contents/MacOS .build/tart.app/Contents/Resources
cp -c .build/debug/tart .build/tart.app/Contents/MacOS/tart cp -c .build/debug/tart .build/tart.app/Contents/MacOS/tart
cp -c Resources/embedded.provisionprofile .build/tart.app/Contents/embedded.provisionprofile cp -c Resources/embedded.provisionprofile .build/tart.app/Contents/embedded.provisionprofile
cp -c Resources/Info.plist .build/tart.app/Contents/Info.plist
cp -c Resources/AppIcon.png .build/tart.app/Contents/Resources
.build/tart.app/Contents/MacOS/tart "$@" .build/tart.app/Contents/MacOS/tart "$@"