Skip to content
Merged
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
34 changes: 29 additions & 5 deletions Sources/ContainerCommands/Image/ImageLoad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,39 @@ 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)")
Application.exit(withError: ArgumentParser.ExitCode(1))
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).tar")
defer {
try? FileManager.default.removeItem(at: tempFile)
}

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

guard let fileHandle = 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 }
fileHandle.write(chunk)
}
try fileHandle.close()
} else {
guard FileManager.default.fileExists(atPath: input!) else {
print("File does not exist \(input!)")
Application.exit(withError: ArgumentParser.ExitCode(1))
}
}

let progressConfig = try ProgressConfig(
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
30 changes: 28 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 @@ -88,9 +88,35 @@ extension Application {

guard images.count == references.count else {
throw ContainerizationError(.invalidArgument, message: "failed to save image(s)")
}

// Write to stdout; otherwise write to the output file
if output == nil {
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: tempFile.path(), platform: p)

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

let bufferSize = 4096
while true {
let chunk = fileHandle.readData(ofLength: bufferSize)
if chunk.isEmpty { break }
FileHandle.standardOutput.write(chunk)
}
try fileHandle.close()
} else {
try await ClientImage.save(references: references, out: output!, platform: p)
}
try await ClientImage.save(references: references, out: output, platform: p)

progress.finish()
for reference in references {
Expand Down
61 changes: 61 additions & 0 deletions Tests/CLITests/Subcommands/Images/TestCLIImages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,4 +359,65 @@ extension TestCLIImagesCommand {
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)")
}

// 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
}
}
}