feat: Adds generate-config as json file.

This commit is contained in:
2024-11-29 22:25:50 -05:00
parent 505f7d8013
commit ed3f752694
6 changed files with 97 additions and 36 deletions

View File

@@ -16,7 +16,7 @@ public struct CliClient: Sendable {
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 createConfiguration: @Sendable (String) throws -> Void
public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void
public func runCommand(
quiet: Bool,
@@ -50,16 +50,14 @@ extension CliClient: DependencyKey {
} encoder: {
encoder
} loadConfiguration: {
let urls = try findConfigurationFiles(env: env)
let url = try findConfigurationFiles(env: env)
var env = env
logger.trace("Loading configuration from: \(urls)")
for url in urls {
try fileClient.loadFile(url, &env, decoder)
}
logger.trace("Loading configuration from: \(url)")
try fileClient.loadFile(url, &env, decoder)
return try .fromEnv(env, encoder: encoder, decoder: decoder)
} runCommand: { args, quiet, shell in
@Dependency(\.asyncShellClient) var shellClient
if !quiet {
@@ -76,8 +74,27 @@ extension CliClient: DependencyKey {
in: nil,
args
))
} createConfiguration: { path in
try fileClient.write(path, Data(Configuration.fileTemplate.utf8))
} 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)
}
}
@@ -87,3 +104,13 @@ extension CliClient: DependencyKey {
public static let testValue: CliClient = Self()
}
enum CliClientError: Error {
case fileExistsAtPath(String)
}
private let jsonEncoder: JSONEncoder = {
var encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
return encoder
}()

View File

@@ -3,7 +3,7 @@ import Foundation
import ShellClient
/// Represents the configuration.
public struct Configuration: Decodable {
public struct Configuration: Codable {
public let playbookDir: String?
public let inventoryPath: String?
@@ -41,8 +41,21 @@ public struct Configuration: Decodable {
return try decoder.decode(Configuration.self, from: data)
}
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: "--vault-id=myId@$SCRIPTS/vault-gopass-client"
)
}
static var fileTemplate: String {
"""
# vi: ft=sh
# Example configuration, uncomment the lines and set the values appropriate for your
# usage.

View File

@@ -18,6 +18,7 @@ public struct FileClient: Sendable {
public var homeDir: @Sendable () -> URL = { URL(string: "~/")! }
public var isDirectory: @Sendable (URL) -> Bool = { _ in false }
public var isReadable: @Sendable (URL) -> Bool = { _ in false }
public var fileExists: @Sendable (String) -> Bool = { _ in false }
public var write: @Sendable (String, Data) throws -> Void
}
@@ -33,6 +34,7 @@ extension FileClient: DependencyKey {
homeDir: { client.homeDir },
isDirectory: { client.isDirectory(url: $0) },
isReadable: { client.isReadable(url: $0) },
fileExists: { client.fileExists(at: $0) },
write: { path, data in
try data.write(to: URL(filePath: path))
}
@@ -66,6 +68,10 @@ private struct LiveFileClient: @unchecked Sendable {
try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
}
func fileExists(at path: String) -> Bool {
fileManager.fileExists(atPath: path)
}
func loadFile(at url: URL, into env: inout [String: String], decoder: JSONDecoder) throws {
@Dependency(\.logger) var logger
logger.trace("Begin load file for: \(path(for: url))")

View File

@@ -5,7 +5,7 @@ import ShellClient
@_spi(Internal)
public func findConfigurationFiles(
env: [String: String] = ProcessInfo.processInfo.environment
) throws -> [URL] {
) throws -> URL {
@Dependency(\.logger) var logger
@Dependency(\.fileClient) var fileClient
@@ -13,50 +13,47 @@ public func findConfigurationFiles(
logger.trace("Env: \(env)")
let homeDir = fileClient.homeDir()
var url = homeDir.appending(path: ".hparc")
if fileClient.isReadable(url) {
logger.debug("Found configuration in home directory")
return [url]
}
// Check for environment variable pointing to a directory that the
// the configuration lives.
if let configHome = env["HPA_CONFIG_HOME"] {
url = .init(filePath: configHome)
let url = URL(filePath: configHome).appending(path: "config")
if fileClient.isDirectory(url) {
logger.debug("Found configuration directory from hpa config home env var.")
return try fileClient.contentsOfDirectory(url)
}
if fileClient.isReadable(url) {
logger.debug("Found configuration from hpa config home env var.")
return [url]
return url
}
}
// Check home directory for a `.hparc` file.
let url = homeDir.appending(path: ".hparc")
if fileClient.isReadable(url) {
logger.debug("Found configuration in home directory")
return url
}
// Check for environment variable pointing to a directory that the
// the configuration lives.
if let pwd = env["PWD"] {
url = .init(filePath: "\(pwd)").appending(path: ".hparc")
let url = URL(filePath: "\(pwd)").appending(path: ".hparc")
if fileClient.isReadable(url) {
logger.debug("Found configuration in current working directory.")
return [url]
return url
}
}
// Check in xdg config home, under an hpa-playbook directory.
if let xdgConfigHome = env["XDG_CONFIG_HOME"] {
logger.debug("XDG Config Home: \(xdgConfigHome)")
url = .init(filePath: "\(xdgConfigHome)")
let url = URL(filePath: "\(xdgConfigHome)")
.appending(path: "hpa-playbook")
.appending(path: "config")
logger.debug("XDG Config url: \(url.absoluteString)")
if fileClient.isDirectory(url) {
logger.debug("Found configuration in xdg config home.")
return try fileClient.contentsOfDirectory(url)
}
if fileClient.isReadable(url) {
logger.debug("Not directory, but readable.")
return [url]
return url
}
}

View File

@@ -11,8 +11,14 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
abstract: "\("Generate a local configuration file.".blue)",
discussion: """
If a directory is not supplied then a configuration file will be created
at \("'~/.config/hpa-playbook/config'".yellow).
\("NOTE:".yellow) If a directory is not supplied then a configuration file will be created
at \("'~/.config/hpa-playbook/config'".yellow).
\("Example:".yellow)
\("Create a directory and generate the configuration file.".green)
$ mkdir -p ~/.config/hpa-playbook
$ hpa generate-config --path ~/.config/hpa-playbook
"""
)
@@ -24,6 +30,12 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
)
var path: String?
@Flag(
name: .shortAndLong,
help: "Generate a json file, instead of default env style"
)
var json: Bool = false
@OptionGroup var globals: BasicGlobalOptions
mutating func run() async throws {
@@ -48,7 +60,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
actualPath = "\(path)/config"
}
try cliClient.createConfiguration(actualPath)
try cliClient.createConfiguration(actualPath, json)
}
}
}

View File

@@ -22,6 +22,7 @@ struct BasicGlobalOptions: ParsableArguments {
}
@dynamicMemberLookup
struct GlobalOptions: ParsableArguments {
@Option(
@@ -44,4 +45,9 @@ struct GlobalOptions: ParsableArguments {
@OptionGroup var basic: BasicGlobalOptions
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
get { basic[keyPath: keyPath] }
set { basic[keyPath: keyPath] = newValue }
}
}