Skip to content

Commit b230226

Browse files
committed
[image-save-load]: support for stdin/stdout
1 parent e49a563 commit b230226

File tree

3 files changed

+116
-6
lines changed

3 files changed

+116
-6
lines changed

Sources/ContainerCommands/Image/ImageLoad.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,38 @@ extension Application {
3434
transform: { str in
3535
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
3636
})
37-
var input: String
37+
var input: String?
3838

3939
@OptionGroup
4040
var global: Flags.Global
4141

4242
public func run() async throws {
43-
guard FileManager.default.fileExists(atPath: input) else {
44-
print("File does not exist \(input)")
43+
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).tar")
44+
defer {
45+
try? FileManager.default.removeItem(at: tempFile)
46+
}
47+
48+
// Read from stdin
49+
if input == nil {
50+
guard FileManager.default.createFile(atPath: tempFile.path(), contents: nil) else {
51+
throw ContainerizationError(.internalError, message: "unable to create temporary file")
52+
}
53+
54+
guard let outputHandle = try? FileHandle(forWritingTo: tempFile) else {
55+
throw ContainerizationError(.internalError, message: "unable to open temporary file for writing")
56+
}
57+
58+
let bufferSize = 4096
59+
while true {
60+
let chunk = FileHandle.standardInput.readData(ofLength: bufferSize)
61+
if chunk.isEmpty { break }
62+
outputHandle.write(chunk)
63+
}
64+
try outputHandle.close()
65+
}
66+
67+
guard FileManager.default.fileExists(atPath: input ?? tempFile.path()) else {
68+
print("File does not exist \(input ?? tempFile.path())")
4569
Application.exit(withError: ArgumentParser.ExitCode(1))
4670
}
4771

@@ -57,7 +81,7 @@ extension Application {
5781
progress.start()
5882

5983
progress.set(description: "Loading tar archive")
60-
let loaded = try await ClientImage.load(from: input)
84+
let loaded = try await ClientImage.load(from: input ?? tempFile.path())
6185

6286
let taskManager = ProgressTaskCoordinator()
6387
let unpackTask = await taskManager.startTask()

Sources/ContainerCommands/Image/ImageSave.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension Application {
4646
transform: { str in
4747
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
4848
})
49-
var output: String
49+
var output: String?
5050

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

9292
}
93-
try await ClientImage.save(references: references, out: output, platform: p)
93+
94+
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).tar")
95+
defer {
96+
try? FileManager.default.removeItem(at: tempFile)
97+
}
98+
99+
guard FileManager.default.createFile(atPath: tempFile.path(), contents: nil) else {
100+
throw ContainerizationError(.internalError, message: "unable to create temporary file")
101+
}
102+
103+
try await ClientImage.save(references: references, out: output ?? tempFile.path(), platform: p)
104+
105+
// Write to stdout
106+
if output == nil {
107+
guard let outputHandle = try? FileHandle(forReadingFrom: tempFile) else {
108+
throw ContainerizationError(.internalError, message: "unable to open temporary file for reading")
109+
}
110+
111+
let bufferSize = 4096
112+
while true {
113+
let chunk = outputHandle.readData(ofLength: bufferSize)
114+
if chunk.isEmpty { break }
115+
FileHandle.standardOutput.write(chunk)
116+
}
117+
try outputHandle.close()
118+
}
94119

95120
progress.finish()
96121
for reference in references {

Tests/CLITests/Subcommands/Images/TestCLIImages.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,4 +359,65 @@ extension TestCLIImagesCommand {
359359
return
360360
}
361361
}
362+
363+
@Test func testImageSaveAndLoadStdinStdout() throws {
364+
do {
365+
// 1. pull image
366+
try doPull(imageName: alpine)
367+
try doPull(imageName: busybox)
368+
369+
// 2. Tag image so we can safely remove later
370+
let alpineRef: Reference = try Reference.parse(alpine)
371+
let alpineTagged = "\(alpineRef.name):testImageSaveAndLoadStdinStdout"
372+
try doImageTag(image: alpine, newName: alpineTagged)
373+
let alpineTaggedImagePresent = try isImagePresent(targetImage: alpineTagged)
374+
#expect(alpineTaggedImagePresent, "expected to see image \(alpineTagged) tagged")
375+
376+
let busyboxRef: Reference = try Reference.parse(busybox)
377+
let busyboxTagged = "\(busyboxRef.name):testImageSaveAndLoadStdinStdout"
378+
try doImageTag(image: busybox, newName: busyboxTagged)
379+
let busyboxTaggedImagePresent = try isImagePresent(targetImage: busyboxTagged)
380+
#expect(busyboxTaggedImagePresent, "expected to see image \(busyboxTagged) tagged")
381+
382+
// 3. save the image and output to stdout
383+
let saveArgs = [
384+
"image",
385+
"save",
386+
alpineTagged,
387+
busyboxTagged,
388+
]
389+
let (stdoutData, _, error, status) = try run(arguments: saveArgs)
390+
if status != 0 {
391+
throw CLIError.executionFailed("command failed: \(error)")
392+
}
393+
394+
// 4. remove the image through container
395+
try doRemoveImages(images: [alpineTagged, busyboxTagged])
396+
397+
// 5. verify image is no longer present
398+
let alpineImageRemoved = try !isImagePresent(targetImage: alpineTagged)
399+
#expect(alpineImageRemoved, "expected image \(alpineTagged) to be removed")
400+
let busyboxImageRemoved = try !isImagePresent(targetImage: busyboxTagged)
401+
#expect(busyboxImageRemoved, "expected image \(busyboxTagged) to be removed")
402+
403+
// 6. load the tarball from the stdout data as stdin
404+
let loadArgs = [
405+
"image",
406+
"load",
407+
]
408+
let (_, _, loadErr, loadStatus) = try run(arguments: loadArgs, stdin: stdoutData)
409+
if loadStatus != 0 {
410+
throw CLIError.executionFailed("command failed: \(loadErr)")
411+
}
412+
413+
// 7. verify image is in the list again
414+
let alpineImagePresent = try isImagePresent(targetImage: alpineTagged)
415+
#expect(alpineImagePresent, "expected \(alpineTagged) to be present")
416+
let busyboxImagePresent = try isImagePresent(targetImage: busyboxTagged)
417+
#expect(busyboxImagePresent, "expected \(busyboxTagged) to be present")
418+
} catch {
419+
Issue.record("failed to save and load image \(error)")
420+
return
421+
}
422+
}
362423
}

0 commit comments

Comments
 (0)