diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 6f985584..5397c06a 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -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 ) } diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift index 5bba92b0..13149fc4 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift @@ -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)") diff --git a/Tests/ContainerClientTests/ParserTest.swift b/Tests/ContainerClientTests/ParserTest.swift index 8868e1c1..961ae6fb 100644 --- a/Tests/ContainerClientTests/ParserTest.swift +++ b/Tests/ContainerClientTests/ParserTest.swift @@ -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