feat: Begins adding docker containers

This commit is contained in:
2024-12-17 10:15:07 -05:00
parent 99459a0a71
commit f7f168b7fd
10 changed files with 114 additions and 27 deletions

View File

@@ -4,6 +4,8 @@ import Foundation
import TOMLKit import TOMLKit
public extension DependencyValues { public extension DependencyValues {
/// Holds onto decoders and encoders for json and toml files.
var coders: Coders { var coders: Coders {
get { self[Coders.self] } get { self[Coders.self] }
set { self[Coders.self] = newValue } set { self[Coders.self] = newValue }

View File

@@ -204,7 +204,12 @@ extension ShellCommand.Shell {
if let path { if let path {
self = .custom(path: path, useDashC: true) self = .custom(path: path, useDashC: true)
} else { } else {
self = .zsh(useDashC: true) #if os(macOS)
self = .zsh(useDashC: true)
#else
// Generally we're in a docker container when this occurs, which does not have `zsh` installed.
self = .sh(useDashC: true)
#endif
} }
} }
} }

View File

@@ -2,13 +2,28 @@ import Foundation
// NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated. // NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated.
/// Represents configurable settings for the command line tool. /// Represents configurable settings for the application.
public struct Configuration: Codable, Equatable, Sendable { public struct Configuration: Codable, Equatable, Sendable {
/// Default arguments / options that can get passed into
/// ansible-playbook commands.
public let args: [String]? public let args: [String]?
/// Whether to use the vault arguments as defaults that get passed into
/// the ansible-playbook commands.
public let useVaultArgs: Bool public let useVaultArgs: Bool
/// Configuration for when generating files from templated directories.
public let generate: Generate? public let generate: Generate?
/// Configuration of the ansible-playbook, these are more for developing the
/// playbook locally and generally not needed by the user.
public let playbook: Playbook? public let playbook: Playbook?
/// Template configuration options.
public let template: Template public let template: Template
/// Ansible-vault configuration options.
public let vault: Vault public let vault: Vault
public init( public init(

View File

@@ -3,23 +3,47 @@ import DependenciesMacros
import Foundation import Foundation
public extension DependencyValues { public extension DependencyValues {
/// Represents interactions with the file system.
///
var fileClient: FileClient { var fileClient: FileClient {
get { self[FileClient.self] } get { self[FileClient.self] }
set { self[FileClient.self] = newValue } set { self[FileClient.self] = newValue }
} }
} }
/// Represents interactions with the file system.
///
///
@DependencyClient @DependencyClient
public struct FileClient: Sendable { public struct FileClient: Sendable {
/// Copy an item from one location to another.
public var copy: @Sendable (URL, URL) async throws -> Void public var copy: @Sendable (URL, URL) async throws -> Void
/// Create a directory at the given location.
public var createDirectory: @Sendable (URL) async throws -> Void public var createDirectory: @Sendable (URL) async throws -> Void
/// Check if a file exists at the given location.
public var fileExists: @Sendable (URL) -> Bool = { _ in true } public var fileExists: @Sendable (URL) -> Bool = { _ in true }
/// Find an ansible-vault file in the given location, checking up to 1 level deep
/// in subfolders.
public var findVaultFile: @Sendable (URL) async throws -> URL? public var findVaultFile: @Sendable (URL) async throws -> URL?
/// Return the user's home directory.
public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") } public var homeDirectory: @Sendable () -> URL = { URL(filePath: "~/") }
/// Check if an item is a directory or not.
public var isDirectory: @Sendable (URL) async throws -> Bool public var isDirectory: @Sendable (URL) async throws -> Bool
/// Load a file from the given location.
public var load: @Sendable (URL) async throws -> Data public var load: @Sendable (URL) async throws -> Data
/// Write data to a file at the given location.
public var write: @Sendable (Data, URL) async throws -> Void public var write: @Sendable (Data, URL) async throws -> Void
/// Find an ansible-vault file in the current directory, checking up to 1 level
/// deep in subfolders.
public func findVaultFileInCurrentDirectory() async throws -> URL? { public func findVaultFileInCurrentDirectory() async throws -> URL? {
try await findVaultFile(URL(filePath: "./")) try await findVaultFile(URL(filePath: "./"))
} }
@@ -30,23 +54,16 @@ extension FileClient: DependencyKey {
public static var liveValue: Self { public static var liveValue: Self {
let manager = LiveFileClient() let manager = LiveFileClient()
return .init { return .init(
try await manager.copy($0, to: $1) copy: { try await manager.copy($0, to: $1) },
} createDirectory: { createDirectory: { try await manager.creatDirectory($0) },
try await manager.creatDirectory($0) fileExists: { manager.fileExists(at: $0) },
} fileExists: { url in findVaultFile: { try await manager.findVaultFile(in: $0) },
manager.fileExists(at: url) homeDirectory: { manager.homeDirectory() },
} findVaultFile: { isDirectory: { manager.isDirectory($0) },
try await manager.findVaultFile(in: $0) load: { try await manager.load(from: $0) },
} homeDirectory: { write: { try await manager.write($0, to: $1) }
manager.homeDirectory() )
} isDirectory: {
manager.isDirectory($0)
} load: { url in
try await manager.load(from: url)
} write: { data, url in
try await manager.write(data, to: url)
}
} }
} }

View File

@@ -18,6 +18,7 @@ struct ConfigurationClientTests: TestCase {
@Test(arguments: ["config.toml", "config.json"]) @Test(arguments: ["config.toml", "config.json"])
func generateConfigFile(fileName: String) async throws { func generateConfigFile(fileName: String) async throws {
try await withTestLogger(key: "generateConfigFile") { try await withTestLogger(key: "generateConfigFile") {
$0.coders = .liveValue
$0.fileClient = .liveValue $0.fileClient = .liveValue
} operation: { } operation: {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@@ -53,6 +54,7 @@ struct ConfigurationClientTests: TestCase {
@Test(arguments: ["config.toml", "config.json", nil]) @Test(arguments: ["config.toml", "config.json", nil])
func loadConfigFile(fileName: String?) async throws { func loadConfigFile(fileName: String?) async throws {
try await withTestLogger(key: "generateConfigFile") { try await withTestLogger(key: "generateConfigFile") {
$0.coders = .liveValue
$0.fileClient = .liveValue $0.fileClient = .liveValue
} operation: { } operation: {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger

View File

@@ -23,7 +23,7 @@ struct FileClientTests {
let fileClient = FileClient.liveValue let fileClient = FileClient.liveValue
let vaultFilePath = url.appending(path: fileName) let vaultFilePath = url.appending(path: fileName)
try FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil) FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
let output = try await fileClient.findVaultFile(url)! let output = try await fileClient.findVaultFile(url)!
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath) #expect(output.cleanFilePath == vaultFilePath.cleanFilePath)
@@ -43,7 +43,7 @@ struct FileClientTests {
try await fileClient.createDirectory(subDir) try await fileClient.createDirectory(subDir)
let vaultFilePath = subDir.appending(path: fileName) let vaultFilePath = subDir.appending(path: fileName)
try FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil) FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
let output = try await fileClient.findVaultFile(url)! let output = try await fileClient.findVaultFile(url)!
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath) #expect(output.cleanFilePath == vaultFilePath.cleanFilePath)

View File

@@ -22,12 +22,14 @@ struct PlaybookClientTests: TestCase {
@Test(.tags(.repository)) @Test(.tags(.repository))
func repositoryInstallation() async throws { func repositoryInstallation() async throws {
try await withDependencies { try await withTestLogger(key: "repositoryInstallation") {
$0.fileClient = .liveValue $0.fileClient = .liveValue
$0.asyncShellClient = .liveValue $0.asyncShellClient = .liveValue
$0.commandClient = .liveValue $0.commandClient = .liveValue
} operation: { } operation: {
try await withTemporaryDirectory { tempDirectory in try await withTemporaryDirectory { tempDirectory in
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
let pathUrl = tempDirectory.appending(path: "playbook") let pathUrl = tempDirectory.appending(path: "playbook")
let playbookClient = PlaybookClient.liveValue let playbookClient = PlaybookClient.liveValue
@@ -35,7 +37,8 @@ struct PlaybookClientTests: TestCase {
try? FileManager.default.removeItem(at: pathUrl) try? FileManager.default.removeItem(at: pathUrl)
try await playbookClient.repository.install(configuration) try await playbookClient.repository.install(configuration)
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath) logger.debug("Done cloning playbook")
let exists = try await fileClient.isDirectory(pathUrl)
#expect(exists) #expect(exists)
} }
} }
@@ -61,8 +64,6 @@ struct PlaybookClientTests: TestCase {
try await withMockConfiguration(captured, key: "runBuildProject") { try await withMockConfiguration(captured, key: "runBuildProject") {
@Dependency(\.playbookClient) var playbookClient @Dependency(\.playbookClient) var playbookClient
let configuration = Configuration.mock
try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions)) try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions))
let arguments = await captured.options!.arguments let arguments = await captured.options!.arguments
@@ -92,8 +93,6 @@ struct PlaybookClientTests: TestCase {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient @Dependency(\.playbookClient) var playbookClient
let configuration = Configuration.mock
try await playbookClient.run.createProject( try await playbookClient.run.createProject(
.init( .init(
projectDirectory: "/project", projectDirectory: "/project",

23
docker/Dockerfile Executable file
View File

@@ -0,0 +1,23 @@
# Used this to build the release version of the image.
# Build the executable
ARG SWIFT_IMAGE_VERSION="6.0.3"
FROM swift:${SWIFT_IMAGE_VERSION} AS build
WORKDIR /build
COPY ./Package.* ./
RUN swift package resolve
COPY . .
RUN swift build -c release -Xswiftc -g
# Run image
FROM swift:${SWIFT_IMAGE_VERSION}-slim
RUN export DEBIAN_FRONTEND=nointeractive DEBCONF_NOINTERACTIVE_SEEN=true && apt-get -q update && \
apt-get -q install -y \
ansible \
pandoc \
texlive \
&& rm -r /var/lib/apt/lists/*
COPY --from=build /build/.build/release/hpa /usr/local/bin
CMD ["/bin/bash", "-xc", "/usr/local/bin/hpa"]

10
docker/Dockerfile.test Normal file
View File

@@ -0,0 +1,10 @@
# Used to build a test image.
ARG SWIFT_IMAGE_VERSION="6.0.3"
FROM swift:${SWIFT_IMAGE_VERSION}
WORKDIR /app
COPY ./Package.* ./
RUN swift package resolve
COPY . .
RUN swift build
CMD ["/bin/bash", "-xc", "swift", "test"]

View File

@@ -1,14 +1,28 @@
docker_image_name := "swift-hpa"
build mode="debug": build mode="debug":
swift build -c {{mode}} swift build -c {{mode}}
alias b := build alias b := build
build-docker file="Dockerfile" tag="latest":
@docker build \
--file docker/{{file}} \
--tag {{docker_image_name}}:{{tag}} .
build-docker-test: (build-docker "Dockerfile.test" "test")
test *ARGS: test *ARGS:
swift test {{ARGS}} swift test {{ARGS}}
alias t := test alias t := test
test-docker *ARGS: (build-docker-test)
@docker run --rm -it \
--network host \
{{docker_image_name}}:test \
swift test {{ARGS}}
run *ARGS: run *ARGS:
swift run hpa {{ARGS}} swift run hpa {{ARGS}}