mirror of
https://github.com/apple/container.git
synced 2026-07-13 04:57:08 +00:00
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:
committed by
GitHub
parent
3c3a83c98a
commit
9c239aa36c
@@ -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)")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user