feat: Breaking out more dependencies.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ DerivedData/
|
|||||||
.netrc
|
.netrc
|
||||||
.nvim/*
|
.nvim/*
|
||||||
.swiftpm/*
|
.swiftpm/*
|
||||||
|
hpa.toml
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 6.0
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
@@ -8,7 +7,9 @@ let package = Package(
|
|||||||
platforms: [.macOS(.v14)],
|
platforms: [.macOS(.v14)],
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "hpa", targets: ["hpa"]),
|
.executable(name: "hpa", targets: ["hpa"]),
|
||||||
.library(name: "CliClient", targets: ["CliClient"])
|
.library(name: "CliClient", targets: ["CliClient"]),
|
||||||
|
.library(name: "CodersClient", targets: ["CodersClient"]),
|
||||||
|
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"])
|
||||||
],
|
],
|
||||||
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"),
|
||||||
@@ -22,6 +23,7 @@ let package = Package(
|
|||||||
name: "hpa",
|
name: "hpa",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CliClient",
|
"CliClient",
|
||||||
|
"ConfigurationClient",
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "CliDoc", package: "swift-cli-doc"),
|
.product(name: "CliDoc", package: "swift-cli-doc"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
@@ -31,9 +33,11 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "CliClient",
|
name: "CliClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"CodersClient",
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client"),
|
||||||
|
.product(name: "TOMLKit", package: "TOMLKit")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
@@ -48,6 +52,48 @@ let package = Package(
|
|||||||
.copy("Resources/vault.yml"),
|
.copy("Resources/vault.yml"),
|
||||||
.copy("Resources/hpa-playbook")
|
.copy("Resources/hpa-playbook")
|
||||||
]
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "CodersClient",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "TOMLKit", package: "TOMLKit")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "TestSupport",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "ConfigurationClient",
|
||||||
|
dependencies: [
|
||||||
|
"CodersClient",
|
||||||
|
"FileClient",
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||||
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
.copy("Resources/hpa.toml")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ConfigurationClientTests",
|
||||||
|
dependencies: [
|
||||||
|
"ConfigurationClient",
|
||||||
|
"TestSupport"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "FileClient",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
|
.product(name: "DependenciesMacros", package: "swift-dependencies")
|
||||||
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import DependenciesMacros
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
// TODO: Drop support for non-json configuration.
|
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
var cliClient: CliClient {
|
var cliClient: CliClient {
|
||||||
get { self[CliClient.self] }
|
get { self[CliClient.self] }
|
||||||
@@ -14,11 +12,7 @@ public extension DependencyValues {
|
|||||||
|
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct CliClient: Sendable {
|
public struct CliClient: Sendable {
|
||||||
public var decoder: @Sendable () -> JSONDecoder = { .init() }
|
|
||||||
public var encoder: @Sendable () -> JSONEncoder = { .init() }
|
|
||||||
public var loadConfiguration: @Sendable () throws -> Configuration
|
|
||||||
public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void
|
public var runCommand: @Sendable ([String], Bool, ShellCommand.Shell) async throws -> Void
|
||||||
public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void
|
|
||||||
public var findVaultFileInCurrentDirectory: @Sendable () throws -> String
|
public var findVaultFileInCurrentDirectory: @Sendable () throws -> String
|
||||||
|
|
||||||
public func runCommand(
|
public func runCommand(
|
||||||
@@ -40,30 +34,12 @@ public struct CliClient: Sendable {
|
|||||||
|
|
||||||
extension CliClient: DependencyKey {
|
extension CliClient: DependencyKey {
|
||||||
|
|
||||||
// swiftlint:disable function_body_length
|
|
||||||
public static func live(
|
public static func live(
|
||||||
decoder: JSONDecoder = .init(),
|
|
||||||
encoder: JSONEncoder = .init(),
|
|
||||||
env: [String: String]
|
env: [String: String]
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@Dependency(\.fileClient) var fileClient
|
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
return .init {
|
return .init { args, quiet, shell in
|
||||||
decoder
|
|
||||||
} encoder: {
|
|
||||||
encoder
|
|
||||||
} loadConfiguration: {
|
|
||||||
let url = try findConfigurationFiles(env: env)
|
|
||||||
var env = env
|
|
||||||
|
|
||||||
logger.trace("Loading configuration from: \(url)")
|
|
||||||
guard let config = try fileClient.loadFile(url, &env, decoder) else {
|
|
||||||
return try .fromEnv(env)
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
|
|
||||||
} runCommand: { args, quiet, shell in
|
|
||||||
@Dependency(\.asyncShellClient) var shellClient
|
@Dependency(\.asyncShellClient) var shellClient
|
||||||
if !quiet {
|
if !quiet {
|
||||||
try await shellClient.foreground(.init(
|
try await shellClient.foreground(.init(
|
||||||
@@ -80,37 +56,15 @@ extension CliClient: DependencyKey {
|
|||||||
args
|
args
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
} createConfiguration: { path, json in
|
|
||||||
|
|
||||||
// Early out if a file exists at the path already.
|
|
||||||
guard !fileClient.fileExists(path) else {
|
|
||||||
throw CliClientError.fileExistsAtPath(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = path
|
|
||||||
let data: Data
|
|
||||||
|
|
||||||
if !json {
|
|
||||||
// Write the default env template.
|
|
||||||
data = Data(Configuration.fileTemplate.utf8)
|
|
||||||
} else {
|
|
||||||
if !path.contains(".json") {
|
|
||||||
path += ".json"
|
|
||||||
}
|
|
||||||
data = try jsonEncoder.encode(Configuration.mock)
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileClient.write(path, data)
|
|
||||||
} findVaultFileInCurrentDirectory: {
|
} findVaultFileInCurrentDirectory: {
|
||||||
guard let url = try fileClient.findVaultFileInCurrentDirectory() else {
|
fatalError()
|
||||||
throw CliClientError.vaultFileNotFound
|
// guard let url = try fileClient.findVaultFileInCurrentDirectory() else {
|
||||||
}
|
// throw CliClientError.vaultFileNotFound
|
||||||
return path(for: url)
|
// }
|
||||||
|
// return path(for: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:enable function_body_length
|
|
||||||
|
|
||||||
public static var liveValue: CliClient {
|
public static var liveValue: CliClient {
|
||||||
.live(env: ProcessInfo.processInfo.environment)
|
.live(env: ProcessInfo.processInfo.environment)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import Dependencies
|
|
||||||
import Foundation
|
|
||||||
import ShellClient
|
|
||||||
|
|
||||||
public struct Configuration2: Codable, Equatable, Sendable {
|
|
||||||
|
|
||||||
public let playbook: Playbook
|
|
||||||
public let template: Template
|
|
||||||
public let vault: Vault
|
|
||||||
|
|
||||||
public init(
|
|
||||||
playbook: Playbook,
|
|
||||||
template: Template,
|
|
||||||
vault: Vault
|
|
||||||
) {
|
|
||||||
self.playbook = playbook
|
|
||||||
self.template = template
|
|
||||||
self.vault = vault
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var mock: Self {
|
|
||||||
.init(playbook: .mock, template: .mock, vault: .mock)
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Playbook: Codable, Equatable, Sendable {
|
|
||||||
public let directory: String?
|
|
||||||
public let inventory: String?
|
|
||||||
public let args: [String]?
|
|
||||||
public let useVaultArgs: Bool
|
|
||||||
|
|
||||||
public init(
|
|
||||||
directory: String? = nil,
|
|
||||||
inventory: String? = nil,
|
|
||||||
args: [String]? = nil,
|
|
||||||
useVaultArgs: Bool = false
|
|
||||||
) {
|
|
||||||
self.directory = directory
|
|
||||||
self.inventory = inventory
|
|
||||||
self.args = args
|
|
||||||
self.useVaultArgs = useVaultArgs
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var mock: Self {
|
|
||||||
.init(
|
|
||||||
directory: "/path/to/local/playbook-directory",
|
|
||||||
inventory: "/path/to/local/inventory.ini",
|
|
||||||
args: [],
|
|
||||||
useVaultArgs: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Template: Codable, Equatable, Sendable {
|
|
||||||
let url: String?
|
|
||||||
let version: String?
|
|
||||||
let directory: String?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
url: String? = nil,
|
|
||||||
version: String? = nil,
|
|
||||||
directory: String? = nil
|
|
||||||
) {
|
|
||||||
self.url = url
|
|
||||||
self.version = version
|
|
||||||
self.directory = directory
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var mock: Self {
|
|
||||||
.init(
|
|
||||||
url: "https://git.example.com/consult-template.git",
|
|
||||||
version: "main",
|
|
||||||
directory: "/path/to/local/template-directory"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Vault: Codable, Equatable, Sendable {
|
|
||||||
public let args: [String]?
|
|
||||||
public let encryptId: String?
|
|
||||||
|
|
||||||
public init(
|
|
||||||
args: [String]? = nil,
|
|
||||||
encryptId: String? = nil
|
|
||||||
) {
|
|
||||||
self.args = args
|
|
||||||
self.encryptId = encryptId
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var mock: Self {
|
|
||||||
.init(
|
|
||||||
args: [
|
|
||||||
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
|
|
||||||
],
|
|
||||||
encryptId: "myId"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the configurable items for the command line tool.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
public struct Configuration: Codable, Equatable, Sendable {
|
|
||||||
|
|
||||||
public let playbookDir: String?
|
|
||||||
public let inventoryPath: String?
|
|
||||||
public let templateRepo: String?
|
|
||||||
public let templateRepoVersion: String?
|
|
||||||
public let templateDir: String?
|
|
||||||
public let defaultPlaybookArgs: [String]?
|
|
||||||
public let defaultVaultArgs: [String]?
|
|
||||||
public let defaultVaultEncryptId: String?
|
|
||||||
|
|
||||||
fileprivate enum CodingKeys: String, CodingKey {
|
|
||||||
case playbookDir = "HPA_PLAYBOOK_DIR"
|
|
||||||
case inventoryPath = "HPA_DEFAULT_INVENTORY"
|
|
||||||
case templateRepo = "HPA_TEMPLATE_REPO"
|
|
||||||
case templateRepoVersion = "HPA_TEMPLATE_VERSION"
|
|
||||||
case templateDir = "HPA_TEMPLATE_DIR"
|
|
||||||
case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS"
|
|
||||||
case defaultVaultArgs = "HPA_DEFAULT_VAULT_ARGS"
|
|
||||||
case defaultVaultEncryptId = "HPA_DEFAULT_VAULT_ENCRYPT_ID"
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func fromEnv(
|
|
||||||
_ env: [String: String]
|
|
||||||
) throws -> Self {
|
|
||||||
@Dependency(\.logger) var logger
|
|
||||||
|
|
||||||
logger.trace("Creating configuration from env...")
|
|
||||||
|
|
||||||
let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") }
|
|
||||||
logger.debug("HPA env vars: \(hpaValues)")
|
|
||||||
|
|
||||||
return Configuration(
|
|
||||||
playbookDir: hpaValues.value(for: .playbookDir),
|
|
||||||
inventoryPath: hpaValues.value(for: .inventoryPath),
|
|
||||||
templateRepo: hpaValues.value(for: .templateRepo),
|
|
||||||
templateRepoVersion: hpaValues.value(for: .templateRepoVersion),
|
|
||||||
templateDir: hpaValues.value(for: .templateDir),
|
|
||||||
defaultPlaybookArgs: hpaValues.array(for: .defaultPlaybookArgs),
|
|
||||||
defaultVaultArgs: hpaValues.array(for: .defaultVaultArgs),
|
|
||||||
defaultVaultEncryptId: hpaValues.value(for: .defaultVaultEncryptId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static var mock: Self {
|
|
||||||
.init(
|
|
||||||
playbookDir: "/path/to/playbook",
|
|
||||||
inventoryPath: "/path/to/inventory.ini",
|
|
||||||
templateRepo: "https://git.example.com/consult-template.git",
|
|
||||||
templateRepoVersion: "main",
|
|
||||||
templateDir: "/path/to/local/template",
|
|
||||||
defaultPlaybookArgs: ["--tags", "debug"],
|
|
||||||
defaultVaultArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"],
|
|
||||||
defaultVaultEncryptId: "myId"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static var fileTemplate: String {
|
|
||||||
"""
|
|
||||||
# vi: ft=sh
|
|
||||||
|
|
||||||
# Example configuration, uncomment the lines and set the values appropriate for your
|
|
||||||
# usage.
|
|
||||||
|
|
||||||
# Set this to the location of the ansible-hpa-playbook on your local machine.
|
|
||||||
#HPA_PLAYBOOK_DIR="/path/to/ansible-hpa-playbook"
|
|
||||||
|
|
||||||
# Set this to the location of a template repository, which is used to create new assessment projects.
|
|
||||||
#HPA_TEMPLATE_REPO="https://git.example.com/your/template.git"
|
|
||||||
|
|
||||||
# Specify a branch, version, or sha of the template repository.
|
|
||||||
#HPA_TEMPLATE_VERSION="main" # branch, version, or sha
|
|
||||||
|
|
||||||
# Set this to a location of a template directory to use to create new projects.
|
|
||||||
#HPA_TEMPLATE_DIR="/path/to/local/template"
|
|
||||||
|
|
||||||
# Extra arguments that get passed directly to the ansible-playbook command.
|
|
||||||
#HPA_DEFAULT_PLAYBOOK_ARGS="--vault-id=consults@$SCRIPTS/vault-gopass-client"
|
|
||||||
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension [String: String] {
|
|
||||||
fileprivate func value(for codingKey: Configuration.CodingKeys) -> String? {
|
|
||||||
self[codingKey.rawValue]
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func array(for codingKey: Configuration.CodingKeys) -> [String]? {
|
|
||||||
value(for: codingKey).flatMap { $0.split(separator: ",").map(String.init) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
13
Sources/CliClient/Constants.swift
Normal file
13
Sources/CliClient/Constants.swift
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/// 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"
|
||||||
|
}
|
||||||
@@ -1,159 +1,183 @@
|
|||||||
import Dependencies
|
// import CodersClient
|
||||||
import DependenciesMacros
|
// import Dependencies
|
||||||
import Foundation
|
// import DependenciesMacros
|
||||||
|
// import Foundation
|
||||||
@_spi(Internal)
|
// import TOMLKit
|
||||||
public extension DependencyValues {
|
//
|
||||||
var fileClient: FileClient {
|
// @_spi(Internal)
|
||||||
get { self[FileClient.self] }
|
// public extension DependencyValues {
|
||||||
set { self[FileClient.self] = newValue }
|
// var fileClient: FileClient {
|
||||||
}
|
// get { self[FileClient.self] }
|
||||||
}
|
// set { self[FileClient.self] = newValue }
|
||||||
|
// }
|
||||||
@_spi(Internal)
|
// }
|
||||||
@DependencyClient
|
//
|
||||||
public struct FileClient: Sendable {
|
// @_spi(Internal)
|
||||||
/// Loads a file at the given path into the environment unless it can decode it as json,
|
// @DependencyClient
|
||||||
/// at which point it will return the decoded file contents as a ``Configuration`` item.
|
// public struct FileClient: Sendable {
|
||||||
public var loadFile: @Sendable (URL, inout [String: String], JSONDecoder) throws -> Configuration?
|
//
|
||||||
|
// /// Load a file as `Data`.
|
||||||
/// Returns the user's home directory path.
|
// public var loadFile: @Sendable (URL) throws -> Data
|
||||||
public var homeDir: @Sendable () -> URL = { URL(string: "~/")! }
|
//
|
||||||
|
// /// Returns the user's home directory path.
|
||||||
/// Check if a path is a directory.
|
// public var homeDir: @Sendable () -> URL = { URL(string: "~/")! }
|
||||||
public var isDirectory: @Sendable (URL) -> Bool = { _ in false }
|
//
|
||||||
|
// /// Check if a path is a directory.
|
||||||
/// Check if a path is a readable.
|
// public var isDirectory: @Sendable (URL) -> Bool = { _ in false }
|
||||||
public var isReadable: @Sendable (URL) -> Bool = { _ in false }
|
//
|
||||||
|
// /// Check if a path is a readable.
|
||||||
/// Check if a file exists at the path.
|
// public var isReadable: @Sendable (URL) -> Bool = { _ in false }
|
||||||
public var fileExists: @Sendable (String) -> Bool = { _ in false }
|
//
|
||||||
|
// /// Check if a file exists at the path.
|
||||||
public var findVaultFile: @Sendable (String) throws -> URL?
|
// public var fileExists: @Sendable (String) -> Bool = { _ in false }
|
||||||
|
//
|
||||||
/// Write data to a file.
|
// public var findVaultFile: @Sendable (String) throws -> URL?
|
||||||
public var write: @Sendable (String, Data) throws -> Void
|
//
|
||||||
|
// /// Write data to a file.
|
||||||
public func findVaultFileInCurrentDirectory() throws -> URL? {
|
// public var write: @Sendable (File, Data) throws -> Void
|
||||||
try findVaultFile(".")
|
//
|
||||||
}
|
// public func findVaultFileInCurrentDirectory() throws -> URL? {
|
||||||
}
|
// try findVaultFile(".")
|
||||||
|
// }
|
||||||
@_spi(Internal)
|
//
|
||||||
extension FileClient: DependencyKey {
|
// public func loadFile(_ file: File) throws -> Data {
|
||||||
public static let testValue: FileClient = Self()
|
// try loadFile(file.url)
|
||||||
|
// }
|
||||||
public static func live(fileManager: FileManager = .default) -> Self {
|
//
|
||||||
let client = LiveFileClient(fileManager: fileManager)
|
// public func loadConfiguration(
|
||||||
return Self(
|
// _ file: File
|
||||||
loadFile: { try client.loadFile(at: $0, into: &$1, decoder: $2) },
|
// ) throws -> Configuration {
|
||||||
homeDir: { client.homeDir },
|
// @Dependency(\.coders) var coders
|
||||||
isDirectory: { client.isDirectory(url: $0) },
|
//
|
||||||
isReadable: { client.isReadable(url: $0) },
|
// let data = try loadFile(file)
|
||||||
fileExists: { client.fileExists(at: $0) },
|
//
|
||||||
findVaultFile: { try client.findVaultFile(in: $0) },
|
// switch file {
|
||||||
write: { path, data in
|
// case .json:
|
||||||
try data.write(to: URL(filePath: path))
|
// return try coders.jsonDecoder().decode(Configuration.self, from: data)
|
||||||
}
|
// case .toml:
|
||||||
)
|
// guard let string = String(data: data, encoding: .utf8) else {
|
||||||
}
|
// throw DecodingError()
|
||||||
|
// }
|
||||||
public static let liveValue = Self.live()
|
// return try coders.tomlDecoder().decode(Configuration.self, from: string)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
private struct LiveFileClient: @unchecked Sendable {
|
// }
|
||||||
|
//
|
||||||
private let fileManager: FileManager
|
// @_spi(Internal)
|
||||||
|
// extension FileClient: DependencyKey {
|
||||||
init(fileManager: FileManager) {
|
// public static let testValue: FileClient = Self()
|
||||||
self.fileManager = fileManager
|
//
|
||||||
}
|
// public static func live(fileManager: FileManager = .default) -> Self {
|
||||||
|
// let client = LiveFileClient(fileManager: fileManager)
|
||||||
var homeDir: URL { fileManager.homeDirectoryForCurrentUser }
|
// return Self(
|
||||||
|
// loadFile: { try Data(contentsOf: $0) },
|
||||||
func isDirectory(url: URL) -> Bool {
|
// homeDir: { client.homeDir },
|
||||||
var isDirectory: ObjCBool = false
|
// isDirectory: { client.isDirectory(url: $0) },
|
||||||
fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory)
|
// isReadable: { client.isReadable(url: $0) },
|
||||||
return isDirectory.boolValue
|
// fileExists: { client.fileExists(at: $0) },
|
||||||
}
|
// findVaultFile: { try client.findVaultFile(in: $0) },
|
||||||
|
// write: { file, data in
|
||||||
func isReadable(url: URL) -> Bool {
|
// try data.write(to: file.url)
|
||||||
fileManager.isReadableFile(atPath: path(for: url))
|
// }
|
||||||
}
|
// )
|
||||||
|
// }
|
||||||
func fileExists(at path: String) -> Bool {
|
//
|
||||||
fileManager.fileExists(atPath: path)
|
// public static let liveValue = Self.live()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// func findVaultFileInCurrentDirectory() throws -> URL? {
|
// private struct LiveFileClient: @unchecked Sendable {
|
||||||
|
//
|
||||||
func findVaultFile(in filePath: String) throws -> URL? {
|
// private let fileManager: FileManager
|
||||||
let urls = try fileManager
|
//
|
||||||
.contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil)
|
// init(fileManager: FileManager) {
|
||||||
|
// self.fileManager = fileManager
|
||||||
if let vault = urls.firstVaultFile {
|
// }
|
||||||
return vault
|
//
|
||||||
}
|
// var homeDir: URL { fileManager.homeDirectoryForCurrentUser }
|
||||||
|
//
|
||||||
// check for folders that end with "vars" and search those next.
|
// func isDirectory(url: URL) -> Bool {
|
||||||
for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) {
|
// var isDirectory: ObjCBool = false
|
||||||
let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
// fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory)
|
||||||
if let vault = files.firstVaultFile {
|
// return isDirectory.boolValue
|
||||||
return vault
|
// }
|
||||||
}
|
//
|
||||||
}
|
// func isReadable(url: URL) -> Bool {
|
||||||
|
// fileManager.isReadableFile(atPath: path(for: url))
|
||||||
// Fallback to check all sub-folders
|
// }
|
||||||
for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) {
|
//
|
||||||
let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
// func fileExists(at path: String) -> Bool {
|
||||||
if let vault = files.firstVaultFile {
|
// fileManager.fileExists(atPath: path)
|
||||||
return vault
|
// }
|
||||||
}
|
//
|
||||||
}
|
// func findVaultFile(in filePath: String) throws -> URL? {
|
||||||
return nil
|
// let urls = try fileManager
|
||||||
}
|
// .contentsOfDirectory(at: URL(filePath: filePath), includingPropertiesForKeys: nil)
|
||||||
|
//
|
||||||
func loadFile(
|
// if let vault = urls.firstVaultFile {
|
||||||
at url: URL,
|
// return vault
|
||||||
into env: inout [String: String],
|
// }
|
||||||
decoder: JSONDecoder
|
//
|
||||||
) throws -> Configuration? {
|
// // check for folders that end with "vars" and search those next.
|
||||||
@Dependency(\.logger) var logger
|
// for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) {
|
||||||
logger.trace("Begin load file for: \(path(for: url))")
|
// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
||||||
|
// if let vault = files.firstVaultFile {
|
||||||
if url.absoluteString.hasSuffix(".json") {
|
// return vault
|
||||||
// Handle json file.
|
// }
|
||||||
let data = try Data(contentsOf: url)
|
// }
|
||||||
return try decoder.decode(Configuration.self, from: data)
|
//
|
||||||
}
|
// // Fallback to check all sub-folders
|
||||||
|
// for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) {
|
||||||
let string = try String(contentsOfFile: path(for: url), encoding: .utf8)
|
// let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
||||||
|
// if let vault = files.firstVaultFile {
|
||||||
logger.trace("Loaded file contents: \(string)")
|
// return vault
|
||||||
|
// }
|
||||||
let lines = string.split(separator: "\n")
|
// }
|
||||||
for line in lines {
|
// return nil
|
||||||
logger.trace("Line: \(line)")
|
// }
|
||||||
let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
//
|
||||||
let splitLine = strippedLine.split(separator: "=").map {
|
// func loadFile(
|
||||||
$0.replacingOccurrences(of: "\"", with: "")
|
// at url: URL,
|
||||||
}
|
// into env: inout [String: String],
|
||||||
logger.trace("Split Line: \(splitLine)")
|
// decoder: JSONDecoder
|
||||||
guard splitLine.count >= 2 else { continue }
|
// ) throws -> Configuration? {
|
||||||
|
// @Dependency(\.logger) var logger
|
||||||
if splitLine.count > 2 {
|
// logger.trace("Begin load file for: \(path(for: url))")
|
||||||
let rest = splitLine.dropFirst()
|
//
|
||||||
env[String(splitLine[0])] = String(rest.joined(separator: "="))
|
// if url.absoluteString.hasSuffix(".json") {
|
||||||
} else {
|
// // Handle json file.
|
||||||
env[String(splitLine[0])] = String(splitLine[1])
|
// let data = try Data(contentsOf: url)
|
||||||
}
|
// return try decoder.decode(Configuration.self, from: data)
|
||||||
}
|
// }
|
||||||
return nil
|
//
|
||||||
}
|
// let string = try String(contentsOfFile: path(for: url), encoding: .utf8)
|
||||||
}
|
//
|
||||||
|
// logger.trace("Loaded file contents: \(string)")
|
||||||
private extension Array where Element == URL {
|
//
|
||||||
var firstVaultFile: URL? {
|
// let lines = string.split(separator: "\n")
|
||||||
first { $0.absoluteString.hasSuffix("vault.yml") }
|
// for line in lines {
|
||||||
}
|
// logger.trace("Line: \(line)")
|
||||||
}
|
// let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
// let splitLine = strippedLine.split(separator: "=").map {
|
||||||
|
// $0.replacingOccurrences(of: "\"", with: "")
|
||||||
|
// }
|
||||||
|
// logger.trace("Split Line: \(splitLine)")
|
||||||
|
// guard splitLine.count >= 2 else { continue }
|
||||||
|
//
|
||||||
|
// if splitLine.count > 2 {
|
||||||
|
// let rest = splitLine.dropFirst()
|
||||||
|
// env[String(splitLine[0])] = String(rest.joined(separator: "="))
|
||||||
|
// } else {
|
||||||
|
// env[String(splitLine[0])] = String(splitLine[1])
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// struct DecodingError: Error {}
|
||||||
|
//
|
||||||
|
// private extension Array where Element == URL {
|
||||||
|
// var firstVaultFile: URL? {
|
||||||
|
// first { $0.absoluteString.hasSuffix("vault.yml") }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,110 +1,116 @@
|
|||||||
import Dependencies
|
// import Dependencies
|
||||||
import Foundation
|
// import Foundation
|
||||||
import ShellClient
|
// import ShellClient
|
||||||
|
//
|
||||||
@_spi(Internal)
|
// @_spi(Internal)
|
||||||
public func findConfigurationFiles(
|
// public func findConfigurationFiles(
|
||||||
env: [String: String] = ProcessInfo.processInfo.environment
|
// env: [String: String] = ProcessInfo.processInfo.environment
|
||||||
) throws -> URL {
|
// ) throws -> File {
|
||||||
@Dependency(\.logger) var logger
|
// @Dependency(\.logger) var logger
|
||||||
@Dependency(\.fileClient) var fileClient
|
// @Dependency(\.fileClient) var fileClient
|
||||||
|
//
|
||||||
logger.debug("Begin find configuration files.")
|
// logger.debug("Begin find configuration files.")
|
||||||
logger.trace("Env: \(env)")
|
// logger.trace("Env: \(env)")
|
||||||
|
//
|
||||||
// Check for environment variable pointing to a directory that the
|
// // Check for environment variable pointing to a directory that the
|
||||||
// the configuration lives.
|
// // the configuration lives.
|
||||||
if let pwd = env["PWD"],
|
// if let pwd = env["PWD"],
|
||||||
let url = fileClient.checkUrl(.file(pwd, ".hparc"))
|
// let file = fileClient.checkUrl(.directory(pwd))
|
||||||
{
|
// {
|
||||||
logger.debug("Found configuration in current working directory.")
|
// logger.debug("Found configuration in current working directory.")
|
||||||
return url
|
// return file
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Check for environment variable pointing to a file that the
|
// // Check for environment variable pointing to a file that the
|
||||||
// the configuration lives.
|
// // the configuration lives.
|
||||||
if let configFile = env["HPA_CONFIG_FILE"],
|
// if let configFile = env[EnvironmentKey.hpaConfigFileKey],
|
||||||
let url = fileClient.checkUrl(.file(configFile))
|
// let file = fileClient.checkUrl(.file(configFile))
|
||||||
{
|
// {
|
||||||
logger.debug("Found configuration from hpa config file env var.")
|
// logger.debug("Found configuration from hpa config file env var.")
|
||||||
return url
|
// return file
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Check for environment variable pointing to a directory that the
|
// // Check for environment variable pointing to a directory that the
|
||||||
// the configuration lives.
|
// // the configuration lives.
|
||||||
if let configHome = env["HPA_CONFIG_HOME"],
|
// if let configHome = env[EnvironmentKey.hpaConfigDirKey],
|
||||||
let url = fileClient.checkUrl(.directory(configHome))
|
// let file = fileClient.checkUrl(.directory(configHome))
|
||||||
{
|
// {
|
||||||
logger.debug("Found configuration from hpa config home env var.")
|
// logger.debug("Found configuration from hpa config home env var.")
|
||||||
return url
|
// return file
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Check home directory for a `.hparc` file.
|
// // Check home directory for a `.hparc` file.
|
||||||
if let url = fileClient.checkUrl(.file(fileClient.homeDir().appending(path: ".hparc"))) {
|
// if let file = fileClient.checkUrl(.directory(fileClient.homeDir())) {
|
||||||
logger.debug("Found configuration in home directory")
|
// logger.debug("Found configuration in home directory")
|
||||||
return url
|
// return file
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Check in xdg config home, under an hpa-playbook directory.
|
// // Check in xdg config home, under an hpa-playbook directory.
|
||||||
if let xdgConfigHome = env["XDG_CONFIG_HOME"],
|
// if let xdgConfigHome = env[EnvironmentKey.xdgConfigHomeKey],
|
||||||
let url = fileClient.checkUrl(.directory(xdgConfigHome, "hpa-playbook"))
|
// let file = fileClient.checkUrl(.directory(xdgConfigHome, HPAKey.hpaConfigDirectoryName))
|
||||||
{
|
// {
|
||||||
logger.debug("XDG Config url: \(url.absoluteString)")
|
// logger.debug("XDG Config url: \(file.path)")
|
||||||
return url
|
// return file
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// We could not find configuration in any usual places.
|
// // We could not find configuration in any usual places.
|
||||||
throw ConfigurationError.configurationNotFound
|
// throw ConfigurationError.configurationNotFound
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@_spi(Internal)
|
// @_spi(Internal)
|
||||||
public func path(for url: URL) -> String {
|
// public func path(for url: URL) -> String {
|
||||||
url.absoluteString.replacing("file://", with: "")
|
// url.absoluteString.replacing("file://", with: "")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
enum ConfigurationError: Error {
|
// enum ConfigurationError: Error {
|
||||||
case configurationNotFound
|
// case configurationNotFound
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private extension FileClient {
|
// private extension FileClient {
|
||||||
|
//
|
||||||
enum ConfigurationUrlCheck {
|
// static let validFileNames = [
|
||||||
case file(URL)
|
// "config.json", "config.toml",
|
||||||
case directory(URL)
|
// ".hparc.json", ".hparc.toml"
|
||||||
|
// ]
|
||||||
static func file(_ path: String) -> Self { .file(URL(filePath: path)) }
|
//
|
||||||
|
// enum ConfigurationUrlCheck {
|
||||||
static func file(_ paths: String...) -> Self {
|
// case file(URL)
|
||||||
var url = URL(filePath: paths[0])
|
// case directory(URL)
|
||||||
url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
|
//
|
||||||
return .file(url)
|
// static func file(_ path: String) -> Self { .file(URL(filePath: path)) }
|
||||||
}
|
//
|
||||||
|
// static func file(_ paths: String...) -> Self {
|
||||||
static func directory(_ path: String) -> Self { .directory(URL(filePath: path)) }
|
// var url = URL(filePath: paths[0])
|
||||||
static func directory(_ paths: String...) -> Self {
|
// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
|
||||||
var url = URL(filePath: paths[0])
|
// return .file(url)
|
||||||
url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
|
// }
|
||||||
return .directory(url)
|
//
|
||||||
}
|
// static func directory(_ path: String) -> Self { .directory(URL(filePath: path)) }
|
||||||
}
|
//
|
||||||
|
// static func directory(_ paths: String...) -> Self {
|
||||||
func checkUrl(_ check: ConfigurationUrlCheck) -> URL? {
|
// var url = URL(filePath: paths[0])
|
||||||
switch check {
|
// url = paths.dropFirst().reduce(into: url) { $0.append(path: $1) }
|
||||||
case let .file(url):
|
// return .directory(url)
|
||||||
if isReadable(url) { return url }
|
// }
|
||||||
return nil
|
// }
|
||||||
case let .directory(url):
|
//
|
||||||
return findConfigurationInDirectory(url)
|
// func checkUrl(_ check: ConfigurationUrlCheck) -> File? {
|
||||||
}
|
// switch check {
|
||||||
}
|
// case let .file(url):
|
||||||
|
// if isReadable(url) { return .init(url) }
|
||||||
func findConfigurationInDirectory(_ url: URL) -> URL? {
|
// return nil
|
||||||
for file in ["config", "config.json"] {
|
// case let .directory(url):
|
||||||
let fileUrl = url.appending(path: file)
|
// return findConfigurationInDirectory(url)
|
||||||
if isReadable(fileUrl) {
|
// }
|
||||||
return fileUrl
|
// }
|
||||||
}
|
//
|
||||||
}
|
// func findConfigurationInDirectory(_ url: URL) -> File? {
|
||||||
return nil
|
// for file in Self.validFileNames {
|
||||||
}
|
// let fileUrl = url.appending(path: file)
|
||||||
}
|
// if isReadable(fileUrl) {
|
||||||
|
// return .init(fileUrl)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
33
Sources/CodersClient/Coders.swift
Normal file
33
Sources/CodersClient/Coders.swift
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Foundation
|
||||||
|
import TOMLKit
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
var coders: Coders {
|
||||||
|
get { self[Coders.self] }
|
||||||
|
set { self[Coders.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct Coders: Sendable {
|
||||||
|
public var jsonDecoder: @Sendable () -> JSONDecoder = { .init() }
|
||||||
|
public var jsonEncoder: @Sendable () -> JSONEncoder = { .init() }
|
||||||
|
public var tomlDecoder: @Sendable () -> TOMLDecoder = { .init() }
|
||||||
|
public var tomlEncoder: @Sendable () -> TOMLEncoder = { .init() }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Coders: DependencyKey {
|
||||||
|
|
||||||
|
public static var testValue: Self { Self() }
|
||||||
|
|
||||||
|
public static var liveValue: Self {
|
||||||
|
.init(
|
||||||
|
jsonDecoder: { JSONDecoder() },
|
||||||
|
jsonEncoder: { JSONEncoder() },
|
||||||
|
tomlDecoder: { TOMLDecoder() },
|
||||||
|
tomlEncoder: { TOMLEncoder() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
188
Sources/ConfigurationClient/Configuration.swift
Normal file
188
Sources/ConfigurationClient/Configuration.swift
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents configurable settings for the command line tool.
|
||||||
|
public struct Configuration: Codable, Equatable, Sendable {
|
||||||
|
public let args: [String]?
|
||||||
|
public let useVaultArgs: Bool
|
||||||
|
public let playbook: Playbook?
|
||||||
|
public let template: Template
|
||||||
|
public let vault: Vault
|
||||||
|
|
||||||
|
public init(
|
||||||
|
args: [String]? = nil,
|
||||||
|
useVaultArgs: Bool = true,
|
||||||
|
playbook: Playbook? = nil,
|
||||||
|
template: Template = .init(),
|
||||||
|
vault: Vault = .init()
|
||||||
|
) {
|
||||||
|
self.args = args
|
||||||
|
self.useVaultArgs = useVaultArgs
|
||||||
|
self.playbook = playbook
|
||||||
|
self.template = template
|
||||||
|
self.vault = vault
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var mock: Self {
|
||||||
|
.init(
|
||||||
|
args: [],
|
||||||
|
useVaultArgs: true,
|
||||||
|
playbook: nil,
|
||||||
|
template: .mock,
|
||||||
|
vault: .mock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Playbook: Codable, Equatable, Sendable {
|
||||||
|
public let directory: String?
|
||||||
|
public let inventory: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
directory: String? = nil,
|
||||||
|
inventory: String? = nil
|
||||||
|
) {
|
||||||
|
self.directory = directory
|
||||||
|
self.inventory = inventory
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var mock: Self { .init() }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Template: Codable, Equatable, Sendable {
|
||||||
|
public let url: String?
|
||||||
|
public let version: String?
|
||||||
|
public let directory: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
url: String? = nil,
|
||||||
|
version: String? = nil,
|
||||||
|
directory: String? = nil
|
||||||
|
) {
|
||||||
|
self.url = url
|
||||||
|
self.version = version
|
||||||
|
self.directory = directory
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var mock: Self {
|
||||||
|
.init(
|
||||||
|
url: "https://git.example.com/consult-template.git",
|
||||||
|
version: "1.0.0",
|
||||||
|
directory: "/path/to/local/template-directory"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Vault: Codable, Equatable, Sendable {
|
||||||
|
public let args: [String]?
|
||||||
|
public let encryptId: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
args: [String]? = nil,
|
||||||
|
encryptId: String? = nil
|
||||||
|
) {
|
||||||
|
self.args = args
|
||||||
|
self.encryptId = encryptId
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var mock: Self {
|
||||||
|
.init(
|
||||||
|
args: [
|
||||||
|
"--vault-id=myId@$SCRIPTS/vault-gopass-client"
|
||||||
|
],
|
||||||
|
encryptId: "myId"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// public struct Configuration: Codable, Equatable, Sendable {
|
||||||
|
//
|
||||||
|
// public let playbookDir: String?
|
||||||
|
// public let inventoryPath: String?
|
||||||
|
// public let templateRepo: String?
|
||||||
|
// public let templateRepoVersion: String?
|
||||||
|
// public let templateDir: String?
|
||||||
|
// public let defaultPlaybookArgs: [String]?
|
||||||
|
// public let defaultVaultArgs: [String]?
|
||||||
|
// public let defaultVaultEncryptId: String?
|
||||||
|
//
|
||||||
|
// fileprivate enum CodingKeys: String, CodingKey {
|
||||||
|
// case playbookDir = "HPA_PLAYBOOK_DIR"
|
||||||
|
// case inventoryPath = "HPA_DEFAULT_INVENTORY"
|
||||||
|
// case templateRepo = "HPA_TEMPLATE_REPO"
|
||||||
|
// case templateRepoVersion = "HPA_TEMPLATE_VERSION"
|
||||||
|
// case templateDir = "HPA_TEMPLATE_DIR"
|
||||||
|
// case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS"
|
||||||
|
// case defaultVaultArgs = "HPA_DEFAULT_VAULT_ARGS"
|
||||||
|
// case defaultVaultEncryptId = "HPA_DEFAULT_VAULT_ENCRYPT_ID"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public static func fromEnv(
|
||||||
|
// _ env: [String: String]
|
||||||
|
// ) throws -> Self {
|
||||||
|
// @Dependency(\.logger) var logger
|
||||||
|
//
|
||||||
|
// logger.trace("Creating configuration from env...")
|
||||||
|
//
|
||||||
|
// let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") }
|
||||||
|
// logger.debug("HPA env vars: \(hpaValues)")
|
||||||
|
//
|
||||||
|
// return Configuration(
|
||||||
|
// playbookDir: hpaValues.value(for: .playbookDir),
|
||||||
|
// inventoryPath: hpaValues.value(for: .inventoryPath),
|
||||||
|
// templateRepo: hpaValues.value(for: .templateRepo),
|
||||||
|
// templateRepoVersion: hpaValues.value(for: .templateRepoVersion),
|
||||||
|
// templateDir: hpaValues.value(for: .templateDir),
|
||||||
|
// defaultPlaybookArgs: hpaValues.array(for: .defaultPlaybookArgs),
|
||||||
|
// defaultVaultArgs: hpaValues.array(for: .defaultVaultArgs),
|
||||||
|
// defaultVaultEncryptId: hpaValues.value(for: .defaultVaultEncryptId)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// static var mock: Self {
|
||||||
|
// .init(
|
||||||
|
// playbookDir: "/path/to/playbook",
|
||||||
|
// inventoryPath: "/path/to/inventory.ini",
|
||||||
|
// templateRepo: "https://git.example.com/consult-template.git",
|
||||||
|
// templateRepoVersion: "main",
|
||||||
|
// templateDir: "/path/to/local/template",
|
||||||
|
// defaultPlaybookArgs: ["--tags", "debug"],
|
||||||
|
// defaultVaultArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"],
|
||||||
|
// defaultVaultEncryptId: "myId"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// static var fileTemplate: String {
|
||||||
|
// """
|
||||||
|
// # vi: ft=sh
|
||||||
|
//
|
||||||
|
// # Example configuration, uncomment the lines and set the values appropriate for your
|
||||||
|
// # usage.
|
||||||
|
//
|
||||||
|
// # Set this to the location of the ansible-hpa-playbook on your local machine.
|
||||||
|
// #HPA_PLAYBOOK_DIR="/path/to/ansible-hpa-playbook"
|
||||||
|
//
|
||||||
|
// # Set this to the location of a template repository, which is used to create new assessment projects.
|
||||||
|
// #HPA_TEMPLATE_REPO="https://git.example.com/your/template.git"
|
||||||
|
//
|
||||||
|
// # Specify a branch, version, or sha of the template repository.
|
||||||
|
// #HPA_TEMPLATE_VERSION="main" # branch, version, or sha
|
||||||
|
//
|
||||||
|
// # Set this to a location of a template directory to use to create new projects.
|
||||||
|
// #HPA_TEMPLATE_DIR="/path/to/local/template"
|
||||||
|
//
|
||||||
|
// # Extra arguments that get passed directly to the ansible-playbook command.
|
||||||
|
// #HPA_DEFAULT_PLAYBOOK_ARGS="--vault-id=consults@$SCRIPTS/vault-gopass-client"
|
||||||
|
//
|
||||||
|
// """
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// extension [String: String] {
|
||||||
|
// fileprivate func value(for codingKey: Configuration.CodingKeys) -> String? {
|
||||||
|
// self[codingKey.rawValue]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fileprivate func array(for codingKey: Configuration.CodingKeys) -> [String]? {
|
||||||
|
// value(for: codingKey).flatMap { $0.split(separator: ",").map(String.init) }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
212
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
212
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import CodersClient
|
||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import FileClient
|
||||||
|
import Foundation
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
var configuration: ConfigurationClient {
|
||||||
|
get { self[ConfigurationClient.self] }
|
||||||
|
set { self[ConfigurationClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct ConfigurationClient: Sendable {
|
||||||
|
public var find: @Sendable () async throws -> File
|
||||||
|
var generate: @Sendable (File, Bool) async throws -> Void
|
||||||
|
public var load: @Sendable (File?) async throws -> Configuration
|
||||||
|
var write: @Sendable (File, Configuration, Bool) async throws -> Void
|
||||||
|
|
||||||
|
public func findAndLoad() async throws -> Configuration {
|
||||||
|
let file = try? await find()
|
||||||
|
return try await load(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func generate(
|
||||||
|
at file: File,
|
||||||
|
force: Bool = false
|
||||||
|
) async throws {
|
||||||
|
try await generate(file, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func write(
|
||||||
|
_ configuration: Configuration,
|
||||||
|
to file: File,
|
||||||
|
force: Bool = false
|
||||||
|
) async throws {
|
||||||
|
try await write(file, configuration, force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConfigurationClient: DependencyKey {
|
||||||
|
public static var testValue: Self { Self() }
|
||||||
|
|
||||||
|
public static func live(environment: [String: String]) -> Self {
|
||||||
|
let liveClient = LiveConfigurationClient(environment: environment)
|
||||||
|
return .init {
|
||||||
|
try await liveClient.find()
|
||||||
|
} generate: { file, force in
|
||||||
|
try await liveClient.generate(at: file, force: force)
|
||||||
|
} load: { file in
|
||||||
|
try await liveClient.load(file: file)
|
||||||
|
} write: { file, configuration, force in
|
||||||
|
try await liveClient.write(configuration, to: file, force: force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var liveValue: Self {
|
||||||
|
.live(environment: ProcessInfo.processInfo.environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveConfigurationClient {
|
||||||
|
|
||||||
|
private let environment: [String: String]
|
||||||
|
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
|
@Dependency(\.fileClient) var fileManager
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
let validFileNames = [
|
||||||
|
"config.json", "config.toml",
|
||||||
|
".hparc.json", ".hparc.toml"
|
||||||
|
]
|
||||||
|
|
||||||
|
init(environment: [String: String]) {
|
||||||
|
self.environment = environment
|
||||||
|
}
|
||||||
|
|
||||||
|
func find() async throws -> File {
|
||||||
|
logger.debug("Begin find configuration.")
|
||||||
|
|
||||||
|
if let pwd = environment["PWD"],
|
||||||
|
let file = await findInDirectory(pwd)
|
||||||
|
{
|
||||||
|
logger.debug("Found configuration in pwd: \(file.path)")
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
if let configHome = environment.hpaConfigHome,
|
||||||
|
let file = await findInDirectory(configHome)
|
||||||
|
{
|
||||||
|
logger.debug("Found configuration in config home env var: \(file.path)")
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
if let configFile = environment.hpaConfigFile,
|
||||||
|
isReadable(configFile),
|
||||||
|
let file = File(configFile)
|
||||||
|
{
|
||||||
|
logger.debug("Found configuration in config file env var: \(file.path)")
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
if let file = await findInDirectory(fileManager.homeDirectory()) {
|
||||||
|
logger.debug("Found configuration in home directory: \(file.path)")
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
if let file = await findInDirectory("\(environment.xdgConfigHome)/\(HPAKey.configDirName)") {
|
||||||
|
logger.debug("Found configuration in xdg config directory: \(file.path)")
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ConfigurationError.configurationNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(at file: File, force: Bool) async throws {
|
||||||
|
if !force {
|
||||||
|
guard !fileManager.fileExists(file.url) else {
|
||||||
|
throw ConfigurationError.fileExists(path: file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .toml = file {
|
||||||
|
// In the case of toml, we copy the internal resource that includes
|
||||||
|
// usage comments in the file.
|
||||||
|
guard let resourceFile = Bundle.module.url(
|
||||||
|
forResource: HPAKey.resourceFileName,
|
||||||
|
withExtension: HPAKey.resourceFileExtension
|
||||||
|
) else {
|
||||||
|
throw ConfigurationError.resourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
try await fileManager.copy(resourceFile, file.url)
|
||||||
|
} else {
|
||||||
|
// Json does not allow comments, so we write the mock configuration
|
||||||
|
// to the file path.
|
||||||
|
try await write(.mock, to: file, force: force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(file: File?) async throws -> Configuration {
|
||||||
|
guard let file else { return .init() }
|
||||||
|
let data = try await fileManager.load(file.url)
|
||||||
|
|
||||||
|
switch file {
|
||||||
|
case .json:
|
||||||
|
return try coders.jsonDecoder().decode(Configuration.self, from: data)
|
||||||
|
case .toml:
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
throw ConfigurationError.decodingError
|
||||||
|
}
|
||||||
|
return try coders.tomlDecoder().decode(Configuration.self, from: string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(
|
||||||
|
_ configuration: Configuration,
|
||||||
|
to file: File,
|
||||||
|
force: Bool
|
||||||
|
) async throws {
|
||||||
|
if !force {
|
||||||
|
guard !fileManager.fileExists(file.url) else {
|
||||||
|
throw ConfigurationError.fileExists(path: file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: Data
|
||||||
|
|
||||||
|
switch file {
|
||||||
|
case .json:
|
||||||
|
data = try coders.jsonEncoder().encode(configuration)
|
||||||
|
case .toml:
|
||||||
|
let string = try coders.tomlEncoder().encode(configuration)
|
||||||
|
data = Data(string.utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
try await fileManager.write(data, file.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findInDirectory(_ directory: URL) async -> File? {
|
||||||
|
for file in validFileNames {
|
||||||
|
let url = directory.appending(path: file)
|
||||||
|
if isReadable(url), let file = File(url) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findInDirectory(_ directory: String) async -> File? {
|
||||||
|
await findInDirectory(URL(filePath: directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isReadable(_ file: URL) -> Bool {
|
||||||
|
FileManager.default.isReadableFile(atPath: file.cleanFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isReadable(_ file: String) -> Bool {
|
||||||
|
isReadable(URL(filePath: file))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConfigurationError: Error {
|
||||||
|
case configurationNotFound
|
||||||
|
case resourceNotFound
|
||||||
|
case decodingError
|
||||||
|
case fileExists(path: String)
|
||||||
|
}
|
||||||
28
Sources/ConfigurationClient/Constants.swift
Normal file
28
Sources/ConfigurationClient/Constants.swift
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@_spi(Internal)
|
||||||
|
public enum EnvironmentKey {
|
||||||
|
static let xdgConfigHome = "XDG_CONFIG_HOME"
|
||||||
|
static let hpaConfigHome = "HPA_CONFIG_HOME"
|
||||||
|
static let hpaConfigFile = "HPA_CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public enum HPAKey {
|
||||||
|
public static let configDirName = "hpa"
|
||||||
|
public static let resourceFileName = "hpa"
|
||||||
|
public static let resourceFileExtension = "toml"
|
||||||
|
public static let defaultFileName = "config.toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
extension [String: String] {
|
||||||
|
var xdgConfigHome: String {
|
||||||
|
self[EnvironmentKey.xdgConfigHome] ?? "~/.config"
|
||||||
|
}
|
||||||
|
|
||||||
|
var hpaConfigHome: String? {
|
||||||
|
self[EnvironmentKey.hpaConfigHome]
|
||||||
|
}
|
||||||
|
|
||||||
|
var hpaConfigFile: String? {
|
||||||
|
self[EnvironmentKey.hpaConfigFile]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Sources/ConfigurationClient/File.swift
Normal file
53
Sources/ConfigurationClient/File.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a file location and type on disk for a configuration file.
|
||||||
|
public enum File: Equatable, Sendable {
|
||||||
|
|
||||||
|
case json(URL)
|
||||||
|
case toml(URL)
|
||||||
|
|
||||||
|
public init?(_ url: URL) {
|
||||||
|
if url.cleanFilePath.hasSuffix("json") {
|
||||||
|
self = .json(url)
|
||||||
|
} else if url.cleanFilePath.hasSuffix("toml") {
|
||||||
|
self = .toml(url)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(_ path: String) {
|
||||||
|
self.init(URL(filePath: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func json(_ path: String) -> Self {
|
||||||
|
.json(URL(filePath: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func toml(_ path: String) -> Self {
|
||||||
|
.toml(URL(filePath: path))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var url: URL {
|
||||||
|
switch self {
|
||||||
|
case let .json(url): return url
|
||||||
|
case let .toml(url): return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var path: String {
|
||||||
|
url.cleanFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var `default`: Self {
|
||||||
|
.toml("~/.config/\(HPAKey.configDirName)/\(HPAKey.defaultFileName)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_spi(Internal)
|
||||||
|
public extension URL {
|
||||||
|
|
||||||
|
var cleanFilePath: String {
|
||||||
|
absoluteString.replacing("file://", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Sources/FileClient/FileClient.swift
Normal file
63
Sources/FileClient/FileClient.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesMacros
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension DependencyValues {
|
||||||
|
var fileClient: FileClient {
|
||||||
|
get { self[FileClient.self] }
|
||||||
|
set { self[FileClient.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyClient
|
||||||
|
public struct FileClient: Sendable {
|
||||||
|
public var copy: @Sendable (URL, URL) async throws -> Void
|
||||||
|
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
|
||||||
|
public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") }
|
||||||
|
public var load: @Sendable (URL) async throws -> Data
|
||||||
|
public var write: @Sendable (Data, URL) async throws -> Void
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileClient: DependencyKey {
|
||||||
|
public static let testValue: FileClient = Self()
|
||||||
|
|
||||||
|
public static var liveValue: Self {
|
||||||
|
let manager = LiveFileClient()
|
||||||
|
return .init {
|
||||||
|
try await manager.copy($0, to: $1)
|
||||||
|
} fileExists: { url in
|
||||||
|
manager.fileExists(at: url)
|
||||||
|
} homeDirectory: {
|
||||||
|
manager.homeDirectory()
|
||||||
|
} load: { url in
|
||||||
|
try await manager.load(from: url)
|
||||||
|
} write: { data, url in
|
||||||
|
try await manager.write(data, to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LiveFileClient: Sendable {
|
||||||
|
|
||||||
|
private var manager: FileManager { FileManager.default }
|
||||||
|
|
||||||
|
func copy(_ url: URL, to toUrl: URL) async throws {
|
||||||
|
try manager.copyItem(at: url, to: toUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileExists(at url: URL) -> Bool {
|
||||||
|
manager.fileExists(atPath: url.absoluteString.replacing("file://", with: ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func homeDirectory() -> URL {
|
||||||
|
manager.homeDirectoryForCurrentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(from url: URL) async throws -> Data {
|
||||||
|
try Data(contentsOf: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(_ data: Data, to url: URL) async throws {
|
||||||
|
try data.write(to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Sources/TestSupport/TestSupport.swift
Normal file
130
Sources/TestSupport/TestSupport.swift
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
@_exported import Dependencies
|
||||||
|
@_exported import ShellClient
|
||||||
|
|
||||||
|
public protocol TestCase {}
|
||||||
|
|
||||||
|
public extension TestCase {
|
||||||
|
func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
operation: @escaping @Sendable () throws -> Void
|
||||||
|
) rethrows {
|
||||||
|
try TestSupport.withTestLogger(
|
||||||
|
key: key,
|
||||||
|
label: "\(Self.self)",
|
||||||
|
logLevel: logLevel
|
||||||
|
) {
|
||||||
|
try operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void,
|
||||||
|
operation: @escaping @Sendable () throws -> Void
|
||||||
|
) rethrows {
|
||||||
|
try TestSupport.withTestLogger(
|
||||||
|
key: key,
|
||||||
|
label: "\(Self.self)",
|
||||||
|
logLevel: logLevel,
|
||||||
|
dependencies: setupDependencies
|
||||||
|
) {
|
||||||
|
try operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
operation: @escaping @Sendable () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await TestSupport.withTestLogger(
|
||||||
|
key: key,
|
||||||
|
label: "\(Self.self)",
|
||||||
|
logLevel: logLevel
|
||||||
|
) {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void,
|
||||||
|
operation: @escaping @Sendable () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await TestSupport.withTestLogger(
|
||||||
|
key: key,
|
||||||
|
label: "\(Self.self)",
|
||||||
|
logLevel: logLevel,
|
||||||
|
dependencies: setupDependencies
|
||||||
|
) {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
label: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
operation: @escaping @Sendable () throws -> Void
|
||||||
|
) rethrows {
|
||||||
|
try withDependencies {
|
||||||
|
$0.logger = .init(label: label)
|
||||||
|
$0.logger[metadataKey: "test"] = "\(key)"
|
||||||
|
$0.logger.logLevel = logLevel
|
||||||
|
} operation: {
|
||||||
|
try operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
label: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void,
|
||||||
|
operation: @escaping @Sendable () throws -> Void
|
||||||
|
) rethrows {
|
||||||
|
try withDependencies {
|
||||||
|
$0.logger = .init(label: label)
|
||||||
|
$0.logger[metadataKey: "test"] = "\(key)"
|
||||||
|
$0.logger.logLevel = logLevel
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
label: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
operation: @escaping @Sendable () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger = .init(label: label)
|
||||||
|
$0.logger[metadataKey: "test"] = "\(key)"
|
||||||
|
$0.logger.logLevel = logLevel
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func withTestLogger(
|
||||||
|
key: String,
|
||||||
|
label: String,
|
||||||
|
logLevel: Logger.Level = .debug,
|
||||||
|
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void,
|
||||||
|
operation: @escaping @Sendable () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger = .init(label: label)
|
||||||
|
$0.logger[metadataKey: "test"] = "\(key)"
|
||||||
|
$0.logger.logLevel = logLevel
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
@@ -61,12 +62,14 @@ struct CreateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
private func _run() async throws {
|
private func _run() async throws {
|
||||||
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
@Dependency(\.configuration) var configurationClient
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
let encoder = cliClient.encoder()
|
let encoder = coders.jsonEncoder()
|
||||||
|
|
||||||
let configuration = try cliClient.loadConfiguration()
|
let configuration = try await configurationClient.findAndLoad()
|
||||||
|
|
||||||
logger.debug("Configuration: \(configuration)")
|
logger.debug("Configuration: \(configuration)")
|
||||||
|
|
||||||
@@ -102,9 +105,9 @@ private func parseOptions(
|
|||||||
logger: Logger,
|
logger: Logger,
|
||||||
encoder: JSONEncoder
|
encoder: JSONEncoder
|
||||||
) throws -> Data {
|
) throws -> Data {
|
||||||
let templateDir = command.templateDir ?? configuration.templateDir
|
let templateDir = command.templateDir ?? configuration.template.directory
|
||||||
let templateRepo = command.repo ?? configuration.templateRepo
|
let templateRepo = command.repo ?? configuration.template.url
|
||||||
let version = (command.branch ?? configuration.templateRepoVersion) ?? "main"
|
let version = (command.branch ?? configuration.template.version) ?? "main"
|
||||||
|
|
||||||
logger.debug("""
|
logger.debug("""
|
||||||
(\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo)))
|
(\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo)))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import CliClient
|
import CliClient
|
||||||
|
import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
import Logging
|
import Logging
|
||||||
@@ -15,11 +16,12 @@ func runPlaybook(
|
|||||||
) async throws {
|
) async throws {
|
||||||
try await withSetupLogger(commandName: commandName, globals: globals) {
|
try await withSetupLogger(commandName: commandName, globals: globals) {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
@Dependency(\.configuration) var configurationClient
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
logger.debug("Begin run playbook: \(globals)")
|
logger.debug("Begin run playbook: \(globals)")
|
||||||
|
|
||||||
let configuration = try cliClient.ensuredConfiguration(configuration)
|
let configuration = try await configurationClient.findAndLoad()
|
||||||
|
|
||||||
logger.debug("Configuration: \(configuration)")
|
logger.debug("Configuration: \(configuration)")
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ func runPlaybook(
|
|||||||
globals: globals,
|
globals: globals,
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
globalsKeyPath: \.playbookDir,
|
globalsKeyPath: \.playbookDir,
|
||||||
configurationKeyPath: \.playbookDir
|
configurationKeyPath: \.playbook?.directory
|
||||||
)
|
)
|
||||||
let playbook = "\(playbookDir)/\(Constants.playbookFileName)"
|
let playbook = "\(playbookDir)/\(Constants.playbookFileName)"
|
||||||
|
|
||||||
@@ -35,11 +37,11 @@ func runPlaybook(
|
|||||||
globals: globals,
|
globals: globals,
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
globalsKeyPath: \.inventoryPath,
|
globalsKeyPath: \.inventoryPath,
|
||||||
configurationKeyPath: \.inventoryPath
|
configurationKeyPath: \.playbook?.inventory
|
||||||
)) ?? "\(playbookDir)/\(Constants.inventoryFileName)"
|
)) ?? "\(playbookDir)/\(Constants.inventoryFileName)"
|
||||||
|
|
||||||
let defaultArgs = (configuration.defaultPlaybookArgs ?? [])
|
let defaultArgs = (configuration.args ?? [])
|
||||||
+ (configuration.defaultVaultArgs ?? [])
|
+ (configuration.useVaultArgs ? configuration.vault.args ?? [] : [])
|
||||||
|
|
||||||
try await cliClient.runCommand(
|
try await cliClient.runCommand(
|
||||||
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
||||||
@@ -70,15 +72,6 @@ func runPlaybook(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CliClient {
|
|
||||||
func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration {
|
|
||||||
guard let configuration else {
|
|
||||||
return try loadConfiguration()
|
|
||||||
}
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BasicGlobalOptions {
|
extension BasicGlobalOptions {
|
||||||
|
|
||||||
var shellOrDefault: ShellCommand.Shell {
|
var shellOrDefault: ShellCommand.Shell {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ConfigurationClient
|
||||||
import Dependencies
|
import Dependencies
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
@@ -8,11 +9,12 @@ func runVault(
|
|||||||
) async throws {
|
) async throws {
|
||||||
try await withSetupLogger(commandName: commandName, globals: options.globals) {
|
try await withSetupLogger(commandName: commandName, globals: options.globals) {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
@Dependency(\.configuration) var configurationClient
|
||||||
@Dependency(\.logger) var logger
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
logger.debug("Begin run vault: \(options)")
|
logger.debug("Begin run vault: \(options)")
|
||||||
|
|
||||||
let configuration = try cliClient.ensuredConfiguration(nil)
|
let configuration = try await configurationClient.findAndLoad()
|
||||||
logger.debug("Configuration: \(configuration)")
|
logger.debug("Configuration: \(configuration)")
|
||||||
|
|
||||||
let path: String
|
let path: String
|
||||||
@@ -24,7 +26,7 @@ func runVault(
|
|||||||
|
|
||||||
logger.debug("Vault path: \(path)")
|
logger.debug("Vault path: \(path)")
|
||||||
|
|
||||||
let defaultArgs = configuration.defaultVaultArgs ?? []
|
let defaultArgs = configuration.vault.args ?? []
|
||||||
|
|
||||||
var vaultArgs = ["ansible-vault"]
|
var vaultArgs = ["ansible-vault"]
|
||||||
+ args
|
+ args
|
||||||
@@ -34,7 +36,7 @@ func runVault(
|
|||||||
|
|
||||||
if args.contains("encrypt"),
|
if args.contains("encrypt"),
|
||||||
!vaultArgs.contains("--encrypt-vault-id"),
|
!vaultArgs.contains("--encrypt-vault-id"),
|
||||||
let id = configuration.defaultVaultEncryptId
|
let id = configuration.vault.encryptId
|
||||||
{
|
{
|
||||||
vaultArgs.append(contentsOf: ["--encrypt-vault-id", id])
|
vaultArgs.append(contentsOf: ["--encrypt-vault-id", id])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
try await _run()
|
try await _run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIX:
|
||||||
private func _run() async throws {
|
private func _run() async throws {
|
||||||
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
try await withSetupLogger(commandName: Self.commandName, globals: globals) {
|
||||||
@Dependency(\.cliClient) var cliClient
|
@Dependency(\.cliClient) var cliClient
|
||||||
@@ -66,7 +67,8 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
actualPath = "\(path)/config"
|
actualPath = "\(path)/config"
|
||||||
}
|
}
|
||||||
|
|
||||||
try cliClient.createConfiguration(actualPath, json)
|
fatalError()
|
||||||
|
// try cliClient.createConfiguration(actualPath, json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +1,124 @@
|
|||||||
@_spi(Internal) import CliClient
|
// @_spi(Internal) import CliClient
|
||||||
import Dependencies
|
// import Dependencies
|
||||||
import Foundation
|
// import Foundation
|
||||||
import ShellClient
|
// import ShellClient
|
||||||
import Testing
|
// import Testing
|
||||||
import TOMLKit
|
// import TOMLKit
|
||||||
|
//
|
||||||
@Suite("CliClientTests")
|
// @Suite("CliClientTests")
|
||||||
struct 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
|
// @Test
|
||||||
// func writeToml() throws {
|
// func testLiveFileClient() {
|
||||||
// let encoded: String = try TOMLEncoder().encode(Configuration2.mock)
|
// withTestLogger(key: "testFindConfigPaths", logLevel: .trace) {
|
||||||
// try encoded.write(to: URL(filePath: "hpa.toml"), atomically: true, encoding: .utf8)
|
// $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)
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
|
||||||
func withTestLogger(
|
|
||||||
key: String,
|
|
||||||
label: String = "CliClientTests",
|
|
||||||
logLevel: Logger.Level = .debug,
|
|
||||||
operation: @escaping @Sendable () throws -> Void
|
|
||||||
) rethrows {
|
|
||||||
try withDependencies {
|
|
||||||
$0.logger = .init(label: label)
|
|
||||||
$0.logger[metadataKey: "test"] = "\(key)"
|
|
||||||
$0.logger.logLevel = logLevel
|
|
||||||
} operation: {
|
|
||||||
try operation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withTestLogger(
|
|
||||||
key: String,
|
|
||||||
label: String = "CliClientTests",
|
|
||||||
logLevel: Logger.Level = .debug,
|
|
||||||
dependencies setupDependencies: @escaping (inout DependencyValues) -> Void,
|
|
||||||
operation: @escaping @Sendable () throws -> Void
|
|
||||||
) rethrows {
|
|
||||||
try withDependencies {
|
|
||||||
$0.logger = .init(label: label)
|
|
||||||
$0.logger[metadataKey: "test"] = "\(key)"
|
|
||||||
$0.logger.logLevel = logLevel
|
|
||||||
setupDependencies(&$0)
|
|
||||||
} operation: {
|
|
||||||
try operation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
190
Tests/ConfigurationClientTests/ConfigurationClientTests.swift
Normal file
190
Tests/ConfigurationClientTests/ConfigurationClientTests.swift
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@_spi(Internal) import ConfigurationClient
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
import TestSupport
|
||||||
|
|
||||||
|
@Suite("ConfigurationClientTests")
|
||||||
|
struct ConfigurationClientTests: TestCase {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func sanity() {
|
||||||
|
withTestLogger(key: "sanity") {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
logger.debug("Testing sanity.")
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["config.toml", "config.json"])
|
||||||
|
func generateConfigFile(fileName: String) async throws {
|
||||||
|
try await withTestLogger(key: "generateConfigFile") {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
let configuration = ConfigurationClient.liveValue
|
||||||
|
|
||||||
|
try await withTemporaryDirectory { tempDir in
|
||||||
|
let tempFile = tempDir.appending(path: fileName)
|
||||||
|
try await configuration.generate(at: File(tempFile)!, force: false)
|
||||||
|
#expect(FileManager.default.fileExists(atPath: tempFile.cleanFilePath))
|
||||||
|
#expect(fileClient.fileExists(tempFile))
|
||||||
|
|
||||||
|
// Ensure that we do not overwrite files if they exist.
|
||||||
|
do {
|
||||||
|
try await configuration.generate(at: File(tempFile)!, force: false)
|
||||||
|
#expect(Bool(false))
|
||||||
|
} catch {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["config.toml", "config.json", nil])
|
||||||
|
func loadConfigFile(fileName: String?) async throws {
|
||||||
|
try await withTestLogger(key: "generateConfigFile") {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
let configuration = ConfigurationClient.liveValue
|
||||||
|
|
||||||
|
guard let fileName else {
|
||||||
|
let loaded = try await configuration.load(nil)
|
||||||
|
#expect(loaded == .init())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try await withGeneratedConfigFile(named: fileName, client: configuration) { file in
|
||||||
|
let loaded = try await configuration.load(file)
|
||||||
|
#expect(loaded == .mock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["config.toml", "config.json", ".hparc.json", ".hparc.toml"])
|
||||||
|
func findConfiguration(fileName: String) async throws {
|
||||||
|
try await withTestLogger(key: "findConfiguration") {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
let client = ConfigurationClient.liveValue
|
||||||
|
|
||||||
|
try await withGeneratedConfigFile(named: fileName, client: client) { file in
|
||||||
|
for environment in generateFindEnvironments(file: file) {
|
||||||
|
if let home = environment["HOME"] {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.fileClient.homeDirectory = { URL(filePath: home) }
|
||||||
|
} operation: {
|
||||||
|
let configuration = ConfigurationClient.live(environment: environment)
|
||||||
|
let found = try await configuration.find()
|
||||||
|
#expect(found == file)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let configuration = ConfigurationClient.live(environment: environment)
|
||||||
|
let found = try await configuration.find()
|
||||||
|
#expect(found == file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["config.toml", "config.json", ".hparc.json", ".hparc.toml"])
|
||||||
|
func findXdgConfiguration(fileName: String) async throws {
|
||||||
|
try await withTestLogger(key: "findXdgConfiguration") {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
let client = ConfigurationClient.liveValue
|
||||||
|
|
||||||
|
try await withGeneratedXDGConfigFile(named: fileName, client: client) { file, xdgDir in
|
||||||
|
let environment = ["XDG_CONFIG_HOME": xdgDir.cleanFilePath]
|
||||||
|
let configuration = ConfigurationClient.live(environment: environment)
|
||||||
|
let found = try await configuration.find()
|
||||||
|
#expect(found == file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func testFailingFind() async {
|
||||||
|
await withTestLogger(key: "testFailingFind") {
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
} operation: {
|
||||||
|
await withTemporaryDirectory { tempDir in
|
||||||
|
let environment = [
|
||||||
|
"PWD": tempDir.cleanFilePath,
|
||||||
|
"HPA_CONFIG_HOME": tempDir.cleanFilePath
|
||||||
|
]
|
||||||
|
let configuration = ConfigurationClient.live(environment: environment)
|
||||||
|
do {
|
||||||
|
_ = try await configuration.find()
|
||||||
|
#expect(Bool(false))
|
||||||
|
} catch {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFindEnvironments(file: File) -> [[String: String]] {
|
||||||
|
let directory = file.url.deletingLastPathComponent().cleanFilePath
|
||||||
|
|
||||||
|
return [
|
||||||
|
["PWD": directory],
|
||||||
|
["HPA_CONFIG_HOME": directory],
|
||||||
|
["HPA_CONFIG_FILE": file.path],
|
||||||
|
["HOME": directory]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable force_try
|
||||||
|
func withTemporaryDirectory(
|
||||||
|
_ operation: @Sendable (URL) async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
let dir = FileManager.default.temporaryDirectory
|
||||||
|
let tempDir = dir.appending(path: UUID().uuidString)
|
||||||
|
|
||||||
|
try! FileManager.default.createDirectory(
|
||||||
|
atPath: tempDir.cleanFilePath,
|
||||||
|
withIntermediateDirectories: false
|
||||||
|
)
|
||||||
|
try await operation(tempDir)
|
||||||
|
try! FileManager.default.removeItem(at: tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable force_try
|
||||||
|
|
||||||
|
func withGeneratedConfigFile(
|
||||||
|
named fileName: String,
|
||||||
|
client: ConfigurationClient,
|
||||||
|
_ operation: @Sendable (File) async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withTemporaryDirectory { tempDir in
|
||||||
|
let file = File(tempDir.appending(path: fileName))!
|
||||||
|
try await client.generate(at: file)
|
||||||
|
try await operation(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withGeneratedXDGConfigFile(
|
||||||
|
named fileName: String,
|
||||||
|
client: ConfigurationClient,
|
||||||
|
_ operation: @Sendable (File, URL) async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withTemporaryDirectory { tempDir in
|
||||||
|
let xdgDir = tempDir.appending(path: HPAKey.configDirName)
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
atPath: xdgDir.cleanFilePath,
|
||||||
|
withIntermediateDirectories: false
|
||||||
|
)
|
||||||
|
let file = File(xdgDir.appending(path: fileName))!
|
||||||
|
try await client.generate(at: file)
|
||||||
|
try await operation(file, tempDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user