feat: Begins vault commands, adds nodes for rendering discussion documentation.
This commit is contained in:
@@ -17,6 +17,7 @@ public struct CliClient: Sendable {
|
|||||||
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 (_ path: String, _ json: Bool) throws -> Void
|
public var createConfiguration: @Sendable (_ path: String, _ json: Bool) throws -> Void
|
||||||
|
public var findVaultFileInCurrentDirectory: @Sendable () throws -> String
|
||||||
|
|
||||||
public func runCommand(
|
public func runCommand(
|
||||||
quiet: Bool,
|
quiet: Bool,
|
||||||
@@ -95,6 +96,11 @@ extension CliClient: DependencyKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try fileClient.write(path, data)
|
try fileClient.write(path, data)
|
||||||
|
} findVaultFileInCurrentDirectory: {
|
||||||
|
guard let url = try fileClient.findVaultFileInCurrentDirectory() else {
|
||||||
|
throw CliClientError.vaultFileNotFound
|
||||||
|
}
|
||||||
|
return path(for: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +113,7 @@ extension CliClient: DependencyKey {
|
|||||||
|
|
||||||
enum CliClientError: Error {
|
enum CliClientError: Error {
|
||||||
case fileExistsAtPath(String)
|
case fileExistsAtPath(String)
|
||||||
|
case vaultFileNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
private let jsonEncoder: JSONEncoder = {
|
private let jsonEncoder: JSONEncoder = {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Foundation
|
|||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
/// Represents the configuration.
|
/// Represents the configuration.
|
||||||
public struct Configuration: Codable {
|
public struct Configuration: Codable, Sendable {
|
||||||
|
|
||||||
public let playbookDir: String?
|
public let playbookDir: String?
|
||||||
public let inventoryPath: String?
|
public let inventoryPath: String?
|
||||||
@@ -11,6 +11,7 @@ public struct Configuration: Codable {
|
|||||||
public let templateRepoVersion: String?
|
public let templateRepoVersion: String?
|
||||||
public let templateDir: String?
|
public let templateDir: String?
|
||||||
public let defaultPlaybookArgs: [String]?
|
public let defaultPlaybookArgs: [String]?
|
||||||
|
public let defaultVaultArgs: [String]?
|
||||||
|
|
||||||
fileprivate enum CodingKeys: String, CodingKey {
|
fileprivate enum CodingKeys: String, CodingKey {
|
||||||
case playbookDir = "HPA_PLAYBOOK_DIR"
|
case playbookDir = "HPA_PLAYBOOK_DIR"
|
||||||
@@ -19,6 +20,7 @@ public struct Configuration: Codable {
|
|||||||
case templateRepoVersion = "HPA_TEMPLATE_VERSION"
|
case templateRepoVersion = "HPA_TEMPLATE_VERSION"
|
||||||
case templateDir = "HPA_TEMPLATE_DIR"
|
case templateDir = "HPA_TEMPLATE_DIR"
|
||||||
case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS"
|
case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS"
|
||||||
|
case defaultVaultArgs = "HPA_DEFAULT_VAULT_ARGS"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func fromEnv(
|
public static func fromEnv(
|
||||||
@@ -31,16 +33,14 @@ public struct Configuration: Codable {
|
|||||||
let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") }
|
let hpaValues: [String: String] = env.filter { $0.key.contains("HPA") }
|
||||||
logger.debug("HPA env vars: \(hpaValues)")
|
logger.debug("HPA env vars: \(hpaValues)")
|
||||||
|
|
||||||
let defaultArgs: [String]? = hpaValues.value(key: .defaultPlaybookArgs)
|
|
||||||
.flatMap { $0.split(separator: ",").map(String.init) }
|
|
||||||
|
|
||||||
return Configuration(
|
return Configuration(
|
||||||
playbookDir: hpaValues.value(key: .playbookDir),
|
playbookDir: hpaValues.value(for: .playbookDir),
|
||||||
inventoryPath: hpaValues.value(key: .inventoryPath),
|
inventoryPath: hpaValues.value(for: .inventoryPath),
|
||||||
templateRepo: hpaValues.value(key: .templateRepo),
|
templateRepo: hpaValues.value(for: .templateRepo),
|
||||||
templateRepoVersion: hpaValues.value(key: .templateRepoVersion),
|
templateRepoVersion: hpaValues.value(for: .templateRepoVersion),
|
||||||
templateDir: hpaValues.value(key: .templateDir),
|
templateDir: hpaValues.value(for: .templateDir),
|
||||||
defaultPlaybookArgs: defaultArgs
|
defaultPlaybookArgs: hpaValues.array(for: .defaultPlaybookArgs),
|
||||||
|
defaultVaultArgs: hpaValues.array(for: .defaultVaultArgs)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,8 @@ public struct Configuration: Codable {
|
|||||||
templateRepo: "https://git.example.com/consult-template.git",
|
templateRepo: "https://git.example.com/consult-template.git",
|
||||||
templateRepoVersion: "main",
|
templateRepoVersion: "main",
|
||||||
templateDir: "/path/to/local/template",
|
templateDir: "/path/to/local/template",
|
||||||
defaultPlaybookArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"]
|
defaultPlaybookArgs: ["--tags", "debug"],
|
||||||
|
defaultVaultArgs: ["--vault-id=myId@$SCRIPTS/vault-gopass-client"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +83,11 @@ public struct Configuration: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension [String: String] {
|
extension [String: String] {
|
||||||
fileprivate func value(key codingKey: Configuration.CodingKeys) -> String? {
|
fileprivate func value(for codingKey: Configuration.CodingKeys) -> String? {
|
||||||
self[codingKey.rawValue]
|
self[codingKey.rawValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func array(for codingKey: Configuration.CodingKeys) -> [String]? {
|
||||||
|
value(for: codingKey).flatMap { $0.split(separator: ",").map(String.init) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public struct FileClient: Sendable {
|
|||||||
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 fileExists: @Sendable (String) -> Bool = { _ in false }
|
||||||
|
public var findVaultFileInCurrentDirectory: @Sendable () throws -> URL?
|
||||||
public var write: @Sendable (String, Data) throws -> Void
|
public var write: @Sendable (String, Data) throws -> Void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ extension FileClient: DependencyKey {
|
|||||||
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) },
|
fileExists: { client.fileExists(at: $0) },
|
||||||
|
findVaultFileInCurrentDirectory: { try client.findVaultFileInCurrentDirectory() },
|
||||||
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,31 @@ private struct LiveFileClient: @unchecked Sendable {
|
|||||||
fileManager.fileExists(atPath: path)
|
fileManager.fileExists(atPath: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findVaultFileInCurrentDirectory() throws -> URL? {
|
||||||
|
let urls = try fileManager.contentsOfDirectory(at: URL(filePath: "."), includingPropertiesForKeys: nil)
|
||||||
|
|
||||||
|
if let vault = urls.firstVaultFile {
|
||||||
|
return vault
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for folders that end with "vars" and search those next.
|
||||||
|
for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) {
|
||||||
|
let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
||||||
|
if let vault = files.firstVaultFile {
|
||||||
|
return vault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if let vault = files.firstVaultFile {
|
||||||
|
return vault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
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))")
|
||||||
@@ -101,3 +128,9 @@ private struct LiveFileClient: @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension Array where Element == URL {
|
||||||
|
var firstVaultFile: URL? {
|
||||||
|
first { $0.absoluteString.hasSuffix("vault.yml") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import ShellClient
|
|||||||
struct Application: AsyncParsableCommand {
|
struct Application: AsyncParsableCommand {
|
||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: "hpa",
|
commandName: Constants.appName,
|
||||||
abstract: "\("A utility for working with ansible hpa playbook.".blue)",
|
abstract: createAbstract("A utility for working with ansible hpa playbook."),
|
||||||
subcommands: [
|
subcommands: [
|
||||||
BuildCommand.self, CreateCommand.self, UtilsCommand.self
|
BuildCommand.self, CreateCommand.self, VaultCommand.self, UtilsCommand.self
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ struct BuildCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let commandName = "build"
|
static let commandName = "build"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration.playbookCommandConfiguration(
|
static let configuration = CommandConfiguration.playbook(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Build a home performance assesment project.",
|
abstract: "Build a home performance assesment project.",
|
||||||
examples: (label: "Build Project", example: "\(commandName) /path/to/project")
|
examples: (label: "Build Project", example: "\(commandName) /path/to/project")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let commandName = "create"
|
static let commandName = "create"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration.playbookCommandConfiguration(
|
static let configuration = CommandConfiguration.playbook(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Create a home performance assesment project.",
|
abstract: "Create a home performance assesment project.",
|
||||||
examples: (
|
examples: (
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
|
|
||||||
extension CommandConfiguration {
|
|
||||||
static func playbookCommandConfiguration(
|
|
||||||
commandName: String,
|
|
||||||
abstract: String,
|
|
||||||
examples: (label: String, example: String)...
|
|
||||||
) -> Self {
|
|
||||||
guard examples.count > 0 else {
|
|
||||||
fatalError("Did not supply any examples.")
|
|
||||||
}
|
|
||||||
return Self(
|
|
||||||
commandName: commandName,
|
|
||||||
abstract: "\(abstract.blue)",
|
|
||||||
discussion: """
|
|
||||||
\("NOTE:".yellow) Most options are not required if you have a configuration file setup.
|
|
||||||
|
|
||||||
\("Examples:".yellow)
|
|
||||||
|
|
||||||
\(examples.map { "\($0.label.green.italic)\n $ hpa \($0.example)" }.joined(separator: "\n"))
|
|
||||||
|
|
||||||
\("Passing extra args to the playbook.".green.italic)
|
|
||||||
$ hpa \(examples[0].example) -- --vault-id "myId@$SCRIPTS/vault-gopass-client"
|
|
||||||
|
|
||||||
\("See Also:".yellow) \("Ansible playbook options.".italic)
|
|
||||||
|
|
||||||
$ ansible-playbook --help
|
|
||||||
|
|
||||||
\("IMPORTANT NOTE:".red) Any extra arguments to pass to the playbook invocation have to
|
|
||||||
be at the end with `--` before any arguments otherwise there will
|
|
||||||
be an "Unkown option" error. See examples above.
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
108
Sources/hpa/Internal/CommandConfigurationExtensions.swift
Normal file
108
Sources/hpa/Internal/CommandConfigurationExtensions.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
import Rainbow
|
||||||
|
|
||||||
|
extension CommandConfiguration {
|
||||||
|
|
||||||
|
static func create(
|
||||||
|
commandName: String,
|
||||||
|
abstract: String,
|
||||||
|
usesExtraArgs: Bool = true,
|
||||||
|
discussion nodes: [Node]
|
||||||
|
) -> Self {
|
||||||
|
.init(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: createAbstract(abstract),
|
||||||
|
discussion: Discussion(nodes: nodes, usesExtraArgs: usesExtraArgs).render()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func playbook(
|
||||||
|
commandName: String,
|
||||||
|
abstract: String,
|
||||||
|
parentCommand: String? = nil,
|
||||||
|
examples: (label: String, example: String)...
|
||||||
|
) -> Self {
|
||||||
|
.create(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: abstract,
|
||||||
|
discussion: [.note(label: "Most options are not required if you have a configuration file setup.")]
|
||||||
|
+ examples.nodes(parentCommand)
|
||||||
|
+ [.seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAbstract(_ string: String) -> String {
|
||||||
|
"\(string.blue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Discussion {
|
||||||
|
var nodes: [Node]
|
||||||
|
|
||||||
|
init(usesExtraArgs: Bool = true, _ nodes: Node...) {
|
||||||
|
self.init(nodes: nodes, usesExtraArgs: usesExtraArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(nodes: [Node], usesExtraArgs: Bool = true) {
|
||||||
|
var nodes = nodes
|
||||||
|
|
||||||
|
if let firstExampleIndex = nodes.firstIndex(where: \.isExampleNode) {
|
||||||
|
nodes.insert(.exampleHeading, at: firstExampleIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usesExtraArgs {
|
||||||
|
if let lastExampleIndex = nodes.lastIndex(where: \.isExampleNode) {
|
||||||
|
let last = nodes[lastExampleIndex]
|
||||||
|
if case let .example(_, example, parent) = last {
|
||||||
|
nodes.insert(
|
||||||
|
.example(label: "Passing extra args.", example: example, parentCommand: parent),
|
||||||
|
at: nodes.index(after: lastExampleIndex)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.nodes = nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
nodes.map { $0.render() }.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Node: Equatable {
|
||||||
|
case note(header: String = "NOTE:", label: String, appendNewLine: Bool = true)
|
||||||
|
case example(label: String, example: String, parentCommand: String?)
|
||||||
|
case seeAlso(label: String, command: String, appendHelpToCommand: Bool = true)
|
||||||
|
|
||||||
|
static var exampleHeading: Self { .note(header: "Examples:", label: "Some common examples.", appendNewLine: false) }
|
||||||
|
|
||||||
|
var isExampleNode: Bool {
|
||||||
|
if case .example = self { return true }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func render() -> String {
|
||||||
|
switch self {
|
||||||
|
case let .note(header, note, appendNewLine):
|
||||||
|
return "\(header.yellow) \(note.italic)\(appendNewLine ? "\n" : "")"
|
||||||
|
case let .example(label: label, example: example, parentCommand: parent):
|
||||||
|
return """
|
||||||
|
\(label.green.italic)
|
||||||
|
$ \(Constants.appName) \(parent ?? "")\(parent != nil ? " \(example)" : example)
|
||||||
|
|
||||||
|
"""
|
||||||
|
case let .seeAlso(label: label, command: command, appendHelpToCommand: appendHelp):
|
||||||
|
return """
|
||||||
|
\("See Also:".yellow) \(label.italic)
|
||||||
|
$ \(command)\(appendHelp ? " --help" : "")
|
||||||
|
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Array where Element == (label: String, example: String) {
|
||||||
|
func nodes(_ parentCommand: String?) -> [Node] {
|
||||||
|
map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) }
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Sources/hpa/Internal/Constants.swift
Normal file
17
Sources/hpa/Internal/Constants.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Rainbow
|
||||||
|
|
||||||
|
// Constant string values.
|
||||||
|
enum Constants {
|
||||||
|
static let appName = "hpa"
|
||||||
|
static let playbookFileName = "main.yml"
|
||||||
|
static let inventoryFileName = "inventory.ini"
|
||||||
|
static let importantExtraArgsNote = """
|
||||||
|
|
||||||
|
\("IMPORTANT NOTE".bold.underline): \("""
|
||||||
|
Any extra arguments to pass to the underlying command invocation have to
|
||||||
|
be at the end with `--` before any arguments otherwise there will
|
||||||
|
be an "Unkown option" error. See examples above.
|
||||||
|
""".italic
|
||||||
|
)
|
||||||
|
""".red
|
||||||
|
}
|
||||||
@@ -50,4 +50,8 @@ struct GlobalOptions: ParsableArguments {
|
|||||||
set { basic[keyPath: keyPath] = newValue }
|
set { basic[keyPath: keyPath] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscript<T>(dynamicMember keyPath: KeyPath<BasicGlobalOptions, T>) -> T {
|
||||||
|
basic[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
53
Sources/hpa/Internal/LoggingExtensions.swift
Normal file
53
Sources/hpa/Internal/LoggingExtensions.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Dependencies
|
||||||
|
import Logging
|
||||||
|
|
||||||
|
extension Logger.Level {
|
||||||
|
|
||||||
|
/// Set the log level based on the user's options supplied.
|
||||||
|
init(globals: BasicGlobalOptions, quietOnlyPlaybook: Bool) {
|
||||||
|
if quietOnlyPlaybook || !globals.quiet {
|
||||||
|
switch globals.verbose {
|
||||||
|
case 0:
|
||||||
|
self = .info
|
||||||
|
case 1:
|
||||||
|
self = .debug
|
||||||
|
case 2...:
|
||||||
|
self = .trace
|
||||||
|
default:
|
||||||
|
self = .info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self = .info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withSetupLogger(
|
||||||
|
commandName: String,
|
||||||
|
globals: BasicGlobalOptions,
|
||||||
|
quietOnlyPlaybook: Bool = false,
|
||||||
|
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: @escaping () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.logger = .init(label: "\("hpa".yellow)")
|
||||||
|
$0.logger.logLevel = .init(globals: globals, quietOnlyPlaybook: quietOnlyPlaybook)
|
||||||
|
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withSetupLogger(
|
||||||
|
commandName: String,
|
||||||
|
globals: GlobalOptions,
|
||||||
|
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: @escaping () async throws -> Void
|
||||||
|
) async rethrows {
|
||||||
|
try await withSetupLogger(
|
||||||
|
commandName: commandName,
|
||||||
|
globals: globals.basic,
|
||||||
|
quietOnlyPlaybook: globals.quietOnlyPlaybook,
|
||||||
|
dependencies: setupDependencies,
|
||||||
|
operation: operation
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,101 +6,6 @@ import Logging
|
|||||||
import Rainbow
|
import Rainbow
|
||||||
import ShellClient
|
import ShellClient
|
||||||
|
|
||||||
extension BasicGlobalOptions {
|
|
||||||
|
|
||||||
var shellOrDefault: ShellCommand.Shell {
|
|
||||||
guard let shell else { return .zsh(useDashC: true) }
|
|
||||||
return .custom(path: shell, useDashC: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GlobalOptions {
|
|
||||||
|
|
||||||
var shellOrDefault: ShellCommand.Shell { basic.shellOrDefault }
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureString(
|
|
||||||
globals: GlobalOptions,
|
|
||||||
configuration: Configuration,
|
|
||||||
globalsKeyPath: KeyPath<GlobalOptions, String?>,
|
|
||||||
configurationKeyPath: KeyPath<Configuration, String?>
|
|
||||||
) throws -> String {
|
|
||||||
if let global = globals[keyPath: globalsKeyPath] {
|
|
||||||
return global
|
|
||||||
}
|
|
||||||
guard let configuration = configuration[keyPath: configurationKeyPath] else {
|
|
||||||
throw RunPlaybookError.playbookNotFound
|
|
||||||
}
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func withSetupLogger(
|
|
||||||
commandName: String,
|
|
||||||
globals: BasicGlobalOptions,
|
|
||||||
quietOnlyPlaybook: Bool = false,
|
|
||||||
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
|
||||||
operation: @escaping () async throws -> Void
|
|
||||||
) async rethrows {
|
|
||||||
try await withDependencies {
|
|
||||||
$0.logger = .init(label: "\("hpa".yellow)")
|
|
||||||
if quietOnlyPlaybook || !globals.quiet {
|
|
||||||
switch globals.verbose {
|
|
||||||
case 0:
|
|
||||||
$0.logger.logLevel = .info
|
|
||||||
case 1:
|
|
||||||
$0.logger.logLevel = .debug
|
|
||||||
case 2...:
|
|
||||||
$0.logger.logLevel = .trace
|
|
||||||
default:
|
|
||||||
$0.logger.logLevel = .info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
|
|
||||||
} operation: {
|
|
||||||
try await operation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withSetupLogger(
|
|
||||||
commandName: String,
|
|
||||||
globals: GlobalOptions,
|
|
||||||
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
|
||||||
operation: @escaping () async throws -> Void
|
|
||||||
) async rethrows {
|
|
||||||
try await withSetupLogger(
|
|
||||||
commandName: commandName,
|
|
||||||
globals: globals.basic,
|
|
||||||
quietOnlyPlaybook: globals.quietOnlyPlaybook,
|
|
||||||
dependencies: setupDependencies,
|
|
||||||
operation: operation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runPlaybook(
|
|
||||||
commandName: String,
|
|
||||||
globals: GlobalOptions,
|
|
||||||
configuration: Configuration? = nil,
|
|
||||||
extraArgs: [String],
|
|
||||||
_ args: String...
|
|
||||||
) async throws {
|
|
||||||
try await runPlaybook(
|
|
||||||
commandName: commandName,
|
|
||||||
globals: globals,
|
|
||||||
configuration: configuration,
|
|
||||||
extraArgs: extraArgs,
|
|
||||||
args
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension CliClient {
|
|
||||||
func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration {
|
|
||||||
guard let configuration else {
|
|
||||||
return try loadConfiguration()
|
|
||||||
}
|
|
||||||
return configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runPlaybook(
|
func runPlaybook(
|
||||||
commandName: String,
|
commandName: String,
|
||||||
globals: GlobalOptions,
|
globals: GlobalOptions,
|
||||||
@@ -124,19 +29,20 @@ func runPlaybook(
|
|||||||
globalsKeyPath: \.playbookDir,
|
globalsKeyPath: \.playbookDir,
|
||||||
configurationKeyPath: \.playbookDir
|
configurationKeyPath: \.playbookDir
|
||||||
)
|
)
|
||||||
let playbook = "\(playbookDir)/main.yml"
|
let playbook = "\(playbookDir)/\(Constants.playbookFileName)"
|
||||||
|
|
||||||
let inventory = (try? ensureString(
|
let inventory = (try? ensureString(
|
||||||
globals: globals,
|
globals: globals,
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
globalsKeyPath: \.inventoryPath,
|
globalsKeyPath: \.inventoryPath,
|
||||||
configurationKeyPath: \.inventoryPath
|
configurationKeyPath: \.inventoryPath
|
||||||
)) ?? "\(playbookDir)/inventory.ini"
|
)) ?? "\(playbookDir)/\(Constants.inventoryFileName)"
|
||||||
|
|
||||||
let defaultArgs = configuration.defaultPlaybookArgs ?? []
|
let defaultArgs = (configuration.defaultPlaybookArgs ?? [])
|
||||||
|
+ (configuration.defaultVaultArgs ?? [])
|
||||||
|
|
||||||
try await cliClient.runCommand(
|
try await cliClient.runCommand(
|
||||||
quiet: globals.quietOnlyPlaybook ? true : globals.basic.quiet,
|
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
||||||
shell: globals.shellOrDefault,
|
shell: globals.shellOrDefault,
|
||||||
[
|
[
|
||||||
"ansible-playbook", playbook,
|
"ansible-playbook", playbook,
|
||||||
@@ -148,6 +54,54 @@ func runPlaybook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runPlaybook(
|
||||||
|
commandName: String,
|
||||||
|
globals: GlobalOptions,
|
||||||
|
configuration: Configuration? = nil,
|
||||||
|
extraArgs: [String],
|
||||||
|
_ args: String...
|
||||||
|
) async throws {
|
||||||
|
try await runPlaybook(
|
||||||
|
commandName: commandName,
|
||||||
|
globals: globals,
|
||||||
|
configuration: configuration,
|
||||||
|
extraArgs: extraArgs,
|
||||||
|
args
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CliClient {
|
||||||
|
func ensuredConfiguration(_ configuration: Configuration?) throws -> Configuration {
|
||||||
|
guard let configuration else {
|
||||||
|
return try loadConfiguration()
|
||||||
|
}
|
||||||
|
return configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BasicGlobalOptions {
|
||||||
|
|
||||||
|
var shellOrDefault: ShellCommand.Shell {
|
||||||
|
guard let shell else { return .zsh(useDashC: true) }
|
||||||
|
return .custom(path: shell, useDashC: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureString(
|
||||||
|
globals: GlobalOptions,
|
||||||
|
configuration: Configuration,
|
||||||
|
globalsKeyPath: KeyPath<GlobalOptions, String?>,
|
||||||
|
configurationKeyPath: KeyPath<Configuration, String?>
|
||||||
|
) throws -> String {
|
||||||
|
if let global = globals[keyPath: globalsKeyPath] {
|
||||||
|
return global
|
||||||
|
}
|
||||||
|
guard let configuration = configuration[keyPath: configurationKeyPath] else {
|
||||||
|
throw RunPlaybookError.playbookNotFound
|
||||||
|
}
|
||||||
|
return configuration
|
||||||
|
}
|
||||||
|
|
||||||
enum RunPlaybookError: Error {
|
enum RunPlaybookError: Error {
|
||||||
case playbookNotFound
|
case playbookNotFound
|
||||||
case configurationError
|
case configurationError
|
||||||
41
Sources/hpa/Internal/RunVault.swift
Normal file
41
Sources/hpa/Internal/RunVault.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Dependencies
|
||||||
|
import ShellClient
|
||||||
|
|
||||||
|
func runVault(
|
||||||
|
commandName: String,
|
||||||
|
options: VaultOptions,
|
||||||
|
_ args: [String]
|
||||||
|
) async throws {
|
||||||
|
try await withSetupLogger(commandName: commandName, globals: options.globals) {
|
||||||
|
@Dependency(\.cliClient) var cliClient
|
||||||
|
@Dependency(\.logger) var logger
|
||||||
|
|
||||||
|
logger.debug("Begin run vault: \(options)")
|
||||||
|
|
||||||
|
let configuration = try cliClient.ensuredConfiguration(nil)
|
||||||
|
logger.debug("Configuration: \(configuration)")
|
||||||
|
|
||||||
|
let path: String
|
||||||
|
if let file = options.file {
|
||||||
|
path = file
|
||||||
|
} else {
|
||||||
|
path = try cliClient.findVaultFileInCurrentDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Vault path: \(path)")
|
||||||
|
|
||||||
|
let defaultArgs = configuration.defaultVaultArgs ?? []
|
||||||
|
|
||||||
|
try await cliClient.runCommand(
|
||||||
|
quiet: options.quiet,
|
||||||
|
shell: options.shellOrDefault,
|
||||||
|
["ansible-vault"]
|
||||||
|
+ args
|
||||||
|
+ defaultArgs
|
||||||
|
+ options.extraArgs
|
||||||
|
+ [path]
|
||||||
|
)
|
||||||
|
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ struct CreateProjectTemplateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let commandName = "create-template"
|
static let commandName = "create-template"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration.playbookCommandConfiguration(
|
static let configuration = CommandConfiguration.playbook(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Create a home performance assesment project template.",
|
abstract: "Create a home performance assesment project template.",
|
||||||
|
parentCommand: UtilsCommand.commandName,
|
||||||
examples: (label: "Create Template", example: "\(commandName) /path/to/project-template")
|
examples: (label: "Create Template", example: "\(commandName) /path/to/project-template")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "\("Generate a local configuration file.".blue)",
|
abstract: createAbstract("Generate a local configuration file."),
|
||||||
discussion: """
|
discussion: """
|
||||||
|
|
||||||
\("NOTE:".yellow) 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
|
||||||
|
|||||||
23
Sources/hpa/VaultCommands/DecryptCommand.swift
Normal file
23
Sources/hpa/VaultCommands/DecryptCommand.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
struct DecryptCommand: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let commandName = "decrypt"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: createAbstract("Decrypt a vault file.")
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var options: VaultOptions
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Output file."
|
||||||
|
)
|
||||||
|
var output: String?
|
||||||
|
|
||||||
|
mutating func run() async throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Sources/hpa/VaultCommands/EncryptCommand.swift
Normal file
23
Sources/hpa/VaultCommands/EncryptCommand.swift
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
struct EncryptCommand: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let commandName = "encrypt"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: createAbstract("Encrypt a vault file.")
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptionGroup var options: VaultOptions
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "Output file."
|
||||||
|
)
|
||||||
|
var output: String?
|
||||||
|
|
||||||
|
mutating func run() async throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Sources/hpa/VaultCommands/VaultCommand.swift
Normal file
14
Sources/hpa/VaultCommands/VaultCommand.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
struct VaultCommand: AsyncParsableCommand {
|
||||||
|
|
||||||
|
static let commandName = "vault"
|
||||||
|
|
||||||
|
static let configuration = CommandConfiguration(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: createAbstract("Vault commands."),
|
||||||
|
subcommands: [
|
||||||
|
EncryptCommand.self, DecryptCommand.self
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
31
Sources/hpa/VaultCommands/VaultOptions.swift
Normal file
31
Sources/hpa/VaultCommands/VaultOptions.swift
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
// Holds the common options for vault commands, as they all share the
|
||||||
|
// same structure.
|
||||||
|
@dynamicMemberLookup
|
||||||
|
struct VaultOptions: ParsableArguments {
|
||||||
|
|
||||||
|
@OptionGroup var globals: BasicGlobalOptions
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
name: .shortAndLong,
|
||||||
|
help: "The vault file path.",
|
||||||
|
completion: .file()
|
||||||
|
)
|
||||||
|
var file: String?
|
||||||
|
|
||||||
|
@Argument(
|
||||||
|
help: "Extra arguments to pass to the vault command."
|
||||||
|
)
|
||||||
|
var extraArgs: [String] = []
|
||||||
|
|
||||||
|
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
|
||||||
|
get { globals[keyPath: keyPath] }
|
||||||
|
set { globals[keyPath: keyPath] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript<T>(dynamicMember keyPath: KeyPath<BasicGlobalOptions, T>) -> T {
|
||||||
|
globals[keyPath: keyPath]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user