Add support for reading env from named pipes (#974)

This is a fix for
[issue#956](https://github.com/apple/container/issues/956)

`FileManager.default.contents(atPath:)` returns `nil` for named pipes
(FIFOs)
and process substitutions like `/dev/fd/XX` because:
1. It expects regular files with a known size
2. Named pipes are stream-based and block until data arrives

## Solution
Use `FileHandle(forReadingFrom:)` instead, which:
- Properly handles blocking I/O
- Works with named pipes, process substitutions, and regular files
(mentioned in the
[doc](https://developer.apple.com/documentation/foundation/filehandle))

Co-authored-by: Bortniak Volodymyr <Bortnyak@users.noreply.github.com>
This commit is contained in:
Volodymyr Bortniak
2025-12-20 00:36:02 +01:00
committed by GitHub
parent 3c3a83c98a
commit 9c239aa36c
3 changed files with 111 additions and 10 deletions
+10 -8
View File
@@ -110,17 +110,19 @@ public struct Parser {
// This is a somewhat faithful Go->Swift port of Moby's envfile
// parsing in the cli:
// https://github.com/docker/cli/blob/f5a7a3c72eb35fc5ba9c4d65a2a0e2e1bd216bf2/pkg/kvfile/kvfile.go#L81
guard FileManager.default.fileExists(atPath: path) else {
throw ContainerizationError(
.notFound,
message: "envfile at \(path) not found"
)
}
guard let data = FileManager.default.contents(atPath: path) else {
let data: Data
do {
// Use FileHandle to support named pipes (FIFOs) and process substitutions
// like --env-file <(echo "KEY=value")
let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? fileHandle.close() }
data = try fileHandle.readToEnd() ?? Data()
} catch {
throw ContainerizationError(
.invalidArgument,
message: "failed to read envfile at \(path)"
message: "failed to read envfile at \(path)",
cause: error
)
}
@@ -577,6 +577,68 @@ class TestCLIRunCommand: CLITest {
}
}
@Test func testRunCommandEnvFileFromNamedPipe() throws {
do {
let name = getTestName()
let pipePath = FileManager.default.temporaryDirectory.appendingPathComponent("envfile-pipe\(UUID().uuidString)")
// create pipe
let result = mkfifo(pipePath.path(), 0o600)
guard result == 0 else {
Issue.record("failed to create named pipe: \(String(cString: strerror(errno)))")
return
}
defer {
try? FileManager.default.removeItem(at: pipePath)
}
let content = """
FOO=bar
BAR=baz
"""
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
do {
let handle = try FileHandle(forWritingTo: pipePath)
try handle.write(contentsOf: Data(content.utf8))
try handle.close()
} catch {
Issue.record(error)
return
}
group.leave()
}
try doLongRun(name: name, args: ["--env-file", pipePath.path()])
defer {
try? doStop(name: name)
}
group.wait()
let inspectResult = try inspectContainer(name)
let expected = [
"FOO=bar",
"BAR=baz",
]
for item in expected {
#expect(
inspectResult.configuration.initProcess.environment.contains(item),
"expected environment variable \(item) not found"
)
}
try doStop(name: name)
} catch {
Issue.record(error)
}
}
func getDefaultDomain() throws -> String? {
let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")
+39 -2
View File
@@ -493,10 +493,12 @@ struct ParserTest {
#expect {
_ = try Parser.envFile(path: "/nonexistent/foo_bar_baz")
} throws: { error in
guard let error = error as? ContainerizationError else {
guard let error = error as? ContainerizationError,
let cause = error.cause
else {
return false
}
return error.description.contains("not found")
return String(describing: cause).contains("No such file or directory")
}
}
@@ -579,6 +581,41 @@ struct ParserTest {
}
}
@Test
func testParseEnvFileFromNamedPipe() throws {
let pipePath = FileManager.default.temporaryDirectory
.appendingPathComponent("envfile-pipe-\(UUID().uuidString)")
// Create a named pipe (FIFO)
let result = mkfifo(pipePath.path, 0o600)
guard result == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
}
defer { try? FileManager.default.removeItem(at: pipePath) }
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
do {
let handle = try FileHandle(forWritingTo: pipePath)
try handle.write(contentsOf: "SECRET_KEY=value123\n".data(using: .utf8)!)
try handle.close()
} catch {
Issue.record(error)
}
group.leave()
}
// Read from pipe (blocks until writer connects)
let lines = try Parser.envFile(path: pipePath.path)
// Wait for write to complete
group.wait()
#expect(lines == ["SECRET_KEY=value123"])
}
// MARK: Network Parser Tests
@Test