import CommandClient import ConfigurationClient import Dependencies import DependenciesMacros import FileClient // TODO: Add edit / view routes, possibly create? public extension DependencyValues { var vaultClient: VaultClient { get { self[VaultClient.self] } set { self[VaultClient.self] = newValue } } } @DependencyClient public struct VaultClient: Sendable { public var run: Run @DependencyClient public struct Run: Sendable { public var decrypt: @Sendable (RunOptions) async throws -> String public var encrypt: @Sendable (RunOptions) async throws -> String } public struct RunOptions: Equatable, Sendable { public let extraOptions: [String]? public let loggingOptions: LoggingOptions public let outputFilePath: String? public let quiet: Bool public let shell: String? public let vaultFilePath: String? public init( extraOptions: [String]? = nil, loggingOptions: LoggingOptions, outputFilePath: String? = nil, quiet: Bool = false, shell: String? = nil, vaultFilePath: String? = nil ) { self.extraOptions = extraOptions self.loggingOptions = loggingOptions self.outputFilePath = outputFilePath self.quiet = quiet self.shell = shell self.vaultFilePath = vaultFilePath } } @_spi(Internal) public enum Route: String, Equatable, Sendable { case encrypt case decrypt public var verb: String { rawValue } } } extension VaultClient: DependencyKey { public static let testValue: VaultClient = Self(run: Run()) public static var liveValue: VaultClient { .init( run: .init( decrypt: { try await $0.run(route: .decrypt) }, encrypt: { try await $0.run(route: .encrypt) } ) ) } } @_spi(Internal) public extension VaultClient { enum Constants { public static let vaultCommand = "ansible-vault" } } extension VaultClient.RunOptions { // Sets up the default arguments and runs the `ansible-vault` command, // returning the output file path, which is either supplied by the caller // or found via the `fileClient.findVaultFileInCurrentDirectory`. // // This allows the output to be piped into other commands. // // @discardableResult func run(route: VaultClient.Route) async throws -> String { @Dependency(\.commandClient) var commandClient return try await commandClient.run( logging: loggingOptions, quiet: quiet, shell: shell ) { @Dependency(\.configurationClient) var configurationClient @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger var output: String? let configuration = try await configurationClient.findAndLoad() logger.trace("Configuration: \(configuration)") var vaultFilePath: String? = vaultFilePath if vaultFilePath == nil { vaultFilePath = try await fileClient .findVaultFileInCurrentDirectory()? .cleanFilePath } guard let vaultFilePath else { throw VaultClientError.vaultFileNotFound } output = vaultFilePath logger.trace("Vault file: \(vaultFilePath)") var arguments = [ VaultClient.Constants.vaultCommand, route.verb ] if let outputFilePath { arguments.append(contentsOf: ["--output", outputFilePath]) output = outputFilePath } if let extraOptions { arguments.append(contentsOf: extraOptions) } if let vaultArgs = configuration.vault.args { arguments.append(contentsOf: vaultArgs) } if arguments.contains("encrypt"), !arguments.contains("--encrypt-vault-id"), let id = configuration.vault.encryptId { arguments.append(contentsOf: ["--encrypt-vault-id", id]) } arguments.append(vaultFilePath) logger.trace("Arguments: \(arguments)") return (arguments, output ?? "") } } } enum VaultClientError: Error { case vaultFileNotFound }