feat: Moves playbook run into playbook client, need to move tests.

This commit is contained in:
2024-12-14 23:14:01 -05:00
parent 303cdef84b
commit bc0b740f95
4 changed files with 334 additions and 9 deletions

View File

@@ -3,6 +3,9 @@ public extension PlaybookClient {
@_spi(Internal) @_spi(Internal)
enum Constants { enum Constants {
public static let defaultInstallationPath = "~/.local/share/hpa/playbook" public static let defaultInstallationPath = "~/.local/share/hpa/playbook"
public static let inventoryFileName = "inventory.ini"
public static let playbookCommand = "ansible-playbook"
public static let playbookFileName = "main.yml"
public static let playbookRepoUrl = "https://git.housh.dev/michael/ansible-hpa-playbook.git" public static let playbookRepoUrl = "https://git.housh.dev/michael/ansible-hpa-playbook.git"
public static let playbookRepoVersion = "main" public static let playbookRepoVersion = "main"
} }

View File

@@ -0,0 +1,205 @@
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() async throws {
try await shared.run { arguments, configuration in
let json = try createJSONData(configuration: configuration)
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.SharedRunOptions {
func run(_ apply: @Sendable @escaping (inout [String], Configuration) throws -> Void) async throws {
@Dependency(\.commandClient) var commandClient
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
)
try apply(&arguments, configuration)
return arguments
}
}
}
@_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.
//
extension PlaybookClient.RunPlaybook.CreateOptions {
func createJSONData(
configuration: Configuration,
encoder: JSONEncoder = .init()
) throws -> Data {
@Dependency(\.logger) var logger
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
}
}

View File

@@ -1,3 +1,4 @@
import CommandClient
import ConfigurationClient import ConfigurationClient
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
@@ -17,6 +18,7 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct PlaybookClient: Sendable { public struct PlaybookClient: Sendable {
public var repository: Repository public var repository: Repository
public var run: RunPlaybook
} }
public extension PlaybookClient { public extension PlaybookClient {
@@ -33,23 +35,131 @@ public extension PlaybookClient {
public func directory() async throws -> String { public func directory() async throws -> String {
try await directory(nil) try await directory(nil)
} }
}
}
public static var liveValue: Self { public extension PlaybookClient {
.init {
try await installPlaybook(configuration: $0) @DependencyClient
} directory: { struct RunPlaybook: Sendable {
try await findDirectory(configuration: $0) public var buildProject: @Sendable (BuildOptions) async throws -> Void
public var createProject: @Sendable (CreateOptions) async throws -> Void
public struct SharedRunOptions: Equatable, Sendable {
public let extraOptions: [String]?
public let inventoryFilePath: String?
public let loggingOptions: LoggingOptions
public let quiet: Bool
public let shell: String?
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
loggingOptions: LoggingOptions,
quiet: Bool,
shell: String?
) {
self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath
self.loggingOptions = loggingOptions
self.quiet = quiet
self.shell = shell
}
}
@dynamicMemberLookup
public struct BuildOptions: Equatable, Sendable {
public let projectDirectory: String?
public let shared: SharedRunOptions
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
loggingOptions: LoggingOptions,
quiet: Bool,
shell: String?,
projectDirectory: String
) {
self.projectDirectory = projectDirectory
self.shared = .init(
extraOptions: extraOptions,
inventoryFilePath: inventoryFilePath,
loggingOptions: loggingOptions,
quiet: quiet,
shell: shell
)
}
public subscript<T>(dynamicMember keyPath: KeyPath<SharedRunOptions, T>) -> T {
shared[keyPath: keyPath]
}
}
@dynamicMemberLookup
public struct CreateOptions: Equatable, Sendable {
public let projectDirectory: String
public let shared: SharedRunOptions
public let template: Configuration.Template
public let useLocalTemplateDirectory: Bool
public init(
extraOptions: [String]?,
inventoryFilePath: String?,
loggingOptions: LoggingOptions,
projectDirectory: String,
quiet: Bool,
shell: String?,
template: Configuration.Template,
useLocalTemplateDirectory: Bool
) {
self.projectDirectory = projectDirectory
self.template = template
self.useLocalTemplateDirectory = useLocalTemplateDirectory
self.shared = .init(
extraOptions: extraOptions,
inventoryFilePath: inventoryFilePath,
loggingOptions: loggingOptions,
quiet: quiet,
shell: shell
)
}
public subscript<T>(dynamicMember keyPath: KeyPath<SharedRunOptions, T>) -> T {
shared[keyPath: keyPath]
} }
} }
} }
} }
extension PlaybookClient: DependencyKey { extension PlaybookClient.Repository: DependencyKey {
public static let testValue: PlaybookClient = Self(repository: Repository()) public static var liveValue: Self {
.init {
try await installPlaybook(configuration: $0)
} directory: {
try await findDirectory(configuration: $0)
}
}
}
public static var liveValue: PlaybookClient { extension PlaybookClient.RunPlaybook: DependencyKey {
public static var liveValue: PlaybookClient.RunPlaybook {
.init( .init(
repository: .liveValue buildProject: { try await $0.run() },
createProject: { try await $0.run() }
)
}
}
extension PlaybookClient: DependencyKey {
public static let testValue: PlaybookClient = Self(
repository: Repository(),
run: RunPlaybook()
)
public static var liveValue: PlaybookClient {
.init(
repository: .liveValue,
run: .liveValue
) )
} }
} }

View File

@@ -0,0 +1,7 @@
import Foundation
enum PlaybookClientError: Error {
case projectDirectoryNotFound
case templateDirectoryNotFound
case templateDirectoryOrRepoNotSpecified
}