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 1 commit
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
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"])
]
)
13 changes: 1 addition & 12 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,7 @@ struct Clone: AsyncParsableCommand {

func run() async throws {
do {
let vmStorage = VMStorage()
let sourceVMDir = try vmStorage.read(sourceName)
let newVMDir = try vmStorage.create(newName)

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().open(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)
}
}
}
25 changes: 25 additions & 0 deletions Sources/tart/Commands/Pull.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)

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

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

Foundation.exit(1)
}
}
}
31 changes: 31 additions & 0 deletions Sources/tart/Commands/Push.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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")
var remoteName: String
fkorotkov marked this conversation as resolved.
Show resolved Hide resolved

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

let remoteName = try RemoteName(remoteName)
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace)

try await localVMDir.pushToRegistry(registry: registry, reference: remoteName.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)
}
69 changes: 69 additions & 0 deletions Sources/tart/Credentials.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Foundation

class Credentials {
static func retrieve(host: String) throws -> (String, String) {
do {
return try retrieveKeychain(host: host)
} catch {
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,
]

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

if status != errSecSuccess {
if status == errSecItemNotFound {
throw RegistryError.AuthFailed
}

throw RegistryError.AuthFailed
}

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
}

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",
]

switch SecItemAdd(attributes as CFDictionary, nil) {
case errSecSuccess, errSecDuplicateItem:
return
default:
throw RegistryError.AuthFailed
}
}
}
27 changes: 27 additions & 0 deletions Sources/tart/OCI/Digest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
import CryptoKit

class Digest {
var hash: SHA256 = SHA256()

func update(_ data: Data) {
hash.update(data: data)
}

func finalize() -> String {
hash.finalize().hexdigest()
}

static func hash(_ data: Data) -> String {
SHA256.hash(data: data).hexdigest()
}
}

extension SHA256.Digest {
func hexdigest() -> String {
"sha256:" + self.map {
String(format: "%02x", $0)
}
.joined()
}
}
28 changes: 28 additions & 0 deletions Sources/tart/OCI/Manifest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

let ociManifestMediaType = "application/vnd.oci.image.manifest.v1+json"
let ociConfigMediaType = "application/vnd.oci.image.config.v1+json"

struct OCIManifest: Encodable, Decodable {
var schemaVersion: Int = 2
var mediaType: String = ociManifestMediaType
var config: OCIManifestConfig
var layers: [OCIManifestLayer] = Array()
}

struct OCIManifestConfig: Encodable, Decodable {
var mediaType: String = ociConfigMediaType
var size: Int
var digest: String
}

struct OCIManifestLayer: Encodable, Decodable {
var mediaType: String
var size: Int
var digest: String
}

struct Descriptor {
var size: Int
var digest: String
}
Loading