Compare commits

..
4 Commits
10 changed files with 125 additions and 28 deletions
+11 -1
View File
@@ -191,10 +191,20 @@ tart push my-local-vm-name acme.io/remoteorg/name:latest acme.io/remoteorg/name:
#### Pulling a Remote Image
You can either pull an image:
```shell
tart pull acme.io/remoteorg/name:latest my-local-vm-name
tart pull acme.io/remoteorg/name:latest
```
...or instantiate a VM from a remote image:
```shell
tart clone acme.io/remoteorg/name:latest my-local-vm-name
```
This invocation calls the `tart pull` implicitly (if the image is not being present) before doing the actual cloning.
## FAQ
<details>
+1 -1
View File
@@ -45,7 +45,7 @@ struct Login: AsyncParsableCommand {
do {
let registry = try Registry(host: host, namespace: "", insecure: insecure,
credentialsProvider: credentialsProvider)
credentialsProviders: [credentialsProvider])
try await registry.ping()
} catch {
print("invalid credentials: \(error)")
@@ -0,0 +1,63 @@
import Foundation
class HelperProgramCredentialsProvider: CredentialsProvider {
func retrieve(host: String) throws -> (String, String)? {
let dockerConfigURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
if !FileManager.default.fileExists(atPath: dockerConfigURL.path) {
return nil
}
let config = try JSONDecoder().decode(DockerConfig.self, from: Data(contentsOf: dockerConfigURL))
if let helperProgram = config.credHelpers?[host] {
return try executeHelper(binaryName: "docker-credential-\(helperProgram)", host: host)
}
return nil
}
private func executeHelper(binaryName: String, host: String) throws -> (String, String)? {
guard let executableURL = resolveBinaryPath(binaryName) else {
throw CredentialsProviderError.Failed(message: "\(binaryName) not found in PATH")
}
let process = Process.init()
process.executableURL = executableURL
process.arguments = ["get"]
let outPipe = Pipe()
let inPipe = Pipe()
process.standardOutput = outPipe
process.standardError = outPipe
process.standardInput = inPipe
process.launch()
inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!)
inPipe.fileHandleForWriting.closeFile()
process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
throw CredentialsProviderError.Failed(message: "Docker helper failed!")
}
let getOutput = try JSONDecoder().decode(
DockerGetOutput.self, from: outPipe.fileHandleForReading.readDataToEndOfFile()
)
return (getOutput.Username, getOutput.Secret)
}
func store(host: String, user: String, password: String) throws {
throw CredentialsProviderError.Failed(message: "Docker helpers don't support storing!")
}
}
struct DockerConfig: Codable {
var credHelpers: Dictionary<String, String>? = Dictionary()
}
struct DockerGetOutput: Codable {
var Username: String
var Secret: String
}
+16 -7
View File
@@ -88,29 +88,29 @@ class Registry {
let baseURL: URL
let namespace: String
let credentialsProvider: CredentialsProvider
let credentialsProviders: [CredentialsProvider]
var currentAuthToken: Authentication? = nil
init(urlComponents: URLComponents,
namespace: String,
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
) throws {
baseURL = urlComponents.url!
self.namespace = namespace
self.credentialsProvider = credentialsProvider
self.credentialsProviders = credentialsProviders
}
convenience init(
host: String,
namespace: String,
insecure: Bool = false,
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
) throws {
let proto = insecure ? "http" : "https"
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProvider: credentialsProvider)
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProviders: credentialsProviders)
}
func ping() async throws {
@@ -303,7 +303,7 @@ class Registry {
let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: wwwAuthenticateRaw)
if wwwAuthenticate.scheme == "Basic" {
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
currentAuthToken = BasicAuthentication(user: user, password: password)
}
@@ -340,7 +340,7 @@ class Registry {
var headers: Dictionary<String, String> = Dictionary()
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
let encodedCredentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
headers["Authorization"] = "Basic \(encodedCredentials!)"
}
@@ -356,6 +356,15 @@ class Registry {
currentAuthToken = try TokenResponse.parse(fromData: bodyData)
}
private func lookupCredentials(host: String) throws -> (String, String)? {
for provider in credentialsProviders {
if let (user, password) = try provider.retrieve(host: host) {
return (user, password)
}
}
return nil
}
private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse {
var request = request
+10
View File
@@ -84,4 +84,14 @@ struct Darwin: Platform {
return result
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
if #available(macOS 13, *) {
// Trackpad is only supported starting with macOS Ventura
// macOS Monterey will continue using a USB device == .darwin
return [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()]
} else {
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
}
}
+4
View File
@@ -30,4 +30,8 @@ struct Linux: Platform {
return result
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
[VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
}
+1
View File
@@ -5,4 +5,5 @@ protocol Platform: Codable {
func bootLoader(nvramURL: URL) throws -> VZBootLoader
func platform(nvramURL: URL) -> VZPlatformConfiguration
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
func pointingDevices() -> [VZPointingDeviceConfiguration]
}
+1 -18
View File
@@ -12,7 +12,7 @@ class Softnet {
init(vmMACAddress: String) throws {
let binaryName = "softnet"
guard let executableURL = Self.resolveBinaryPath(binaryName) else {
guard let executableURL = resolveBinaryPath(binaryName) else {
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
}
@@ -43,23 +43,6 @@ class Softnet {
process.waitUntilExit()
}
private static func resolveBinaryPath(_ name: String) -> URL? {
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
return nil
}
for pathComponent in path.split(separator: ":") {
let url = URL(fileURLWithPath: String(pathComponent))
.appendingPathComponent(name, isDirectory: false)
if FileManager.default.fileExists(atPath: url.path) {
return url
}
}
return nil
}
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
var option_value = sizeBytes
let option_len = socklen_t(MemoryLayout<Int>.size)
+17
View File
@@ -5,3 +5,20 @@ extension Collection {
indices.contains(index) ? self[index] : nil
}
}
func resolveBinaryPath(_ name: String) -> URL? {
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
return nil
}
for pathComponent in path.split(separator: ":") {
let url = URL(fileURLWithPath: String(pathComponent))
.appendingPathComponent(name, isDirectory: false)
if FileManager.default.fileExists(atPath: url.path) {
return url
}
}
return nil
}
+1 -1
View File
@@ -256,7 +256,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Keyboard and mouse
configuration.keyboards = [VZUSBKeyboardConfiguration()]
configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
configuration.pointingDevices = vmConfig.platform.pointingDevices()
// Networking
let vio = VZVirtioNetworkDeviceConfiguration()