From ddb5e6767af5d4a63c814c66ac786cea635be4c2 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Wed, 11 Dec 2024 12:31:41 -0500 Subject: [PATCH] feat: Begins moving run commands into cli-client module --- Package.swift | 1 + Sources/CliClient/CliClient.swift | 191 ++++++++++++++++++++++++++++-- Sources/CliClient/Constants.swift | 17 +-- Sources/CodersClient/Coders.swift | 8 +- 4 files changed, 196 insertions(+), 21 deletions(-) diff --git a/Package.swift b/Package.swift index d378ec5..ed54705 100644 --- a/Package.swift +++ b/Package.swift @@ -36,6 +36,7 @@ let package = Package( name: "CliClient", dependencies: [ "CodersClient", + "ConfigurationClient", .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "ShellClient", package: "swift-shell-client"), diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index cb41972..d6cdf60 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -1,5 +1,7 @@ +import ConfigurationClient import Dependencies import DependenciesMacros +import FileClient import Foundation import ShellClient @@ -29,6 +31,128 @@ public struct CliClient: Sendable { ) async throws { try await runCommand(args, quiet, shell) } + + public func runPlaybookCommand(_ options: PlaybookOptions) async throws { + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.logger) var logger + + let configuration = try await configurationClient.ensuredConfiguration(options.configuration) + logger.trace("Configuration: \(configuration)") + + let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory) + let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" + logger.trace("Playbook path: \(playbookPath)") + + let inventoryPath = ensuredInventoryPath( + options.inventoryFilePath, + configuration: configuration, + playboodDirectory: playbookDirectory + ) + logger.trace("Inventory path: \(inventoryPath)") + + var arguments = [ + Constants.playbookCommand, playbookPath, + "--inventory", inventoryPath + ] + options.arguments + + if let defaultArgs = configuration.args { + arguments.append(contentsOf: defaultArgs) + } + + if configuration.useVaultArgs, let vaultArgs = configuration.vault.args { + arguments.append(contentsOf: vaultArgs) + } + + logger.trace("Running playbook command with arguments: \(arguments)") + + try await runCommand( + quiet: options.quiet, + shell: options.shell.shellOrDefault, + arguments + ) + } + + public func runVaultCommand(_ options: VaultOptions) async throws { + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.fileClient) var fileClient + @Dependency(\.logger) var logger + + let configuration = try await configurationClient.ensuredConfiguration(options.configuration) + logger.trace("Configuration: \(configuration)") + + let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath) + logger.trace("Vault file: \(vaultFilePath)") + + var arguments = [ + Constants.vaultCommand + ] + options.arguments + + if let defaultArgs = configuration.vault.args { + arguments.append(contentsOf: defaultArgs) + } + + if arguments.contains("encrypt"), + !arguments.contains("--encrypt-vault-id"), + let id = configuration.vault.encryptId + { + arguments.append(contentsOf: ["--encrypt-vault-id", id]) + } + + try await runCommand( + quiet: options.quiet, + shell: options.shell.shellOrDefault, + arguments + ) + } +} + +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 { @@ -65,13 +189,64 @@ extension CliClient: DependencyKey { public static let testValue: CliClient = Self() } -enum CliClientError: Error { - case fileExistsAtPath(String) - case vaultFileNotFound +private extension ConfigurationClient { + func ensuredConfiguration(_ optionalConfig: Configuration?) async throws -> Configuration { + guard let config = optionalConfig else { + return try await findAndLoad() + } + return config + } } -private let jsonEncoder: JSONEncoder = { - var encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes] - return encoder -}() +private extension Configuration { + + func ensuredPlaybookDirectory(_ optionalDirectory: String?) throws -> String { + guard let directory = optionalDirectory else { + guard let directory = playbook?.directory else { + throw CliClientError.playbookDirectoryNotFound + } + return directory + } + return directory + } +} + +private extension Optional where Wrapped == String { + + var shellOrDefault: ShellCommand.Shell { + guard let shell = self else { return .zsh(useDashC: true) } + return .custom(path: shell, useDashC: true) + } +} + +private func ensuredInventoryPath( + _ optionalInventoryPath: String?, + configuration: Configuration, + playboodDirectory: String +) -> String { + guard let path = optionalInventoryPath else { + guard let path = configuration.playbook?.inventory else { + return "\(playboodDirectory)/\(Constants.inventoryFileName)" + } + return path + } + return path +} + +private extension FileClient { + + func ensuredVaultFilePath(_ optionalPath: String?) async throws -> String { + guard let path = optionalPath else { + guard let url = try await findVaultFileInCurrentDirectory() else { + throw CliClientError.vaultFileNotFound + } + return url.cleanFilePath + } + return path + } +} + +enum CliClientError: Error { + case playbookDirectoryNotFound + case vaultFileNotFound +} diff --git a/Sources/CliClient/Constants.swift b/Sources/CliClient/Constants.swift index 65dcedd..d000029 100644 --- a/Sources/CliClient/Constants.swift +++ b/Sources/CliClient/Constants.swift @@ -1,13 +1,6 @@ -/// Holds keys associated with environment values. -public enum EnvironmentKey { - public static let xdgConfigHomeKey = "XDG_CONFIG_HOME" - public static let hpaConfigDirKey = "HPA_CONFIG_DIR" - public static let hpaConfigFileKey = "HPA_CONFIG_FILE" -} - -/// Holds keys associated with hpa configuration files. -public enum HPAKey { - public static let defaultConfigHome = "~/.config/\(Self.hpaConfigDirectoryName)/\(Self.defaultConfigFileName)" - public static let defaultConfigFileName = "config.toml" - public static let hpaConfigDirectoryName = "hpa" +enum Constants { + static let playbookCommand = "ansible-playbook" + static let playbookFileName = "main.yml" + static let inventoryFileName = "inventory.ini" + static let vaultCommand = "ansible-vault" } diff --git a/Sources/CodersClient/Coders.swift b/Sources/CodersClient/Coders.swift index cc51bdd..c97ee39 100644 --- a/Sources/CodersClient/Coders.swift +++ b/Sources/CodersClient/Coders.swift @@ -25,9 +25,15 @@ extension Coders: DependencyKey { public static var liveValue: Self { .init( jsonDecoder: { JSONDecoder() }, - jsonEncoder: { JSONEncoder() }, + jsonEncoder: { defaultJsonEncoder }, tomlDecoder: { TOMLDecoder() }, tomlEncoder: { TOMLEncoder() } ) } } + +private let defaultJsonEncoder: JSONEncoder = { + var encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes] + return encoder +}()