Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pulling/pushing VMs to OCI-compatible registries #32

Merged
merged 21 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b2408e9
Support pulling/pushing VMs to OCI-compatible registries
edigaryev Apr 29, 2022
d421e88
Registry: rename request() to endpointRequest() for clarity
edigaryev May 3, 2022
1b466b0
Registry: include JSON details on HTTP status code mismatch errors
edigaryev May 3, 2022
9c0fb93
.cirrus.yml: run tests
edigaryev May 3, 2022
68ecfd6
Fix testDigest
edigaryev May 3, 2022
0144913
Registry: set Content-{Length,Type} headers when pushing blob
edigaryev May 3, 2022
e586780
Refactor Registry.auth() and enrich RegistryError.AuthFailed
edigaryev May 3, 2022
0f31aba
Remove useless comment
edigaryev May 3, 2022
80645ff
Fix WWWAuthenticate a bit and add tests
edigaryev May 3, 2022
22e01bd
WWWAuthenticate: expect a Bearer scheme
edigaryev May 3, 2022
cbf75b4
Registry.auth(): document the passing of ["scope", "service"] parameters
edigaryev May 3, 2022
8f2a51d
Clarify unexpected HTTP code error when retrieving auth token
edigaryev May 3, 2022
20fffbb
Make RemoteName parser more relaxed for now
edigaryev May 3, 2022
a06b188
tart clone: pull the VM if it's OCI-based first
edigaryev May 3, 2022
80c87ca
VMStorageOCI: ensure the old symbolic link is overwritten
edigaryev May 3, 2022
d4d10ad
tart push: support multiple remote VM names
edigaryev May 3, 2022
27bb3c5
Logging: push/pull progress
edigaryev May 3, 2022
adf80e2
Use 500 MB chunks (instead of 500 MiB) to evenly cut disk
edigaryev May 3, 2022
b6fd0ed
Credentials: only read credentials labeled "Tart Credentials"
edigaryev May 3, 2022
821a99b
Call proper pushToRegistry() method to upload multiple manifests
edigaryev May 3, 2022
b7b4ad6
tart create: we should call create() instead open()
edigaryev May 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions .cirrus.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
persistent_worker:
labels:
os: darwin
arch: arm64

task:
name: Test
test_script: swift test

task:
name: Build
only_if: $CIRRUS_TAG == ''
persistent_worker:
labels:
os: darwin
arch: arm64
build_script: swift build --product tart
sign_script: codesign --sign - --entitlements Resources/tart.entitlements --force .build/debug/tart
binary_artifacts:
Expand All @@ -13,10 +18,6 @@ task:
task:
name: Release
only_if: $CIRRUS_TAG != ''
persistent_worker:
labels:
os: darwin
arch: arm64
env:
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
Expand Down
27 changes: 27 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@
"revision" : "f3c9084a71ef4376f2fabbdf1d3d90a49f1fabdb",
"version" : "1.1.2"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "ce9c0d897db8a840c39de64caaa9b60119cf4be8",
"version" : "0.8.1"
}
},
{
"identity" : "swift-parsing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "28d32e9ace1c4c43f5e5a177be837a202494c2d5",
"version" : "0.9.2"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version" : "0.2.1"
}
}
],
"version" : 2
Expand Down
10 changes: 6 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"),
.package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"),
],
targets: [
.executableTarget(name: "tart",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
.executableTarget(name: "tart", dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Parsing", package: "swift-parsing"),
]),
.testTarget(name: "TartTests", dependencies: ["tart"])
]
)
17 changes: 6 additions & 11 deletions Sources/tart/Commands/Clone.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import ArgumentParser
import Foundation
import SystemConfiguration
import Virtualization

