Skip to content

Commit d93260d

Browse files
authored
Merge pull request #40 from Automattic/isolate/s3
Reduce SotoS3 dependency
2 parents 9827ec1 + 0cc996a commit d93260d

23 files changed

+482
-413
lines changed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ let package = Package(
1717
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
1818
.package(url: "https://github.com/jkmassel/kcpassword-swift.git", from: "1.0.0"),
1919
.package(url: "https://github.com/swiftpackages/DotEnv.git", from: "3.0.0"),
20-
.package(url: "https://github.com/apple/swift-tools-support-core", from: "0.2.5")
21-
20+
.package(url: "https://github.com/apple/swift-tools-support-core", from: "0.2.5"),
21+
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.1"))
2222
],
2323
targets: [
2424
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -27,20 +27,20 @@ let package = Package(
2727
name: "hostmgr",
2828
dependencies: [
2929
.product(name: "ArgumentParser", package: "swift-argument-parser"),
30-
.product(name: "SotoS3", package: "soto"),
3130
.product(name: "prlctl", package: "prlctl"),
3231
.product(name: "Tqdm", package: "swift-tqdm"),
3332
.product(name: "Logging", package: "swift-log"),
3433
.product(name: "kcpassword", package: "kcpassword-swift"),
35-
.target(name: "libhostmgr")
34+
.target(name: "libhostmgr"),
3635
]
3736
),
3837
.target(
3938
name: "libhostmgr",
4039
dependencies: [
4140
.product(name: "ArgumentParser", package: "swift-argument-parser"),
4241
.product(name: "SotoS3", package: "soto"),
43-
.product(name: "TSCBasic", package: "swift-tools-support-core")
42+
.product(name: "TSCBasic", package: "swift-tools-support-core"),
43+
.product(name: "Alamofire", package: "Alamofire"),
4444
]
4545
),
4646
.testTarget(

Sources/hostmgr/BenchmarkCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import ArgumentParser
33

4-
struct BenchmarkCommand: ParsableCommand {
4+
struct BenchmarkCommand: AsyncParsableCommand {
55
static let configuration = CommandConfiguration(
66
commandName: "benchmark",
77
abstract: "System tests",

Sources/hostmgr/HostMgrCommand.swift

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import ArgumentParser
2-
import SotoS3
32
import Logging
43
import libhostmgr
54

@@ -66,12 +65,10 @@ struct InitCommand: ParsableCommand {
6665
currentValue: Configuration.shared.vmImagesBucket
6766
)
6867

69-
let vmImagesRegion = prompt(
68+
Configuration.shared.vmImagesRegion = prompt(
7069
"Which AWS region contains the \(Configuration.shared.vmImagesBucket) bucket?",
71-
currentValue: Configuration.shared.vmImagesRegion.rawValue
72-
) { Region(awsRegionName: $0) != nil }
73-
74-
Configuration.shared.vmImagesRegion = Region(awsRegionName: vmImagesRegion)!
70+
currentValue: Configuration.shared.vmImagesRegion
71+
)
7572

7673
print("== Authorized Keys Sync ==\n")
7774

@@ -80,12 +77,10 @@ struct InitCommand: ParsableCommand {
8077
currentValue: Configuration.shared.authorizedKeysBucket
8178
)
8279

83-
let authorizedKeysRegion = prompt(
80+
Configuration.shared.authorizedKeysRegion = prompt(
8481
"Which AWS region contains the \(Configuration.shared.authorizedKeysBucket) bucket?",
85-
currentValue: Configuration.shared.authorizedKeysRegion.rawValue
86-
) { Region(awsRegionName: $0) != nil }
87-
88-
Configuration.shared.authorizedKeysRegion = Region(awsRegionName: authorizedKeysRegion)!
82+
currentValue: Configuration.shared.authorizedKeysRegion
83+
)
8984

9085
Configuration.shared.gitMirrorBucket = prompt(
9186
"Which S3 bucket would you like to use as the data source for the git mirror server?",

Sources/hostmgr/SyncCommand.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import ArgumentParser
33
import libhostmgr
44

5-
struct SyncCommand: ParsableCommand {
5+
struct SyncCommand: AsyncParsableCommand {
66
static let configuration = CommandConfiguration(
77
commandName: "sync",
88
abstract: "Sync remote data with this host",
@@ -18,7 +18,7 @@ struct SyncCommand: ParsableCommand {
1818
@OptionGroup
1919
var options: SharedSyncOptions
2020

21-
func run() throws {
21+
mutating func run() async throws {
2222

2323
if list {
2424
Configuration.SchedulableSyncCommand.allCases.forEach { print($0) }
@@ -29,20 +29,20 @@ struct SyncCommand: ParsableCommand {
2929
// to make it so it doesn't need to be installed separately
3030
try GenerateGitMirrorManifestTask().run()
3131

32-
try Configuration.shared.syncTasks.forEach { command in
33-
options.force ? print("Force-running \(command.rawValue)") : print("Running \(command.rawValue)")
34-
try perform(task: command, immediately: options.force)
32+
for task in Configuration.shared.syncTasks {
33+
options.force ? print("Force-running \(task.rawValue)") : print("Running \(task.rawValue)")
34+
try await perform(task: task)
3535
}
3636
}
3737

38-
private func perform(task: Configuration.SchedulableSyncCommand, immediately: Bool) throws {
38+
private func perform(task: Configuration.SchedulableSyncCommand) async throws {
3939
switch task {
4040
case .authorizedKeys:
4141
let command = SyncAuthorizedKeysCommand(options: self._options)
42-
try command.run()
42+
try await command.run()
4343
case .vmImages:
4444
let command = SyncVMImagesCommand(options: self._options)
45-
try command.run()
45+
try await command.run()
4646
}
4747
}
4848
}

Sources/hostmgr/VMCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import ArgumentParser
33
import prlctl
44

5-
struct VMCommand: ParsableCommand {
5+
struct VMCommand: AsyncParsableCommand {
66

77
static let configuration = CommandConfiguration(
88
commandName: "vm",

Sources/hostmgr/commands/benchmark/NetworkBenchmark.swift

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,53 @@ import libhostmgr
66

77
private let startDate = Date()
88

9-
struct NetworkBenchmark: ParsableCommand {
9+
struct NetworkBenchmark: AsyncParsableCommand {
1010

1111
static let configuration = CommandConfiguration(
1212
commandName: "network",
1313
abstract: "Test Network Speed"
1414
)
1515

16-
func run() throws {
17-
guard let file = try VMRemoteImageManager().list().sorted(by: { $0.size < $1.size }).first else {
16+
private static let limiter = Limiter(policy: .throttle, operationsPerSecond: 1)
17+
18+
func run() async throws {
19+
let remoteImages = try await RemoteVMRepository().listImages(sortedBy: .size)
20+
21+
guard let file = remoteImages.last else {
1822
throw CleanExit.message("Unable to find a remote image to use as a network benchmark")
1923
}
2024

21-
try S3Manager().streamingDownloadFile(
22-
region: Configuration.shared.vmImagesRegion,
25+
let manager = S3Manager(
2326
bucket: Configuration.shared.vmImagesBucket,
24-
key: file.imageKey,
25-
destination: URL(fileURLWithPath: "/dev/null"),
26-
progressCallback: self.showProgress
27+
region: Configuration.shared.vmImagesRegion
28+
)
29+
30+
try await manager.download(
31+
object: file.imageObject,
32+
to: FileManager.default.temporaryFilePath(),
33+
progressCallback: self.updateProgress
2734
)
2835
}
2936

30-
private func showProgress(availableBytes: Int, downloadedBytes: Int, totalBytes: Int64) {
37+
private func imageSizeSort(_ lhs: RemoteVMImage, _ rhs: RemoteVMImage) -> Bool {
38+
lhs.imageObject.size < rhs.imageObject.size
39+
}
3140

32-
// Sample only one in 100 entries
33-
guard Int.random(in: 0...1000) == 0 else {
34-
return
35-
}
41+
private func updateProgress(_ progress: FileTransferProgress) {
42+
Self.limiter.perform {
43+
let downloadedSize = ByteCountFormatter.string(fromByteCount: Int64(progress.current), countStyle: .file)
44+
let totalSize = ByteCountFormatter.string(fromByteCount: Int64(progress.total), countStyle: .file)
3645

37-
let downloadedSize = ByteCountFormatter.string(fromByteCount: Int64(downloadedBytes), countStyle: .file)
38-
let totalSize = ByteCountFormatter.string(fromByteCount: totalBytes, countStyle: .file)
46+
let secondsElapsed = Date().timeIntervalSince(startDate)
47+
let perSecond = Double(progress.current) / Double(secondsElapsed)
3948

40-
let secondsElapsed = Date().timeIntervalSince(startDate)
41-
let perSecond = Double(downloadedBytes) / Double(secondsElapsed)
42-
let rate = ByteCountFormatter.string(fromByteCount: Int64(perSecond), countStyle: .file)
49+
// Don't continue unless the rate can be represented by `Int64`
50+
guard perSecond.isNormal else {
51+
return
52+
}
4353

44-
logger.info("Downloaded \(downloadedSize) of \(totalSize) [Rate: \(rate) per second]")
54+
let rate = ByteCountFormatter.string(fromByteCount: Int64(perSecond), countStyle: .file)
55+
logger.info("Downloaded \(downloadedSize) of \(totalSize) [Rate: \(rate) per second]")
56+
}
4557
}
4658
}

Sources/hostmgr/commands/set/SetAutomaticLoginPasswordCommand.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Foundation
22
import ArgumentParser
3-
import SotoS3
43
import kcpassword
54

65
struct SetAutomaticLoginPasswordCommand: ParsableCommand {

Sources/hostmgr/commands/sync/SyncAuthorizedKeysCommand.swift

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import Foundation
22
import ArgumentParser
3-
import SotoS3
43
import libhostmgr
54

6-
struct SyncAuthorizedKeysCommand: ParsableCommand, FollowsCommandPolicies {
5+
struct SyncAuthorizedKeysCommand: AsyncParsableCommand, FollowsCommandPolicies {
76

87
static let configuration = CommandConfiguration(
9-
commandName: "authorized_keys",
8+
commandName: Configuration.SchedulableSyncCommand.authorizedKeys.rawValue,
109
abstract: "Set this machine's authorized_keys file"
1110
)
1211

@@ -20,7 +19,7 @@ struct SyncAuthorizedKeysCommand: ParsableCommand, FollowsCommandPolicies {
2019
name: .shortAndLong,
2120
help: "The S3 region for the bucket"
2221
)
23-
var region: Region = Configuration.shared.authorizedKeysRegion
22+
var region: String = Configuration.shared.authorizedKeysRegion
2423

2524
@Option(
2625
name: .shortAndLong,
@@ -44,30 +43,26 @@ struct SyncAuthorizedKeysCommand: ParsableCommand, FollowsCommandPolicies {
4443
.scheduled(every: 3600)
4544
]
4645

47-
func run() throws {
46+
func run() async throws {
4847
try to(evaluateCommandPolicies(), unless: options.force)
48+
logger.debug("Job schedule allows for running")
4949

50-
logger.debug("Downloading file from s3://\(bucket)/\(key) in \(region) to \(destination)")
51-
logger.trace("Job schedule allows for running")
50+
logger.info("Downloading file from s3://\(bucket)/\(key) in \(region) to \(destination)")
5251

53-
guard let bytes = try S3Manager().getFileBytes(region: region, bucket: bucket, key: key) else {
54-
print("Unable to sync authorized_keys file – exiting")
55-
SyncAuthorizedKeysCommand.exit()
56-
}
57-
58-
logger.trace("Downloaded \(bytes.count) bytes from S3")
52+
let s3Manager = S3Manager(bucket: self.bucket, region: self.region)
5953

60-
/// Create the parent directory if needed
61-
let parent = URL(fileURLWithPath: Configuration.shared.localAuthorizedKeys).deletingLastPathComponent()
62-
try FileManager.default.createDirectoryTree(atUrl: parent)
54+
guard let object = try await s3Manager.lookupObject(atPath: key) else {
55+
logger.error("Unable to locate authorized_keys file – exiting")
56+
throw ExitCode(rawValue: 1)
57+
}
6358

64-
/// Overwrite the existing file
65-
try bytes.write(to: URL(fileURLWithPath: destination))
59+
let url = URL(fileURLWithPath: self.destination)
60+
try await s3Manager.download(object: object, to: url, progressCallback: nil)
6661

6762
/// Fix the permissions on the file, if needed
6863
try FileManager.default.setAttributes([
6964
.posixPermissions: 0o600
70-
], ofItemAtPath: destination)
65+
], ofItemAtPath: self.destination)
7166

7267
try recordLastRun()
7368
}

Sources/hostmgr/commands/sync/SyncVMImagesCommand.swift

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import Foundation
22
import ArgumentParser
3-
import SotoS3
43
import prlctl
54
import libhostmgr
65

7-
struct SyncVMImagesCommand: ParsableCommand, FollowsCommandPolicies {
6+
struct SyncVMImagesCommand: AsyncParsableCommand, FollowsCommandPolicies {
87

98
static let configuration = CommandConfiguration(
10-
commandName: "vm_images",
11-
abstract: "Sync this machine's VM images with those avaiable remotely"
9+
commandName: Configuration.SchedulableSyncCommand.vmImages.rawValue,
10+
abstract: "Sync this machine's VM images with those available remotely"
1211
)
1312

1413
@OptionGroup
@@ -22,19 +21,19 @@ struct SyncVMImagesCommand: ParsableCommand, FollowsCommandPolicies {
2221
.scheduled(every: 3600)
2322
]
2423

25-
func run() throws {
24+
func run() async throws {
2625
try to(evaluateCommandPolicies(), unless: options.force)
2726

2827
/// The manifest defines which images should be distributed to VM hosts
29-
let manifest = try VMRemoteImageManager().getManifest()
30-
logger.debug("Downloaded manifest:\n\(manifest)")
28+
let manifest = try await RemoteVMRepository().getManifest()
29+
logger.info("Downloaded manifest:\n\(manifest)")
3130

3231
/// Candidate images are any that the manifest says *could* be installed on this VM host
33-
let candidateImages = try VMRemoteImageManager().list().filter { manifest.contains($0.basename) }
34-
logger.debug("Available remote images:\n\(candidateImages)")
32+
let candidateImages = try await RemoteVMRepository().listImages().filter { manifest.contains($0.basename) }
33+
logger.info("Available remote images:\n\(candidateImages)")
3534

3635
let localImages = try VMLocalImageManager().list()
37-
logger.debug("Local Images:\(localImages)")
36+
logger.info("Local Images:\(localImages)")
3837

3938
let imagesToDownload = candidateImages.filter { !localImages.contains($0.basename) }
4039
let imagesToDelete = localImages
@@ -46,27 +45,25 @@ struct SyncVMImagesCommand: ParsableCommand, FollowsCommandPolicies {
4645
logger.info("Deleting local images:\(imagesToDelete)")
4746
try VMLocalImageManager().delete(images: imagesToDelete)
4847

49-
try imagesToDownload.forEach {
50-
try download(image: $0)
48+
for image in imagesToDownload {
49+
try await download(image: image)
5150
}
5251

5352
try recordLastRun()
5453
}
5554

56-
private func download(image: VMRemoteImageManager.RemoteImage) throws {
55+
private func download(image: RemoteVMImage) async throws {
5756
let storageDirectory = Configuration.shared.vmStorageDirectory
5857
let destination = storageDirectory.appendingPathComponent(image.fileName)
5958

60-
logger.info("Downloading the VM – this will take a few minutes")
61-
logger.trace("Downloading \(image.basename) to \(destination)")
59+
logger.info("Downloading \(image.basename) to \(destination) – this will take a few minutes")
6260

6361
let limiter = Limiter(policy: .throttle, operationsPerSecond: 1)
6462

65-
try VMRemoteImageManager().download(image: image, to: destination) { _, downloaded, total in
63+
try await RemoteVMRepository().download(image: image, to: destination) { progress in
6664
limiter.perform {
6765
try? recordHeartbeat()
68-
let percent = String(format: "%.2f", Double(downloaded) / Double(total) * 100)
69-
logger.trace("\(percent)% complete")
66+
logger.info("\(progress.decimalPercent)% complete")
7067
}
7168
}
7269

@@ -87,6 +84,5 @@ struct SyncVMImagesCommand: ParsableCommand, FollowsCommandPolicies {
8784
logger.info("Imported Complete")
8885
logger.info("\tName:\t\(vmToImport.name)")
8986
logger.info("\tUUID:\t\(vmToImport.uuid)")
90-
9187
}
9288
}

0 commit comments

Comments
 (0)