feat: Moves commands into cli-client module, adds tests, needs implemented in the executable.

This commit is contained in:
2024-12-11 14:41:06 -05:00
parent ddb5e6767a
commit c1a14ea855
8 changed files with 276 additions and 280 deletions

View File

@@ -39,21 +39,14 @@ let package = Package(
"ConfigurationClient",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client"),
.product(name: "TOMLKit", package: "TOMLKit")
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.testTarget(
name: "CliClientTests",
dependencies: [
"CliClient",
.product(name: "TOMLKit", package: "TOMLKit")
],
resources: [
.copy("Resources/config.json"),
.copy("Resources/.hparc"),
.copy("Resources/vault.yml"),
.copy("Resources/hpa-playbook")
"TestSupport"
]
),
.target(

View File

@@ -5,34 +5,25 @@ import FileClient
import Foundation
import ShellClient
public extension DependencyValues {
var cliClient: CliClient {
get { self[CliClient.self] }
set { self[CliClient.self] = newValue }
}
}
public extension CliClient {
@DependencyClient
public struct CliClient: Sendable {
public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void
public func runCommand(
func runCommand(
quiet: Bool,
shell: ShellCommand.Shell,
_ args: [String]
) async throws {
try await runCommand(args, quiet, shell)
try await runCommand(.init(arguments: args, quiet: quiet, shell: shell))
}
public func runCommand(
func runCommand(
quiet: Bool,
shell: ShellCommand.Shell,
_ args: String...
) async throws {
try await runCommand(args, quiet, shell)
try await runCommand(quiet: quiet, shell: shell, args)
}
public func runPlaybookCommand(_ options: PlaybookOptions) async throws {
func runPlaybookCommand(_ options: PlaybookOptions) async throws {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
@@ -46,7 +37,7 @@ public struct CliClient: Sendable {
let inventoryPath = ensuredInventoryPath(
options.inventoryFilePath,
configuration: configuration,
playboodDirectory: playbookDirectory
playbookDirectory: playbookDirectory
)
logger.trace("Inventory path: \(inventoryPath)")
@@ -72,7 +63,7 @@ public struct CliClient: Sendable {
)
}
public func runVaultCommand(_ options: VaultOptions) async throws {
func runVaultCommand(_ options: VaultOptions) async throws {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
@@ -98,6 +89,10 @@ public struct CliClient: Sendable {
arguments.append(contentsOf: ["--encrypt-vault-id", id])
}
arguments.append(vaultFilePath)
logger.trace("Running vault command with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.shellOrDefault,
@@ -106,89 +101,6 @@ public struct CliClient: Sendable {
}
}
public extension CliClient {
struct PlaybookOptions: Sendable, Equatable {
let arguments: [String]
let configuration: Configuration?
let inventoryFilePath: String?
let playbookDirectory: String?
let quiet: Bool
let shell: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
inventoryFilePath: String? = nil,
playbookDirectory: String? = nil,
quiet: Bool,
shell: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.inventoryFilePath = inventoryFilePath
self.playbookDirectory = playbookDirectory
self.quiet = quiet
self.shell = shell
}
}
struct VaultOptions: Equatable, Sendable {
let arguments: [String]
let configuration: Configuration?
let quiet: Bool
let shell: String?
let vaultFilePath: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
quiet: Bool,
shell: String?,
vaultFilePath: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.quiet = quiet
self.shell = shell
self.vaultFilePath = vaultFilePath
}
}
}
extension CliClient: DependencyKey {
public static func live(
env: [String: String]
) -> Self {
@Dependency(\.logger) var logger
return .init { args, quiet, shell in
@Dependency(\.asyncShellClient) var shellClient
if !quiet {
try await shellClient.foreground(.init(
shell: shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
args
))
} else {
try await shellClient.background(.init(
shell: shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
args
))
}
}
}
public static var liveValue: CliClient {
.live(env: ProcessInfo.processInfo.environment)
}
public static let testValue: CliClient = Self()
}
private extension ConfigurationClient {
func ensuredConfiguration(_ optionalConfig: Configuration?) async throws -> Configuration {
guard let config = optionalConfig else {
@@ -222,11 +134,11 @@ private extension Optional where Wrapped == String {
private func ensuredInventoryPath(
_ optionalInventoryPath: String?,
configuration: Configuration,
playboodDirectory: String
playbookDirectory: String
) -> String {
guard let path = optionalInventoryPath else {
guard let path = configuration.playbook?.inventory else {
return "\(playboodDirectory)/\(Constants.inventoryFileName)"
return "\(playbookDirectory)/\(Constants.inventoryFileName)"
}
return path
}
@@ -250,3 +162,5 @@ enum CliClientError: Error {
case playbookDirectoryNotFound
case vaultFileNotFound
}
extension ShellCommand.Shell: @retroactive @unchecked Sendable {}

View File

@@ -0,0 +1,139 @@
import ConfigurationClient
import Dependencies
import DependenciesMacros
import Foundation
import ShellClient
public extension DependencyValues {
var cliClient: CliClient {
get { self[CliClient.self] }
set { self[CliClient.self] = newValue }
}
}
@DependencyClient
public struct CliClient: Sendable {
public var runCommand: @Sendable (RunCommandOptions) async throws -> Void
}
public extension CliClient {
struct PlaybookOptions: Sendable, Equatable {
let arguments: [String]
let configuration: Configuration?
let inventoryFilePath: String?
let playbookDirectory: String?
let quiet: Bool
let shell: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
inventoryFilePath: String? = nil,
playbookDirectory: String? = nil,
quiet: Bool,
shell: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.inventoryFilePath = inventoryFilePath
self.playbookDirectory = playbookDirectory
self.quiet = quiet
self.shell = shell
}
}
struct RunCommandOptions: Sendable, Equatable {
public let arguments: [String]
public let quiet: Bool
public let shell: ShellCommand.Shell
public init(
arguments: [String],
quiet: Bool,
shell: ShellCommand.Shell
) {
self.arguments = arguments
self.quiet = quiet
self.shell = shell
}
}
struct VaultOptions: Equatable, Sendable {
let arguments: [String]
let configuration: Configuration?
let quiet: Bool
let shell: String?
let vaultFilePath: String?
public init(
arguments: [String],
configuration: Configuration? = nil,
quiet: Bool,
shell: String?,
vaultFilePath: String? = nil
) {
self.arguments = arguments
self.configuration = configuration
self.quiet = quiet
self.shell = shell
self.vaultFilePath = vaultFilePath
}
}
}
extension CliClient: DependencyKey {
public static func live(
env: [String: String]
) -> Self {
@Dependency(\.logger) var logger
return .init { options in
@Dependency(\.asyncShellClient) var shellClient
if !options.quiet {
try await shellClient.foreground(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
options.arguments
))
} else {
try await shellClient.background(.init(
shell: options.shell,
environment: ProcessInfo.processInfo.environment,
in: nil,
options.arguments
))
}
}
}
public static var liveValue: CliClient {
.live(env: ProcessInfo.processInfo.environment)
}
public static let testValue: CliClient = Self()
public static func capturing(_ client: CapturingClient) -> Self {
.init { options in
await client.set(options)
}
}
public actor CapturingClient: Sendable {
public private(set) var quiet: Bool?
public private(set) var shell: ShellCommand.Shell?
public private(set) var arguments: [String]?
public init() {}
public func set(
_ options: RunCommandOptions
) {
quiet = options.quiet
shell = options.shell
arguments = options.arguments
}
}
}

View File

@@ -1,124 +1,121 @@
// @_spi(Internal) import CliClient
// import Dependencies
// import Foundation
// import ShellClient
// import Testing
// import TOMLKit
//
// @Suite("CliClientTests")
// struct CliClientTests {
//
// @Test
// func testLiveFileClient() {
// withTestLogger(key: "testFindConfigPaths", logLevel: .trace) {
// $0.fileClient = .liveValue
// } operation: {
// @Dependency(\.fileClient) var fileClient
// let homeDir = fileClient.homeDir()
// #expect(fileClient.isDirectory(homeDir))
// #expect(fileClient.isReadable(homeDir))
// }
// }
//
// @Test
// func testFindConfigPaths() throws {
// withTestLogger(key: "testFindConfigPaths", logLevel: .trace) {
// $0.fileClient = .liveValue
// } operation: {
// @Dependency(\.logger) var logger
// let configURL = Bundle.module.url(forResource: "config", withExtension: "json")!
// var env = [
// "HPA_CONFIG_FILE": path(for: configURL)
// ]
// var url = try? findConfigurationFiles(env: env)
// #expect(url != nil)
//
// env["HPA_CONFIG_FILE"] = nil
// env["HPA_CONFIG_HOME"] = path(for: configURL.deletingLastPathComponent())
// url = try? findConfigurationFiles(env: env)
// #expect(url != nil)
//
// env["HPA_CONFIG_HOME"] = nil
// env["PWD"] = path(for: configURL.deletingLastPathComponent())
// url = try? findConfigurationFiles(env: env)
// #expect(url != nil)
//
// env["PWD"] = nil
// env["XDG_CONFIG_HOME"] = path(for: configURL.deletingLastPathComponent())
// url = try? findConfigurationFiles(env: env)
// #expect(url != nil)
//
// withDependencies {
// $0.fileClient.homeDir = { configURL.deletingLastPathComponent() }
// } operation: {
// url = try? findConfigurationFiles(env: [:])
// #expect(url != nil)
// }
//
// url = try? findConfigurationFiles(env: [:])
// #expect(url == nil)
// }
// }
//
// // @Test
// // func loadConfiguration() throws {
// // let configURL = Bundle.module.url(forResource: "config", withExtension: "json")!
// // let configData = try Data(contentsOf: configURL)
// // let decodedConfig = try JSONDecoder().decode(Configuration.self, from: configData)
// //
// // try withTestLogger(key: "loadConfiguration", logLevel: .debug) {
// // $0.fileClient = .liveValue
// // } operation: {
// // @Dependency(\.logger) var logger
// // let client = CliClient.live(env: ["HPA_CONFIG_FILE": path(for: configURL)])
// // let config = try client.loadConfiguration()
// // #expect(config == decodedConfig)
// // }
// // }
//
// @Test(arguments: ["config", "config.json"])
// func createConfiguration(filePath: String) throws {
// try withTestLogger(key: "createConfiguration", logLevel: .trace) {
// $0.fileClient = .liveValue
// } operation: {
// let client = CliClient.liveValue
// let tempDir = FileManager.default.temporaryDirectory
//
// let tempPath = path(for: tempDir.appending(path: filePath))
//
// try client.createConfiguration(path: tempPath, json: filePath.contains(".json"))
//
// #expect(FileManager.default.fileExists(atPath: tempPath))
//
// do {
// try client.createConfiguration(path: tempPath, json: true)
// #expect(Bool(false))
// } catch {
// #expect(Bool(true))
// }
//
// try FileManager.default.removeItem(atPath: tempPath)
// }
// }
//
// @Test
// func findVaultFile() throws {
// try withTestLogger(key: "findVaultFile", logLevel: .trace) {
// $0.fileClient = .liveValue
// } operation: {
// @Dependency(\.fileClient) var fileClient
// let vaultUrl = Bundle.module.url(forResource: "vault", withExtension: "yml")!
// let vaultDir = vaultUrl.deletingLastPathComponent()
// let url = try fileClient.findVaultFile(path(for: vaultDir))
// #expect(url == vaultUrl)
// }
// }
//
// // @Test
// // func writeToml() throws {
// // let encoded: String = try TOMLEncoder().encode(Configuration.mock)
// // try encoded.write(to: URL(filePath: "hpa.toml"), atomically: true, encoding: .utf8)
// // }
// }
import CliClient
import ConfigurationClient
import Dependencies
import Foundation
import ShellClient
import Testing
import TestSupport
@Suite("CliClientTests")
struct CliClientTests: TestCase {
@Test
func capturingClient() async throws {
let captured = CliClient.CapturingClient()
let client = CliClient.capturing(captured)
try await client.runCommand(quiet: false, shell: .zsh(), "foo", "bar")
let quiet = await captured.quiet!
#expect(quiet == false)
let shell = await captured.shell
#expect(shell == .zsh())
let arguments = await captured.arguments!
#expect(arguments == ["foo", "bar"])
}
@Test(arguments: ["encrypt", "decrypt"])
func runVault(argument: String) async throws {
let captured = CliClient.CapturingClient()
try await withMockConfiguration(captured, key: "runVault") {
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "vault.yml") }
} operation: {
@Dependency(\.cliClient) var cliClient
let configuration = Configuration.mock
try await cliClient.runVaultCommand(.init(arguments: [argument], quiet: false, shell: nil))
let shell = await captured.shell
#expect(shell == .zsh(useDashC: true))
let vaultPath = URL(filePath: #file)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appending(path: "vault.yml")
var encryptArgs: [String] = []
if argument == "encrypt", let id = configuration.vault.encryptId {
encryptArgs = ["--encrypt-vault-id", id]
}
let expectedArguments = [
"ansible-vault", argument
] + configuration.vault.args!
+ encryptArgs
+ [vaultPath.cleanFilePath]
let arguments = await captured.arguments
#expect(arguments == expectedArguments)
}
}
@Test(arguments: [
Configuration(
args: ["--tags", "debug"],
useVaultArgs: true,
playbook: .init(directory: "playbook", inventory: nil),
vault: .mock
)
])
func runPlaybook(configuration: Configuration) async throws {
let captured = CliClient.CapturingClient()
try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") {
@Dependency(\.cliClient) var cliClient
try await cliClient.runPlaybookCommand(.init(
arguments: [],
quiet: false,
shell: nil
))
let expectedArguments = [
"ansible-playbook", "playbook/main.yml",
"--inventory", "playbook/inventory.ini",
"--tags", "debug",
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
]
let arguments = await captured.arguments
#expect(arguments == expectedArguments)
}
}
func withMockConfiguration(
_ capturing: CliClient.CapturingClient,
configuration: Configuration = .mock,
key: String,
logLevel: Logger.Level = .trace,
depednencies setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
operation: @Sendable @escaping () async throws -> Void
) async rethrows {
try await withTestLogger(key: key, logLevel: logLevel) {
$0.configurationClient = .mock(configuration)
$0.cliClient = .capturing(capturing)
setupDependencies(&$0)
} operation: {
try await operation()
}
}
}
extension ConfigurationClient {
static func mock(_ configuration: Configuration) -> Self {
var mock = Self.testValue
mock.find = { throw TestError() }
mock.load = { _ in configuration }
return mock
}
}
struct TestError: Error {}

View File

@@ -1,15 +0,0 @@
{
"HPA_TEMPLATE_VERSION" : "main",
"HPA_TEMPLATE_DIR" : "/path/to/local/template",
"HPA_PLAYBOOK_DIR" : "/path/to/playbook",
"HPA_DEFAULT_VAULT_ARGS" : [
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
],
"HPA_TEMPLATE_REPO" : "https://git.example.com/consult-template.git",
"HPA_DEFAULT_PLAYBOOK_ARGS" : [
"--tags",
"debug"
],
"HPA_DEFAULT_VAULT_ENCRYPT_ID" : "myId",
"HPA_DEFAULT_INVENTORY" : "/path/to/inventory.ini"
}

View File

@@ -1,15 +0,0 @@
{
"HPA_TEMPLATE_VERSION" : "main",
"HPA_TEMPLATE_DIR" : "/path/to/local/template",
"HPA_PLAYBOOK_DIR" : "/path/to/playbook",
"HPA_DEFAULT_VAULT_ARGS" : [
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
],
"HPA_TEMPLATE_REPO" : "https://git.example.com/consult-template.git",
"HPA_DEFAULT_PLAYBOOK_ARGS" : [
"--tags",
"debug"
],
"HPA_DEFAULT_VAULT_ENCRYPT_ID" : "myId",
"HPA_DEFAULT_INVENTORY" : "/path/to/inventory.ini"
}

View File

@@ -1,15 +0,0 @@
{
"HPA_TEMPLATE_VERSION" : "main",
"HPA_TEMPLATE_DIR" : "/path/to/local/template",
"HPA_PLAYBOOK_DIR" : "/path/to/playbook",
"HPA_DEFAULT_VAULT_ARGS" : [
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
],
"HPA_TEMPLATE_REPO" : "https://git.example.com/consult-template.git",
"HPA_DEFAULT_PLAYBOOK_ARGS" : [
"--tags",
"debug"
],
"HPA_DEFAULT_VAULT_ENCRYPT_ID" : "myId",
"HPA_DEFAULT_INVENTORY" : "/path/to/inventory.ini"
}

View File

@@ -1,2 +0,0 @@
---
# this is just here for testing, doesn't need to be encoded.