Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions Sources/ContainerCommands/Image/ImageLoad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,38 @@ extension Application {
transform: { str in
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
})
var input: String
var input: String?

@OptionGroup
var global: Flags.Global

public func run() async throws {
guard FileManager.default.fileExists(atPath: input) else {
print("File does not exist \(input)")
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).tar")
defer {
try? FileManager.default.removeItem(at: tempFile)
}

// Read from stdin
if input == nil {
guard FileManager.default.createFile(atPath: tempFile.path(), contents: nil) else {
throw ContainerizationError(.internalError, message: "unable to create temporary file")
}

guard let outputHandle = try? FileHandle(forWritingTo: tempFile) else {
throw ContainerizationError(.internalError, message: "unable to open temporary file for writing")
}

let bufferSize = 4096
while true {
let chunk = FileHandle.standardInput.readData(ofLength: bufferSize)
if chunk.isEmpty { break }
outputHandle.write(chunk)
}
try outputHandle.close()
}

guard FileManager.default.fileExists(atPath: input ?? tempFile.path()) else {
print("File does not exist \(input ?? tempFile.path())")
Application.exit(withError: ArgumentParser.ExitCode(1))
}

Expand All @@ -57,7 +81,7 @@ extension Application {
progress.start()

progress.set(description: "Loading tar archive")
let loaded = try await ClientImage.load(from: input)
let loaded = try await ClientImage.load(from: input ?? tempFile.path())

let taskManager = ProgressTaskCoordinator()
let unpackTask = await taskManager.startTask()
Expand Down
29 changes: 27 additions & 2 deletions Sources/ContainerCommands/Image/ImageSave.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension Application {
transform: { str in
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
})
var output: String
var output: String?

@Option(
help: "Platform for the saved image (format: os/arch[/variant], takes precedence over --os and --arch)"
Expand Down Expand Up @@ -90,7 +90,32 @@ extension Application {
throw ContainerizationError(.invalidArgument, message: "failed to save image(s)")

}
try await ClientImage.save(references: references, out: output, platform: p)

let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).tar")
defer {
try? FileManager.default.removeItem(at: tempFile)
}

guard FileManager.default.createFile(atPath: tempFile.path(), contents: nil) else {
throw ContainerizationError(.internalError, message: "unable to create temporary file")
}

try await ClientImage.save(references: references, out: output ?? tempFile.path(), platform: p)

// Write to stdout
if output == nil {
guard let outputHandle = try? FileHandle(forReadingFrom: tempFile) else {
throw ContainerizationError(.internalError, message: "unable to open temporary file for reading")
}

let bufferSize = 4096
while true {
let chunk = outputHandle.readData(ofLength: bufferSize)
if chunk.isEmpty { break }
FileHandle.standardOutput.write(chunk)
}
try outputHandle.close()
}

progress.finish()
for reference in references {
Expand Down
6 changes: 3 additions & 3 deletions Tests/CLITests/Subcommands/Build/CLIBuildBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ class TestCLIBuildBase: CLITest {
}

func builderStart(cpus: Int64 = 2, memoryInGBs: Int64 = 2) throws {
let (_, error, status) = try run(arguments: [
let (_, _, error, status) = try run(arguments: [
"builder",
"start",
"-c",
Expand All @@ -266,7 +266,7 @@ class TestCLIBuildBase: CLITest {
}

func builderStop() throws {
let (_, error, status) = try run(arguments: [
let (_, _, error, status) = try run(arguments: [
"builder",
"stop",
])
Expand All @@ -276,7 +276,7 @@ class TestCLIBuildBase: CLITest {
}

func builderDelete(force: Bool = false) throws {
let (_, error, status) = try run(
let (_, _, error, status) = try run(
arguments: [
"builder",
"delete",
Expand Down
71 changes: 66 additions & 5 deletions Tests/CLITests/Subcommands/Images/TestCLIImages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class TestCLIImagesCommand: CLITest {
args.append("--all")
}

let (_, error, status) = try run(arguments: args)
let (_, _, error, status) = try run(arguments: args)
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}
Expand All @@ -49,7 +49,7 @@ class TestCLIImagesCommand: CLITest {
}

func doListImages() throws -> [Image] {
let (output, error, status) = try run(arguments: [
let (_, output, error, status) = try run(arguments: [
"image",
"list",
"--format",
Expand All @@ -75,7 +75,7 @@ class TestCLIImagesCommand: CLITest {
newName,
]

let (_, error, status) = try run(arguments: tagArgs)
let (_, _, error, status) = try run(arguments: tagArgs)
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}
Expand Down Expand Up @@ -323,7 +323,7 @@ extension TestCLIImagesCommand {
"--output",
tempFile.path(),
]
let (_, error, status) = try run(arguments: saveArgs)
let (_, _, error, status) = try run(arguments: saveArgs)
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}
Expand All @@ -344,7 +344,68 @@ extension TestCLIImagesCommand {
"-i",
tempFile.path(),
]
let (_, loadErr, loadStatus) = try run(arguments: loadArgs)
let (_, _, loadErr, loadStatus) = try run(arguments: loadArgs)
if loadStatus != 0 {
throw CLIError.executionFailed("command failed: \(loadErr)")
}

// 7. verify image is in the list again
let alpineImagePresent = try isImagePresent(targetImage: alpineTagged)
#expect(alpineImagePresent, "expected \(alpineTagged) to be present")
let busyboxImagePresent = try isImagePresent(targetImage: busyboxTagged)
#expect(busyboxImagePresent, "expected \(busyboxTagged) to be present")
} catch {
Issue.record("failed to save and load image \(error)")
return
}
}

@Test func testImageSaveAndLoadStdinStdout() throws {
do {
// 1. pull image
try doPull(imageName: alpine)
try doPull(imageName: busybox)

// 2. Tag image so we can safely remove later
let alpineRef: Reference = try Reference.parse(alpine)
let alpineTagged = "\(alpineRef.name):testImageSaveAndLoadStdinStdout"
try doImageTag(image: alpine, newName: alpineTagged)
let alpineTaggedImagePresent = try isImagePresent(targetImage: alpineTagged)
#expect(alpineTaggedImagePresent, "expected to see image \(alpineTagged) tagged")

let busyboxRef: Reference = try Reference.parse(busybox)
let busyboxTagged = "\(busyboxRef.name):testImageSaveAndLoadStdinStdout"
try doImageTag(image: busybox, newName: busyboxTagged)
let busyboxTaggedImagePresent = try isImagePresent(targetImage: busyboxTagged)
#expect(busyboxTaggedImagePresent, "expected to see image \(busyboxTagged) tagged")

// 3. save the image and output to stdout
let saveArgs = [
"image",
"save",
alpineTagged,
busyboxTagged,
]
let (stdoutData, _, error, status) = try run(arguments: saveArgs)
if status != 0 {
throw CLIError.executionFailed("command failed: \(error)")
}

// 4. remove the image through container
try doRemoveImages(images: [alpineTagged, busyboxTagged])

// 5. verify image is no longer present
let alpineImageRemoved = try !isImagePresent(targetImage: alpineTagged)
#expect(alpineImageRemoved, "expected image \(alpineTagged) to be removed")
let busyboxImageRemoved = try !isImagePresent(targetImage: busyboxTagged)
#expect(busyboxImageRemoved, "expected image \(busyboxTagged) to be removed")

// 6. load the tarball from the stdout data as stdin
let loadArgs = [
"image",
"load",
]
let (_, _, loadErr, loadStatus) = try run(arguments: loadArgs, stdin: stdoutData)
if loadStatus != 0 {
throw CLIError.executionFailed("command failed: \(loadErr)")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct TestCLIPluginErrors {
// without the APIServer started, so DefaultCommand will fail to create
// a PluginLoader and emit the improved guidance.
let cli = try CLITest()
let (_, stderr, status) = try cli.run(arguments: ["nosuchplugin"]) // non-existent plugin name
let (_, _, stderr, status) = try cli.run(arguments: ["nosuchplugin"]) // non-existent plugin name

#expect(status != 0)
#expect(stderr.contains("container system start"))
Expand Down
4 changes: 2 additions & 2 deletions Tests/CLITests/Subcommands/Run/TestCLIRunLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class TestCLIRunLifecycle: CLITest {
}
try self.waitForContainerRunning(name)

let (output, _, status) = try self.run(arguments: ["start", name])
let (_, output, _, status) = try self.run(arguments: ["start", name])
#expect(status == 0, "expected start to succeed on already running container")
#expect(output.trimmingCharacters(in: .whitespacesAndNewlines) == name, "expected output to be container name")

Expand All @@ -76,7 +76,7 @@ class TestCLIRunLifecycle: CLITest {
}
try self.waitForContainerRunning(name)

let (_, error, status) = try self.run(arguments: ["start", "-a", name])
let (_, _, error, status) = try self.run(arguments: ["start", "-a", name])
#expect(status != 0, "expected start with attach to fail on already running container")
#expect(error.contains("attach is currently unsupported on already running containers"), "expected error message about attach not supported")

Expand Down
2 changes: 1 addition & 1 deletion Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ class TestCLIRunCommand: CLITest {
}

func getDefaultDomain() throws -> String? {
let (output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")
let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedOutput == "" {
Expand Down
4 changes: 2 additions & 2 deletions Tests/CLITests/Subcommands/System/TestKernelSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class TestCLIKernelSet: CLITest {
"--recommended",
"--force",
]
let (_, error, status) = try run(arguments: arguments)
let (_, _, error, status) = try run(arguments: arguments)
if status != 0 {
throw CLIError.executionFailed("failed to reset kernel to recommended: \(error)")
}
Expand All @@ -56,7 +56,7 @@ class TestCLIKernelSet: CLITest {
]
arguments.append(contentsOf: extraArgs)

let (_, error, status) = try run(arguments: arguments)
let (_, _, error, status) = try run(arguments: arguments)
if status != 0 {
throw CLIError.executionFailed("failed to set kernel: \(error)")
}
Expand Down
Loading