feat: Adds generate-config as json file.
This commit is contained in:
@@ -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)")
|
||||||
|
try fileClient.loadFile(url, &env, decoder)
|
||||||
for url in urls {
|
|
||||||
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
|
||||||
|
}()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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))")
|
||||||
|
|||||||
@@ -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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user