import ArgumentParser import CliClient import Dependencies import Foundation import Logging import Rainbow import ShellClient 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. """ ) } } 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, configurationKeyPath: KeyPath ) 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( 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)/main.yml" let inventory = (try? ensureString( globals: globals, configuration: configuration, globalsKeyPath: \.inventoryPath, configurationKeyPath: \.inventoryPath )) ?? "\(playbookDir)/inventory.ini" var playbookArgs = [ "ansible-playbook", playbook, "--inventory", inventory ] if let defaultArgs = configuration.defaultPlaybookArgs { playbookArgs.append(defaultArgs) } try await cliClient.runCommand( quiet: globals.quietOnlyPlaybook ? true : globals.basic.quiet, shell: globals.shellOrDefault, playbookArgs + args + extraArgs ) } } enum RunPlaybookError: Error { case playbookNotFound case configurationError }