feat: Adds vault client

This commit is contained in:
2024-12-15 17:27:28 -05:00
parent 6d0108da0c
commit 601869d457
7 changed files with 344 additions and 25 deletions

View File

@@ -12,7 +12,8 @@ let package = Package(
.library(name: "CommandClient", targets: ["CommandClient"]),
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
.library(name: "FileClient", targets: ["FileClient"]),
.library(name: "PlaybookClient", targets: ["PlaybookClient"])
.library(name: "PlaybookClient", targets: ["PlaybookClient"]),
.library(name: "VaultClient", targets: ["VaultClient"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
@@ -75,6 +76,7 @@ let package = Package(
.target(
name: "TestSupport",
dependencies: [
"CommandClient",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]
@@ -124,6 +126,21 @@ let package = Package(
"PlaybookClient",
"TestSupport"
]
),
.target(
name: "VaultClient",
dependencies: [
"CommandClient",
"ConfigurationClient",
"FileClient",
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.testTarget(
name: "VaultClientTests",
dependencies: ["VaultClient", "TestSupport"]
)
]
)

View File

@@ -59,6 +59,16 @@ extension ConfigurationClient: DependencyKey {
public static var liveValue: Self {
.live(environment: ProcessInfo.processInfo.environment)
}
@_spi(Internal)
public static func mock(_ configuration: Configuration = .mock) -> Self {
.init(
find: { throw MockFindError() },
generate: Self().generate,
load: { _ in configuration },
write: Self().write
)
}
}
struct LiveConfigurationClient {
@@ -229,3 +239,5 @@ enum ConfigurationError: Error {
case decodingError
case fileExists(path: String)
}
struct MockFindError: Error {}

View File

@@ -1,3 +1,4 @@
import FileClient
import Foundation
/// Represents a file location and type on disk for a configuration file.
@@ -49,11 +50,3 @@ public enum File: Equatable, Sendable {
return .toml(fileUrl)
}
}
@_spi(Internal)
public extension URL {
var cleanFilePath: String {
absoluteString.replacing("file://", with: "")
}
}

View File

@@ -1,3 +1,4 @@
@_spi(Internal) @_exported import CommandClient
@_exported import Dependencies
import Foundation
import Logging
@@ -7,6 +8,35 @@ public protocol TestCase {}
public extension TestCase {
static var loggingOptions: LoggingOptions {
let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug"
let logLevel = Logger.Level(rawValue: levelString) ?? .debug
return .init(commandName: "\(Self.self)", logLevel: logLevel)
}
func withCapturingCommandClient(
_ key: String,
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
run: @Sendable @escaping () async throws -> Void,
assert: @Sendable @escaping (CommandClient.RunCommandOptions) -> Void
) async throws {
let captured = CommandClient.CapturingClient()
try await withDependencies {
$0.commandClient = .capturing(captured)
setupDependencies(&$0)
} operation: {
try await Self.loggingOptions.withLogger {
try await run()
guard let options = await captured.options else {
throw TestSupportError.optionsNotSet
}
assert(options)
}
}
}
func withTestLogger(
key: String,
logLevel: Logger.Level = .debug,
@@ -131,3 +161,7 @@ public func withTestLogger(
try await operation()
}
}
enum TestSupportError: Error {
case optionsNotSet
}

View File

@@ -0,0 +1,164 @@
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: @Sendable (RunOptions) async throws -> Void
public struct RunOptions: Equatable, Sendable {
public let extraOptions: [String]?
public let loggingOptions: LoggingOptions
public let outputFilePath: String?
public let quiet: Bool
public let route: Route
public let shell: String?
public let vaultFilePath: String?
public init(
_ route: Route,
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.route = route
self.shell = shell
self.vaultFilePath = vaultFilePath
}
public enum Route: String, Equatable, Sendable {
case encrypt
case decrypt
@_spi(Internal)
public var verb: String { rawValue }
}
}
}
extension VaultClient: DependencyKey {
public static let testValue: VaultClient = Self()
public static var liveValue: VaultClient {
.init(
run: { try await $0.run() }
)
}
}
@_spi(Internal)
public extension VaultClient {
enum Constants {
public static let vaultCommand = "ansible-vault"
}
}
extension VaultClient.RunOptions {
func run() async throws {
@Dependency(\.commandClient) var commandClient
try await commandClient.run(
logging: loggingOptions,
quiet: quiet,
shell: shell
) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
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
}
logger.trace("Vault file: \(vaultFilePath)")
var arguments = [
VaultClient.Constants.vaultCommand,
route.verb
]
if let outputFilePath {
arguments.append(contentsOf: ["--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
}
}
}
// extension VaultClient.RunOptions.Route {
//
// var arguments: [String] {
// let outputFile: String?
// var arguments: [String]
//
// switch self {
// case let .decrypt(outputFile: output):
// outputFile = output
// arguments = ["decrypt"]
// case let .encrypt(outputFile: output):
// outputFile = output
// arguments = ["encrypt"]
// }
//
// if let outputFile {
// arguments.append(contentsOf: ["--output", outputFile])
// }
// return arguments
// }
// }
enum VaultClientError: Error {
case vaultFileNotFound
}

View File

@@ -1,6 +1,6 @@
import CodersClient
@_spi(Internal) import CommandClient
import ConfigurationClient
@_spi(Internal) import ConfigurationClient
import Dependencies
import FileClient
import Foundation
@@ -12,12 +12,6 @@ import TestSupport
@Suite("PlaybookClientTests")
struct PlaybookClientTests: TestCase {
static let loggingOptions: LoggingOptions = {
let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug"
let logLevel = Logger.Level(rawValue: levelString) ?? .debug
return .init(commandName: "PlaybookClientTests", logLevel: logLevel)
}()
static var sharedRunOptions: PlaybookClient.RunPlaybook.SharedRunOptions {
.init(loggingOptions: loggingOptions)
}
@@ -268,15 +262,6 @@ extension Result where Failure == TestError {
static var failing: Self { .failure(TestError()) }
}
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 {}
extension Tag {

View File

@@ -0,0 +1,114 @@
@_spi(Internal) import ConfigurationClient
import FileClient
import Foundation
import Testing
import TestSupport
@_spi(Internal) import VaultClient
@Suite("VaultClientTests")
struct VaultClientTests: TestCase {
@Test(
arguments: TestOptions.testCases
)
func decrypt(input: TestOptions) async throws {
try await withCapturingCommandClient("decrypt") {
$0.configurationClient = .mock(input.configuration)
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "/vault.yml") }
$0.vaultClient = .liveValue
} run: {
@Dependency(\.vaultClient) var vaultClient
try await vaultClient.run(.init(
.decrypt,
extraOptions: input.extraOptions,
loggingOptions: Self.loggingOptions,
outputFilePath: input.outputFilePath,
vaultFilePath: input.vaultFilePath
))
} assert: { options in
#expect(options.arguments == input.expected(.decrypt))
}
}
@Test(
arguments: TestOptions.testCases
)
func encrypt(input: TestOptions) async throws {
try await withCapturingCommandClient("decrypt") {
$0.configurationClient = .mock(input.configuration)
$0.fileClient.findVaultFileInCurrentDirectory = { URL(filePath: "/vault.yml") }
$0.vaultClient = .liveValue
} run: {
@Dependency(\.vaultClient) var vaultClient
try await vaultClient.run(.init(
.encrypt,
extraOptions: input.extraOptions,
loggingOptions: Self.loggingOptions,
outputFilePath: input.outputFilePath,
vaultFilePath: input.vaultFilePath
))
} assert: { options in
#expect(options.arguments == input.expected(.encrypt))
}
}
}
struct TestOptions: Sendable {
let configuration: Configuration
let extraOptions: [String]?
let outputFilePath: String?
let vaultFilePath: String?
init(
configuration: Configuration = .init(),
extraOptions: [String]? = nil,
outputFilePath: String? = nil,
vaultFilePath: String? = nil
) {
self.configuration = configuration
self.extraOptions = extraOptions
self.outputFilePath = outputFilePath
self.vaultFilePath = vaultFilePath
}
func expected(_ route: VaultClient.RunOptions.Route) -> [String] {
var expected = [
"ansible-vault", "\(route.verb)"
]
if let outputFilePath {
expected.append(contentsOf: ["--output", outputFilePath])
}
if let extraOptions {
expected.append(contentsOf: extraOptions)
}
if let vaultArgs = configuration.vault.args {
expected.append(contentsOf: vaultArgs)
}
if route == .encrypt,
let id = configuration.vault.encryptId
{
expected.append(contentsOf: ["--encrypt-vault-id", id])
}
expected.append(vaultFilePath ?? "/vault.yml")
return expected
}
static let testCases: [Self] = [
TestOptions(vaultFilePath: "/vault.yml"),
TestOptions(extraOptions: ["--verbose"]),
TestOptions(configuration: .mock),
TestOptions(outputFilePath: "/output.yml")
]
}
struct TestError: Error {}