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 encoder: @Sendable () -> JSONEncoder = { .init() }
public var loadConfiguration: @Sendable () throws -> Configuration 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 (String) throws -> Void public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void
public func runCommand( public func runCommand(
quiet: Bool, quiet: Bool,
@@ -50,16 +50,14 @@ extension CliClient: DependencyKey {
} encoder: { } encoder: {
encoder encoder
} loadConfiguration: { } loadConfiguration: {
let urls = try findConfigurationFiles(env: env) let url = try findConfigurationFiles(env: env)
var env = env var env = env
logger.trace("Loading configuration from: \(urls)") logger.trace("Loading configuration from: \(url)")
for url in urls {
try fileClient.loadFile(url, &env, decoder) try fileClient.loadFile(url, &env, decoder)
}
return try .fromEnv(env, encoder: encoder, decoder: decoder) return try .fromEnv(env, encoder: encoder, decoder: decoder)
} runCommand: { args, quiet, shell in } runCommand: { args, quiet, shell in
@Dependency(\.asyncShellClient) var shellClient @Dependency(\.asyncShellClient) var shellClient
if !quiet { if !quiet {
@@ -76,8 +74,27 @@ extension CliClient: DependencyKey {
in: nil, in: nil,
args args
)) ))
} createConfiguration: { path in } createConfiguration: { path, json in
try fileClient.write(path, Data(Configuration.fileTemplate.utf8))
// 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() 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 import ShellClient
/// Represents the configuration. /// Represents the configuration.
public struct Configuration: Decodable { public struct Configuration: Codable {
public let playbookDir: String? public let playbookDir: String?
public let inventoryPath: String? public let inventoryPath: String?
@@ -41,8 +41,21 @@ public struct Configuration: Decodable {
return try decoder.decode(Configuration.self, from: data) 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 { static var fileTemplate: String {
""" """
# vi: ft=sh
# Example configuration, uncomment the lines and set the values appropriate for your # Example configuration, uncomment the lines and set the values appropriate for your
# usage. # usage.

View File

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

View File

@@ -5,7 +5,7 @@ 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 -> URL {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
@@ -13,50 +13,47 @@ public func findConfigurationFiles(
logger.trace("Env: \(env)") logger.trace("Env: \(env)")
let homeDir = fileClient.homeDir() 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"] { 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) { if fileClient.isReadable(url) {
logger.debug("Found configuration from hpa config home env var.") 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"] { if let pwd = env["PWD"] {
url = .init(filePath: "\(pwd)").appending(path: ".hparc") let url = URL(filePath: "\(pwd)").appending(path: ".hparc")
if fileClient.isReadable(url) { if fileClient.isReadable(url) {
logger.debug("Found configuration in current working directory.") 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"] { if let xdgConfigHome = env["XDG_CONFIG_HOME"] {
logger.debug("XDG Config Home: \(xdgConfigHome)") logger.debug("XDG Config Home: \(xdgConfigHome)")
url = .init(filePath: "\(xdgConfigHome)") let url = URL(filePath: "\(xdgConfigHome)")
.appending(path: "hpa-playbook") .appending(path: "hpa-playbook")
.appending(path: "config")
logger.debug("XDG Config url: \(url.absoluteString)") 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) { if fileClient.isReadable(url) {
logger.debug("Not directory, but readable.") return url
return [url]
} }
} }

View File

@@ -11,9 +11,15 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
abstract: "\("Generate a local configuration file.".blue)", abstract: "\("Generate a local configuration file.".blue)",
discussion: """ discussion: """
If a directory is not supplied then a configuration file will be created \("NOTE:".yellow) If a directory is not supplied then a configuration file will be created
at \("'~/.config/hpa-playbook/config'".yellow). 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? var path: String?
@Flag(
name: .shortAndLong,
help: "Generate a json file, instead of default env style"
)
var json: Bool = false
@OptionGroup var globals: BasicGlobalOptions @OptionGroup var globals: BasicGlobalOptions
mutating func run() async throws { mutating func run() async throws {
@@ -48,7 +60,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
actualPath = "\(path)/config" 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 { struct GlobalOptions: ParsableArguments {
@Option( @Option(
@@ -44,4 +45,9 @@ struct GlobalOptions: ParsableArguments {
@OptionGroup var basic: BasicGlobalOptions @OptionGroup var basic: BasicGlobalOptions
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
get { basic[keyPath: keyPath] }
set { basic[keyPath: keyPath] = newValue }
}
} }