feat: Begins vault commands, adds nodes for rendering discussion documentation.
This commit is contained in:
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
|
||||
}
|
||||
57
Sources/hpa/Internal/GlobalOptions.swift
Normal file
57
Sources/hpa/Internal/GlobalOptions.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import ArgumentParser
|
||||
|
||||
struct BasicGlobalOptions: ParsableArguments {
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Increase logging level (can be passed multiple times)."
|
||||
)
|
||||
var verbose: Int
|
||||
|
||||
@Flag(
|
||||
name: .shortAndLong,
|
||||
help: "Supress logging."
|
||||
)
|
||||
var quiet = false
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Optional shell to use when calling shell commands.",
|
||||
completion: .file()
|
||||
)
|
||||
var shell: String?
|
||||
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
struct GlobalOptions: ParsableArguments {
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Optional path to the ansible hpa playbook directory."
|
||||
)
|
||||
var playbookDir: String?
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Optional path to the ansible inventory to use."
|
||||
)
|
||||
var inventoryPath: String?
|
||||
|
||||
@Flag(
|
||||
name: .long,
|
||||
help: "Supress only playbook logging."
|
||||
)
|
||||
var quietOnlyPlaybook = false
|
||||
|
||||
@OptionGroup var basic: BasicGlobalOptions
|
||||
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
|
||||
get { basic[keyPath: keyPath] }
|
||||
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
|
||||
)
|
||||
}
|
||||
108
Sources/hpa/Internal/RunPlaybook.swift
Normal file
108
Sources/hpa/Internal/RunPlaybook.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
import Rainbow
|
||||
import ShellClient
|
||||
|
||||
func runPlaybook(
|
||||
commandName: String,
|
||||
globals: GlobalOptions,
|
||||
configuration: Configuration? = nil,
|
||||
extraArgs: [String],
|
||||
_ args: [String]
|
||||
) async throws {
|
||||
try await withSetupLogger(commandName: commandName, globals: globals) {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
logger.debug("Begin run playbook: \(globals)")
|
||||
|
||||
let configuration = try cliClient.ensuredConfiguration(configuration)
|
||||
|
||||
logger.debug("Configuration: \(configuration)")
|
||||
|
||||
let playbookDir = try ensureString(
|
||||
globals: globals,
|
||||
configuration: configuration,
|
||||
globalsKeyPath: \.playbookDir,
|
||||
configurationKeyPath: \.playbookDir
|
||||
)
|
||||
let playbook = "\(playbookDir)/\(Constants.playbookFileName)"
|
||||
|
||||
let inventory = (try? ensureString(
|
||||
globals: globals,
|
||||
configuration: configuration,
|
||||
globalsKeyPath: \.inventoryPath,
|
||||
configurationKeyPath: \.inventoryPath
|
||||
)) ?? "\(playbookDir)/\(Constants.inventoryFileName)"
|
||||
|
||||
let defaultArgs = (configuration.defaultPlaybookArgs ?? [])
|
||||
+ (configuration.defaultVaultArgs ?? [])
|
||||
|
||||
try await cliClient.runCommand(
|
||||
quiet: globals.quietOnlyPlaybook ? true : globals.quiet,
|
||||
shell: globals.shellOrDefault,
|
||||
[
|
||||
"ansible-playbook", playbook,
|
||||
"--inventory", inventory
|
||||
] + args
|
||||
+ extraArgs
|
||||
+ defaultArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
case playbookNotFound
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user