Skip to content

Commit

Permalink
Migrate to volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
iainsmith committed Apr 24, 2020
1 parent 6b3c1de commit d088e22
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 120 deletions.
4 changes: 2 additions & 2 deletions Sources/SwiftDockerLib/BuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ struct BuildCommandRunner {
try withTemporaryFileClosure(dir, prefix, suffix, deleteOnClose, body)
}

func run(action: Dockerfile.ActionLabel) throws {
func run(action: ActionLabel) throws {
_ = try withTemporaryFile(prefix: "Dockerfile") { (file) -> Void in
let dockerfileBody = Dockerfile.makeMinimalDockerFile(
image: options.baseImage.fullName,
image: options.dockerBaseImage.fullName,
directory: options.projectName,
action: action
)
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftDockerLib/CLI.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ArgumentParser
import var TSCBasic.localFileSystem

public struct SwiftDockerCLI: ParsableCommand {
public static var configuration: CommandConfiguration = CommandConfiguration(
Expand Down
10 changes: 9 additions & 1 deletion Sources/SwiftDockerLib/CLIOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,26 @@ public struct CLIOptions: ParsableArguments {
try! AbsolutePath(validating: url.path)
}

var baseImage: DockerTag {
var dockerBaseImage: DockerTag {
DockerTag(version: swift, image: image)!
}

var projectName: String {
absolutePath.basename
}

var dockerVolumeName: String {
"swiftdockercli-\(projectName)"
}

var defaultDockerfilePath: AbsolutePath {
absolutePath.appending(component: "Dockerfile")
}

var buildFolderPath: AbsolutePath {
absolutePath.appending(component: ".build")
}

public init() {}

public func validate() throws {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftDockerLib/CleanupCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ struct CleanupCommand: ParsableCommand {
discussion: """
All swift-docker DOCKERFILEs are tagged with the following labels
LABEL \(Dockerfile.ActionLabel.label)=\(Dockerfile.ActionLabel.buildForTesting.rawValue)/\(Dockerfile.ActionLabel.build.rawValue)
LABEL \(Dockerfile.FolderLabel.label)=name-of-folder
LABEL \(ActionLabel.label)=\(ActionLabel.buildForTesting.rawValue)/\(ActionLabel.build.rawValue)
LABEL \(FolderLabel.label)=name-of-folder
You can list all test images created using
Expand Down
25 changes: 25 additions & 0 deletions Sources/SwiftDockerLib/DockerLabel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
protocol DockerLabel {
static var label: String { get }
static func label(with value: String) -> String
}

enum ActionLabel: String, DockerLabel {
case buildForTesting = "test"
case build

static let label = "com.\(DockerHub.reservedDockerID).action"

static func label(with value: ActionLabel) -> String {
label(with: value.rawValue)
}
}

enum FolderLabel: DockerLabel {
static let label = "com.\(DockerHub.reservedDockerID).folder"
}

extension DockerLabel {
static func label(with value: String) -> String {
"\(label)=\(value)"
}
}
24 changes: 15 additions & 9 deletions Sources/SwiftDockerLib/DockerOutputRewriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ enum DockerOutputRewriter: OutputRewriter {
static func make(controller: TerminalController) -> TSCBasic.Process.OutputRedirection {
.stream(stdout: { stdBytes in
guard let string = String(bytes: stdBytes, encoding: .utf8) else { return }
string.split { $0.isNewline }.forEach { (substring: Substring) in
if substring.hasPrefix("Step ") {
controller.write(substring.indent(by: 2))
string.eachLine { line in
if line.hasPrefix("Step ") {
controller.write(line.indent(by: 2))
controller.endLine()
}
return
}
}, stderr: { errorBytes in
guard let string = String(bytes: errorBytes, encoding: .utf8) else { return }
string.split(separator: "\n").forEach { substring in
controller.write(String(substring), inColor: .red)
string.eachLine {
controller.write($0, inColor: .red)
controller.endLine()
}
})
Expand All @@ -29,16 +29,22 @@ enum VerboseOutputRedirection: OutputRewriter {
static func make(controller: TerminalController) -> TSCBasic.Process.OutputRedirection {
.stream(stdout: { stdBytes in
guard let string = String(bytes: stdBytes, encoding: .utf8) else { return }
string.split { $0.isNewline }.forEach { (substring: Substring) in
controller.write(substring.indent(by: 2))
string.eachLine {
controller.write($0.indent(by: 2))
controller.endLine()
}
}, stderr: { errorBytes in
guard let string = String(bytes: errorBytes, encoding: .utf8) else { return }
string.split(separator: "\n").forEach { substring in
controller.write(String(substring), inColor: .red)
string.eachLine {
controller.write($0, inColor: .red)
controller.endLine()
}
})
}
}

extension String {
func eachLine(body: (String) throws -> Void) rethrows -> Void {
try split { $0.isNewline }.forEach { try body(String($0)) }
}
}
11 changes: 0 additions & 11 deletions Sources/SwiftDockerLib/Dockerfile.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
enum Dockerfile {
enum ActionLabel: String {
case buildForTesting = "test"
case build

static let label = "com.\(DockerHub.reservedDockerID).action"
}

enum FolderLabel {
static let label = "com.\(DockerHub.reservedDockerID).folder"
}

static func makeMinimalDockerFile(
image: String,
directory directoryName: String,
Expand Down
9 changes: 0 additions & 9 deletions Sources/SwiftDockerLib/FileSystemExtensions.swift

This file was deleted.

8 changes: 8 additions & 0 deletions Sources/SwiftDockerLib/Process.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//
// File.swift
//
//
// Created by Iain Smith on 24/04/2020.
//

import Foundation
13 changes: 13 additions & 0 deletions Sources/SwiftDockerLib/ShellRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ protocol ShellProtocol {

@discardableResult
static func run(_ cmd: String, outputDestination: OutputDestination?, isVerbose: Bool) throws -> ProcessResult

@discardableResult
static func runCleanExit(_ cmd: String, outputDestination: OutputDestination?, isVerbose: Bool) throws -> ProcessResult
}

enum ShellRunner: ShellProtocol {
Expand Down Expand Up @@ -68,3 +71,13 @@ enum ShellRunner: ShellProtocol {
return result
}
}

extension ShellProtocol {
static func runCleanExit(_ cmd: String, outputDestination: OutputDestination?, isVerbose: Bool) throws -> ProcessResult {
let result = try run(cmd, outputDestination: outputDestination, isVerbose: isVerbose)
if case .terminated(code: 1) = result.exitStatus {
throw DockerError("\(cmd) failed")
}
return result
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftDockerLib/SquareBracketsLineRewriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ enum SquareBracketsLineRewriter: OutputRewriter {
.stream(stdout: { stdBytes in
var needsEndLine = false
guard let string = String(bytes: stdBytes, encoding: .utf8) else { return }
string.split { $0.isNewline }.forEach { substring in
string.eachLine { substring in
let isInlineTotal = substring.hasPrefix("[")
if isInlineTotal {
controller.clearLine()
Expand Down
14 changes: 14 additions & 0 deletions Sources/SwiftDockerLib/TSCLibExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import struct Foundation.URL
import TSCBasic

extension FileSystem {
func exists(_ url: URL) -> Bool {
exists(try! AbsolutePath(validating: url.path))
}
}

extension ProcessResult {
func utf8OutputLines() throws -> [String] {
try utf8Output().split { $0.isWhitespace }.map(String.init)
}
}
146 changes: 89 additions & 57 deletions Sources/SwiftDockerLib/TestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,80 @@ public struct TestCommand: ParsableCommand {
@OptionGroup()
var options: CLIOptions

@Flag(help: "Copy the .build folder from your machine to the container")
var seedBuildFolder: Bool

@Flag(help: "Remove the docker .build folder")
var clean: Bool

@Option(parsing: .remaining, help: "swift test arguments such as --configuration/--parallel")
var args: [String]

private var outputDestinaton: OutputDestination = TerminalController(stream: stdoutStream) ?? stdoutStream
private var shell: ShellProtocol.Type = ShellRunner.self

public func run() throws {
// Docker build context is relative to the directory docker is being called from.
// https://github.com/moby/moby/issues/4592
// For testing we use the swift-tools-support InMemoryFileSystem which fatalErrors
// on calls to changeCurrentWorkingDirectory in 0.1.1
try localFileSystem.changeCurrentWorkingDirectory(to: options.absolutePath)
try TestCommandRunner(options: options).run()
}
let projectLabel = FolderLabel.label(with: options.projectName)
ifVerbosePrint("Checking for existing docker volume")

public init() {}
let existingImages = try shell.run(
"docker volume ls --quiet --filter label=\(projectLabel)",
outputDestination: nil,
isVerbose: options.verbose
)

public init(options: CLIOptions) {
self.options = options
}
}
let labels = """
--label \(projectLabel) \
--label \(ActionLabel.label(with: .buildForTesting))
"""

if clean {
try shell.run("docker volume rm \(options.dockerVolumeName)", outputDestination: nil, isVerbose: options.verbose)
}

let existingVolume = try existingImages.utf8OutputLines().contains(options.dockerVolumeName)
if !existingVolume || clean {
ifVerbosePrint("Creating new docker volume to cache .build folder")
let result = try shell.run(
"""
docker volume create \
\(labels) \
\(options.dockerVolumeName)
""",
outputDestination: nil,
isVerbose: options.verbose
)
if case .terminated(code: 1) = result.exitStatus {
throw DockerError("Unable to create image")
}
}

if seedBuildFolder {
ifVerbosePrint("Copying .build folder to volume: \(options.dockerVolumeName)")
let name = "swiftdockercli-seed"
let folder = "/.build"
try shell.runCleanExit("docker container create --name \(name) --mount type=volume,source=\(options.dockerVolumeName),target=\(folder) \(options.dockerBaseImage.fullName)", outputDestination: nil, isVerbose: options.verbose)
try shell.runCleanExit("docker cp \(options.buildFolderPath.pathString)/. \(name):\(folder)", outputDestination: nil, isVerbose: options.verbose)
try shell.runCleanExit("docker rm \(name)", outputDestination: nil, isVerbose: options.verbose)
}

/// You must run this command from the options.absolutePath directory.
struct TestCommandRunner {
private var options: CLIOptions
private let filesystem: FileSystem
private let outputDestinaton: OutputDestination
private let shell: ShellProtocol.Type
private let withTemporaryFileClosure: TemporaryFileFunction
private let computeGitSHA: () -> String

func run() throws {
let uniqueHash = computeGitSHA() // TODO:
let tagName = "swift-docker/\(options.projectName.lowercased()):\(uniqueHash)"

try BuildCommandRunner(
tag: tagName, options: options, fileSystem: filesystem,
output: outputDestinaton, shell: shell,
withTemporaryFile: withTemporaryFileClosure
).run(action: .buildForTesting)

let testCommand = DockerCommands.dockerRun(tag: tagName, remove: true, command: "swift test")
outputDestinaton.writeLine("-> swift test")

var swiftTest = "swift test"
if !args.isEmpty {
swiftTest += " \(args.joined(separator: " "))"
}

let testCommand = """
docker run --rm \
--mount type=bind,source=\(options.absolutePath.pathString),target=/package \
--mount type=volume,source=\(options.dockerVolumeName),target=/package/.build \
--workdir /package \
\(labels) \
\(options.dockerBaseImage.fullName) \
\(swiftTest)
"""

try shell.runWithStreamingOutput(
testCommand,
controller: outputDestinaton,
Expand All @@ -69,40 +106,35 @@ struct TestCommandRunner {
)
}

init(options: CLIOptions) {
self.init(
options: options,
fileSystem: localFileSystem,
output: TerminalController(stream: stdoutStream) ?? stdoutStream,
shell: ShellRunner.self
)
}
public init() {}


init(
options: CLIOptions,
fileSystem: FileSystem = localFileSystem,
seedBuildFolder: Bool = false,
clean: Bool = false,
args: [String] = [],
output: OutputDestination = TerminalController(stream: stdoutStream) ?? stdoutStream,
shell: ShellProtocol.Type = ShellRunner.self,
withTemporaryFile: TemporaryFileFunction? = nil,
computeGitSHA: (() -> String)? = nil
shell: ShellProtocol.Type = ShellRunner.self
) {
self.options = options
filesystem = fileSystem
self.clean = clean
self.seedBuildFolder = false
outputDestinaton = output
self.shell = shell
withTemporaryFileClosure = withTemporaryFile ?? { dir, prefix, suffix, delete, body in
try TSCBasic.withTemporaryFile(dir: dir, prefix: prefix, suffix: suffix, deleteOnClose: delete) { tempfile in try body(tempfile.path) }
}
self.computeGitSHA = computeGitSHA ?? { String(NSUUID().uuidString.prefix(6)) }
self.args = args
}

func withTemporaryFile(
dir: AbsolutePath? = nil,
prefix: String = "TemporaryFile",
suffix: String = "",
deleteOnClose: Bool = true, _
body: (AbsolutePath) throws -> Void
) throws {
try withTemporaryFileClosure(dir, prefix, suffix, deleteOnClose, body)
enum CodingKeys: String, CodingKey {
case options
case seedBuildFolder
case clean
case args
}

func ifVerbosePrint(_ string: String) {
if options.verbose {
outputDestinaton.writeLine(string)
}
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftDockerLib/WriteDockerfileCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct WriteDockerfileCommandRunner {

func run() throws {
let dockerfileBody = Dockerfile.makeMinimalDockerFile(
image: options.baseImage.fullName,
image: options.dockerBaseImage.fullName,
directory: options.projectName,
action: .build
)
Expand Down
Loading

0 comments on commit d088e22

Please sign in to comment.