struct Clone: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Clone a VM")
Expand All @@ -14,17 +13,13 @@ struct Clone: AsyncParsableCommand {

func run() async throws {
do {
let vmStorage = VMStorage()
let sourceVMDir = try vmStorage.read(sourceName)
let newVMDir = try vmStorage.create(newName)
// Pull the VM in case it's OCI-based and doesn't exist locally yet
if let remoteName = try? RemoteName(sourceName), !VMStorageOCI().exists(remoteName) {
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)
try await VMStorageOCI().pull(remoteName, registry: registry)
}

try FileManager.default.copyItem(at: sourceVMDir.configURL, to: newVMDir.configURL)
try FileManager.default.copyItem(at: sourceVMDir.nvramURL, to: newVMDir.nvramURL)
try FileManager.default.copyItem(at: sourceVMDir.diskURL, to: newVMDir.diskURL)

var newVMConfig = try VMConfig(fromURL: newVMDir.configURL)
newVMConfig.macAddress = VZMACAddress.randomLocallyAdministered()
try newVMConfig.save(toURL: newVMDir.configURL)
try VMStorageHelper.open(sourceName).clone(to: VMStorageLocal().create(newName))
fkorotkov marked this conversation as resolved.
Show resolved Hide resolved

Foundation.exit(0)
} catch {
Expand Down
2 changes: 1 addition & 1 deletion Sources/tart/Commands/Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct Create: AsyncParsableCommand {

func run() async throws {
do {
let vmDir = try VMStorage().create(name)
let vmDir = try VMStorageLocal().create(name)

if fromIPSW! == "latest" {
_ = try await VM(vmDir: vmDir, ipswURL: nil, diskSizeGB: diskSize)
Expand Down
2 changes: 1 addition & 1 deletion Sources/tart/Commands/Delete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct Delete: AsyncParsableCommand {

func run() async throws {
do {
try VMStorage().delete(name)
try VMStorageHelper.delete(name)

Foundation.exit(0)
} catch {
Expand Down
2 changes: 1 addition & 1 deletion Sources/tart/Commands/IP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct IP: AsyncParsableCommand {

func run() async throws {
do {
let vmDir = try VMStorage().read(name)
let vmDir = try VMStorageLocal().open(name)
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)

guard let ip = try await resolveIP(vmConfig, secondsToWait: wait) else {
Expand Down
13 changes: 10 additions & 3 deletions Sources/tart/Commands/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ struct List: AsyncParsableCommand {

func run() async throws {
do {
for vmURL in try VMStorage().list() {
print(vmURL)
}
print("Name\tSource")

displayTable("local", try VMStorageLocal().list())
displayTable("oci", try VMStorageOCI().list())

Foundation.exit(0)
} catch {
Expand All @@ -18,4 +19,10 @@ struct List: AsyncParsableCommand {
Foundation.exit(1)
}
}

private func displayTable(_ source: String, _ vms: [(String, VMDirectory)]) {
for (name, _) in vms {
print("\(source)\t\(name)")
}
}
}
24 changes: 24 additions & 0 deletions Sources/tart/Commands/Login.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import ArgumentParser
import Dispatch
import SwiftUI

struct Login: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Login to a registry")

@Argument(help: "host")
var host: String

func run() async throws {
do {
let (user, password) = try Credentials.retrieveStdin()

try Credentials.store(host: host, user: user, password: password)

Foundation.exit(0)
} catch {
print(error)

Foundation.exit(1)
}
}
}
27 changes: 27 additions & 0 deletions Sources/tart/Commands/Pull.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ArgumentParser
import Dispatch
import SwiftUI

struct Pull: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Pull a VM from a registry")

@Argument(help: "remote VM name")
var remoteName: String

func run() async throws {
do {
let remoteName = try RemoteName(remoteName)
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)

defaultLogger.appendNewLine("pulling \(remoteName)...")

try await VMStorageOCI().pull(remoteName, registry: registry)

Foundation.exit(0)
} catch {
print(error)

Foundation.exit(1)
}
}
}
53 changes: 53 additions & 0 deletions Sources/tart/Commands/Push.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import ArgumentParser
import Dispatch
import Foundation
import Compression

struct Push: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Push a VM to a registry")

@Argument(help: "local VM name")
var localName: String

@Argument(help: "remote VM name(s)")
var remoteNames: [String]

func run() async throws {
do {
let localVMDir = try VMStorageLocal().open(localName)

// Parse remote names supplied by the user
let remoteNames = try remoteNames.map{
try RemoteName($0)
}

// Group remote names by registry
struct RegistryIdentifier: Hashable, Equatable {
var host: String
var namespace: String
}

let registryGroups = Dictionary(grouping: remoteNames, by: {
RegistryIdentifier(host: $0.host, namespace: $0.namespace)
})

// Push VM
for (registryIdentifier, remoteNamesForRegistry) in registryGroups {
let registry = try Registry(host: registryIdentifier.host, namespace: registryIdentifier.namespace)

let listOfTagsAndDigests = "{" + remoteNamesForRegistry.map{$0.fullyQualifiedReference }
.joined(separator: ",") + "}"
defaultLogger.appendNewLine("pushing \(localName) to "
+ "\(registryIdentifier.host)/\(registryIdentifier.namespace)\(listOfTagsAndDigests)...")

try await localVMDir.pushToRegistry(registry: registry, references: remoteNamesForRegistry.map{ $0.reference })
}

Foundation.exit(0)
} catch {
print(error)

Foundation.exit(1)
}
}
}
4 changes: 2 additions & 2 deletions Sources/tart/Commands/Run.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ struct Run: AsyncParsableCommand {
var name: String

@Flag var noGraphics: Bool = false

@MainActor
func run() async throws {
let vmDir = try VMStorage().read(name)
let vmDir = try VMStorageLocal().open(name)
vm = try VM(vmDir: vmDir)

Task {
Expand Down
5 changes: 2 additions & 3 deletions Sources/tart/Commands/Set.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ struct Set: AsyncParsableCommand {

func run() async throws {
do {
let vmStorage = VMStorage()
let vmDir = try vmStorage.read(name)
let vmDir = try VMStorageLocal().open(name)
var vmConfig = try VMConfig(fromURL: vmDir.configURL)

if let cpu = cpu {
Expand All @@ -46,7 +45,7 @@ struct Set: AsyncParsableCommand {
}

try vmConfig.save(toURL: vmDir.configURL)

if diskSize != nil {
try vmDir.resizeDisk(diskSize!)
}
Expand Down
9 changes: 9 additions & 0 deletions Sources/tart/Config.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

struct Config {
public static let tartHomeDir: URL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)

public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
}
72 changes: 72 additions & 0 deletions Sources/tart/Credentials.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Foundation

class Credentials {
static func retrieve(host: String) throws -> (String, String) {
do {
return try retrieveKeychain(host: host)
} catch RegistryError.AuthFailed {
return try retrieveStdin()
}
}

static func retrieveKeychain(host: String) throws -> (String, String) {
let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
kSecAttrServer as String: host,
fkorotkov marked this conversation as resolved.
Show resolved Hide resolved
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecReturnData as String: true,
kSecAttrLabel as String: "Tart Credentials",
]

var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)

if status != errSecSuccess {
if status == errSecItemNotFound {
throw RegistryError.AuthFailed(why: "Keychain item not found")
}

throw RegistryError.AuthFailed(why: "Keychain returned unsuccessful status \(status)")
}

guard let item = item as? [String: Any],
let user = item[kSecAttrAccount as String] as? String,
let passwordData = item[kSecValueData as String] as? Data,
let password = String(data: passwordData, encoding: .utf8)
else {
throw RegistryError.AuthFailed(why: "Keychain item has unexpected format")
}

return (user, password)
}

static func retrieveStdin() throws -> (String, String) {
print("User: ", terminator: "")
let user = readLine() ?? ""

let rawPass = getpass("Password: ")
let pass = String(cString: rawPass!, encoding: .utf8)!

return (user, pass)
}

static func store(host: String, user: String, password: String) throws {
let attributes: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: user,
kSecAttrProtocol as String: kSecAttrProtocolHTTPS,
kSecAttrServer as String: host,
kSecValueData as String: password,
kSecAttrLabel as String: "Tart Credentials",
]

let status = SecItemAdd(attributes as CFDictionary, nil)

switch status {
case errSecSuccess, errSecDuplicateItem:
return
default:
throw RegistryError.AuthFailed(why: "Keychain returned unsuccessful status \(status)")
}
}
}
Loading