import CommandClient import ConfigurationClient import Dependencies import Foundation extension PlaybookClient.RunPlaybook.BuildOptions { func run() async throws { try await shared.run { arguments, _ in let projectDirectory = projectDirectory ?? ProcessInfo.processInfo.environment["PWD"] guard let projectDirectory else { throw PlaybookClientError.projectDirectoryNotFound } arguments.append(contentsOf: [ "--tags", "build-project", "--extra-vars", "project_dir=\(projectDirectory)" ]) if let extraOptions = shared.extraOptions { arguments.append(contentsOf: extraOptions) } } } } extension PlaybookClient.RunPlaybook.CreateOptions { func run(encoder jsonEncoder: JSONEncoder?) async throws { try await shared.run { arguments, configuration in let jsonData = try createJSONData( configuration: configuration, encoder: jsonEncoder ) guard let json = String(data: jsonData, encoding: .utf8) else { throw PlaybookClientError.encodingError } arguments.append(contentsOf: [ "--tags", "setup-project", "--extra-vars", "project_dir=\(projectDirectory)", "--extra-vars", "'\(json)'" ]) if let extraOptions = shared.extraOptions { arguments.append(contentsOf: extraOptions) } } } } extension PlaybookClient.RunPlaybook.GenerateTemplateOptions { func run() async throws -> String { try await shared.run { arguments, _ in arguments.append(contentsOf: [ "--tags", "repo-template", "--extra-vars", "output_dir=\(templateDirectory)" ]) if let templateVarsDirectory { arguments.append(contentsOf: ["--extra-vars", "repo_vars_dir=\(templateVarsDirectory)"]) } if !useVault { arguments.append(contentsOf: ["--extra-vars", "use_vault=false"]) } return templateDirectory } } } extension PlaybookClient.RunPlaybook.SharedRunOptions { @discardableResult func run( _ apply: @Sendable @escaping (inout [String], Configuration) throws -> T ) async throws -> T { @Dependency(\.commandClient) var commandClient try await ensurePlaybookExists() return try await commandClient.run( logging: loggingOptions, quiet: quiet, shell: shell ) { @Dependency(\.logger) var logger @Dependency(\.configurationClient) var configurationClient let configuration = try await configurationClient.findAndLoad() var arguments = try await PlaybookClient.RunPlaybook.makeCommonArguments( configuration: configuration, inventoryFilePath: inventoryFilePath ) let output = try apply(&arguments, configuration) return (arguments, output) } } private func ensurePlaybookExists() async throws { @Dependency(\.fileClient) var fileClient @Dependency(\.playbookClient.repository) var repository let directory = try await repository.directory() let exists = try await fileClient.isDirectory(URL(filePath: directory)) if !exists { try await repository.install() } } } @_spi(Internal) public extension PlaybookClient.RunPlaybook { static func ensuredInventoryPath( _ optionalInventoryPath: String?, configuration: Configuration, playbookDirectory: String ) -> String { guard let path = optionalInventoryPath else { guard let path = configuration.playbook?.inventory else { return "\(playbookDirectory)/\(PlaybookClient.Constants.inventoryFileName)" } return path } return path } static func makeCommonArguments( configuration: Configuration, inventoryFilePath: String? ) async throws -> [String] { @Dependency(\.logger) var logger let playbookDirectory = try await PlaybookClient.Repository.findDirectory(configuration: configuration) let playbookPath = "\(playbookDirectory)/\(PlaybookClient.Constants.playbookFileName)" logger.trace("Playbook path: \(playbookPath)") let inventoryPath = ensuredInventoryPath( inventoryFilePath, configuration: configuration, playbookDirectory: playbookDirectory ) logger.trace("Inventory path: \(inventoryPath)") var arguments = [ PlaybookClient.Constants.playbookCommand, playbookPath, "--inventory", inventoryPath ] if let defaultArgs = configuration.args { arguments.append(contentsOf: defaultArgs) } if configuration.useVaultArgs, let vaultArgs = configuration.vault.args { arguments.append(contentsOf: vaultArgs) } logger.trace("Common arguments: \(inventoryPath)") return arguments } } // // NOTE: We're not using the `Coders` client because we generally do not // want the output to be `prettyPrinted` or anything, unless we're running // tests, so we use a supplied json encoder. // @_spi(Internal) public extension PlaybookClient.RunPlaybook.CreateOptions { func createJSONData( configuration: Configuration, encoder: JSONEncoder? ) throws -> Data { @Dependency(\.logger) var logger let encoder = encoder ?? jsonEncoder let templateDir = template?.directory ?? configuration.template.directory let templateRepo = template?.url ?? configuration.template.url let version = template?.version ?? configuration.template.version logger.debug(""" (\(useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo))) """) switch (useLocalTemplateDirectory, templateDir, templateRepo) { case (true, .none, _): // User supplied they wanted to use a local template directory, but we could not find // the path set from command line or in configuration. throw PlaybookClientError.templateDirectoryNotFound case let (false, _, .some(repo)): // User did not supply they wanted to use a local template directory, and we found a repo url that was // either set by the command line or found in the configuration. logger.debug("Using repo.") return try encoder.encode(TemplateRepo(repo: repo, version: version)) case let (true, .some(templateDir), _): // User supplied they wanted to use a local template directory, and we found the template directory // either set by the command line or in the configuration. logger.debug("Using template directory.") return try encoder.encode(TemplateDirJson(path: templateDir)) case let (false, .some(templateDir), _): // User supplied they did not wanted to use a local template directory, and we found the template directory // either set by the command line or in the configuration, and no repo was found / handled previously. logger.debug("Using template directory.") return try encoder.encode(TemplateDirJson(path: templateDir)) case (_, .none, .none): // We could not find a repo or template directory. throw PlaybookClientError.templateDirectoryOrRepoNotSpecified } } } private struct TemplateDirJson: Encodable { let template: Template init(path: String) { self.template = .init(path: path) } struct Template: Encodable { let path: String } } private struct TemplateRepo: Encodable { let template: Template init(repo: String, version: String?) { self.template = .init(repo: .init(url: repo, version: version ?? "main")) } struct Template: Encodable { let repo: Repo } struct Repo: Encodable { let url: String let version: String } } private let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] return encoder }()