feat: Adds vault client
This commit is contained in:
@@ -12,7 +12,8 @@ let package = Package(
|
|||||||
.library(name: "CommandClient", targets: ["CommandClient"]),
|
.library(name: "CommandClient", targets: ["CommandClient"]),
|
||||||
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
|
||||||
.library(name: "FileClient", targets: ["FileClient"]),
|
.library(name: "FileClient", targets: ["FileClient"]),
|
||||||
.library(name: "PlaybookClient", targets: ["PlaybookClient"])
|
.library(name: "PlaybookClient", targets: ["PlaybookClient"]),
|
||||||
|
.library(name: "VaultClient", targets: ["VaultClient"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||||
@@ -75,6 +76,7 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "TestSupport",
|
name: "TestSupport",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"CommandClient",
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
]
|
]
|
||||||
@@ -124,6 +126,21 @@ let package = Package(
|
|||||||
"PlaybookClient",
|
"PlaybookClient",
|
||||||
"TestSupport"
|
"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"]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ extension ConfigurationClient: DependencyKey {
|
|||||||
public static var liveValue: Self {
|
public static var liveValue: Self {
|
||||||
.live(environment: ProcessInfo.processInfo.environment)
|
.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 {
|
struct LiveConfigurationClient {
|
||||||
@@ -229,3 +239,5 @@ enum ConfigurationError: Error {
|
|||||||
case decodingError
|
case decodingError
|
||||||
case fileExists(path: String)
|
case fileExists(path: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MockFindError: Error {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Represents a file location and type on disk for a configuration file.
|
/// Represents a file location and type on disk for a configuration file.
|
||||||
@@ -49,11 +50,3 @@ public enum File: Equatable, Sendable {
|
|||||||
return .toml(fileUrl)
|
return .toml(fileUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@_spi(Internal)
|
|
||||||
public extension URL {
|
|
||||||
|
|
||||||
var cleanFilePath: String {
|
|
||||||
absoluteString.replacing("file://", with: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@_spi(Internal) @_exported import CommandClient
|
||||||
@_exported import Dependencies
|
@_exported import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
@@ -7,6 +8,35 @@ public protocol TestCase {}
|
|||||||
|
|
||||||
public extension 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(
|
func withTestLogger(
|
||||||
key: String,
|
key: String,
|
||||||
logLevel: Logger.Level = .debug,
|
logLevel: Logger.Level = .debug,
|
||||||
@@ -131,3 +161,7 @@ public func withTestLogger(
|
|||||||
try await operation()
|
try await operation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TestSupportError: Error {
|
||||||
|
case optionsNotSet
|
||||||
|
}
|
||||||
|
|||||||
164
Sources/VaultClient/VaultClient.swift
Normal file
164
Sources/VaultClient/VaultClient.swift
Normal 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
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import CodersClient
|
import CodersClient
|
||||||
@_spi(Internal) import CommandClient
|
@_spi(Internal) import CommandClient
|
||||||
import ConfigurationClient
|
@_spi(Internal) import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import FileClient
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -12,12 +12,6 @@ import TestSupport
|
|||||||
@Suite("PlaybookClientTests")
|
@Suite("PlaybookClientTests")
|
||||||
struct PlaybookClientTests: TestCase {
|
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 {
|
static var sharedRunOptions: PlaybookClient.RunPlaybook.SharedRunOptions {
|
||||||
.init(loggingOptions: loggingOptions)
|
.init(loggingOptions: loggingOptions)
|
||||||
}
|
}
|
||||||
@@ -268,15 +262,6 @@ extension Result where Failure == TestError {
|
|||||||
static var failing: Self { .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 {}
|
struct TestError: Error {}
|
||||||
|
|
||||||
extension Tag {
|
extension Tag {
|
||||||
|
|||||||
114
Tests/VaultClientTests/VaultClientTests.swift
Normal file
114
Tests/VaultClientTests/VaultClientTests.swift
Normal 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 {}
|
||||||
Reference in New Issue
Block a user