11package com .goyeau .kubernetes .client .api
22
33import cats .effect .{Async , Resource }
4+ import cats .effect .syntax .all .*
45import cats .syntax .all .*
56import com .goyeau .kubernetes .client .KubeConfig
67import com .goyeau .kubernetes .client .api .ExecRouting .*
@@ -142,24 +143,19 @@ private[client] class NamespacedPodsApi[F[_]](
142143 F .ref(List .empty[StdErr ]),
143144 F .ref(none[ErrorOrStatus ])
144145 ).tupled.flatMap { case (stdErr, errorOrStatus) =>
145- execRequest(podName, Seq (" sh" , " -c" , s " cat ${sourceFile.toString}" ), container).flatMap { request =>
146- wsClient.connectHighLevel(request).use { connection =>
147- connection.receiveStream
148- .through(processWebSocketData)
149- .evalMapFilter[F , Chunk [Byte ]] {
150- case Left (StdOut (data)) => Chunk .array(data).some.pure[F ]
151- case Left (e : StdErr ) => stdErr.update(e :: _).as(None )
152- case Right (statusOrError) => errorOrStatus.update(_.orElse(statusOrError.some)).as(None )
153- }
154- .unchunks
155- .through(Files [F ].writeAll(destinationFile))
156- .compile
157- .drain
158- .flatMap { _ =>
159- (stdErr.get.map(_.reverse), errorOrStatus.get).tupled
160- }
146+ execStream(podName, container, Seq (" sh" , " -c" , s " cat ${sourceFile.toString}" ))
147+ .evalMapFilter[F , Chunk [Byte ]] {
148+ case Left (StdOut (data)) => Chunk .array(data).some.pure[F ]
149+ case Left (e : StdErr ) => stdErr.update(e :: _).as(None )
150+ case Right (statusOrError) => errorOrStatus.update(_.orElse(statusOrError.some)).as(None )
151+ }
152+ .unchunks
153+ .through(Files [F ].writeAll(destinationFile))
154+ .compile
155+ .drain
156+ .flatMap { _ =>
157+ (stdErr.get.map(_.reverse), errorOrStatus.get).tupled
161158 }
162- }
163159 }
164160
165161 @ deprecated(" Use upload() which uses fs2.io.file.Path" , " 0.8.2" )
@@ -179,16 +175,7 @@ private[client] class NamespacedPodsApi[F[_]](
179175 ): F [(List [StdErr ], Option [ErrorOrStatus ])] = {
180176 val mkDirResult = destinationFile.parent match {
181177 case Some (dir) =>
182- execRequest(
183- podName,
184- Seq (" sh" , " -c" , s " mkdir -p $dir" ),
185- container
186- ).flatMap { mkDirRequest =>
187- wsClient
188- .connectHighLevel(mkDirRequest)
189- .map(conn => F .delay(conn.receiveStream.through(processWebSocketData)))
190- .use(_.flatMap(foldErrorStream))
191- }
178+ foldErrorStream(execStream(podName, container, Seq (" sh" , " -c" , s " mkdir -p $dir" )))
192179 case None =>
193180 (List .empty -> None ).pure
194181 }
@@ -201,37 +188,34 @@ private[client] class NamespacedPodsApi[F[_]](
201188 )
202189
203190 val uploadFileResult =
204- uploadRequest.flatMap { uploadRequest =>
205- wsClient.connectHighLevel(uploadRequest).use { connection =>
206- val source = Files [F ].readAll(sourceFile, 4096 , Flags .Read )
207- val sendData = source
208- .mapChunks(chunk => Chunk (WSFrame .Binary (ByteVector (chunk.toChain.prepend(StdInId ).toVector))))
209- .through(connection.sendPipe)
210- val retryAttempts = 5
211- val sendWithRetry = Stream
212- .retry(sendData.compile.drain, delay = 500 .millis, nextDelay = _ * 2 , maxAttempts = retryAttempts)
213- .onError { case e =>
214- Stream .eval(Logger [F ].error(e)(s " Failed send file data after $retryAttempts attempts " ))
215- }
216-
217- val result = for {
218- signal <- Stream .eval(SignallingRef [F , Boolean ](false ))
219- dataStream = sendWithRetry *> Stream .eval(signal.set(true ))
220-
221- output = connection.receiveStream
222- .through(
223- processWebSocketData
224- )
225- .interruptWhen(signal)
226- .concurrently(dataStream)
227-
228- errors = foldErrorStream(
229- output
230- ).map { case (errors, _) => errors }
231- } yield errors
232-
233- result.compile.lastOrError.flatten
234- }
191+ uploadRequest.toResource.flatMap(wsClient.connectHighLevel).use { connection =>
192+ val source = Files [F ].readAll(sourceFile, 4096 , Flags .Read )
193+ val sendData = source
194+ .mapChunks(chunk => Chunk (WSFrame .Binary (ByteVector (chunk.toChain.prepend(StdInId ).toVector))))
195+ .through(connection.sendPipe)
196+ val retryAttempts = 5
197+ val sendWithRetry = Stream
198+ .retry(sendData.compile.drain, delay = 500 .millis, nextDelay = _ * 2 , maxAttempts = retryAttempts)
199+ .onError { case e =>
200+ Stream .eval(Logger [F ].error(e)(s " Failed send file data after $retryAttempts attempts " ))
201+ }
202+
203+ val result = for {
204+ signal <- Stream .eval(SignallingRef [F , Boolean ](false ))
205+ dataStream = sendWithRetry *> Stream .eval(signal.set(true ))
206+
207+ output = connection.receiveStream
208+ .through(skipConnectionClosedErrors)
209+ .through(processWebSocketData)
210+ .interruptWhen(signal)
211+ .concurrently(dataStream)
212+
213+ errors = foldErrorStream(
214+ output
215+ ).map { case (errors, _) => errors }
216+ } yield errors
217+
218+ result.compile.lastOrError.flatten
235219 }
236220
237221 for {
@@ -249,12 +233,15 @@ private[client] class NamespacedPodsApi[F[_]](
249233 stdout : Boolean = true ,
250234 stderr : Boolean = true ,
251235 tty : Boolean = false
252- ): Resource [F , F [Stream [F , Either [ExecStream , ErrorOrStatus ]]]] =
253- Resource .eval(execRequest(podName, command, container, stdin, stdout, stderr, tty)).flatMap { request =>
254- wsClient.connectHighLevel(request).map { connection =>
255- F .delay(connection.receiveStream.through(processWebSocketData))
236+ ): Stream [F , Either [ExecStream , ErrorOrStatus ]] =
237+ Stream
238+ .eval(execRequest(podName, command, container, stdin, stdout, stderr, tty))
239+ .flatMap(request => Stream .resource(wsClient.connectHighLevel(request)))
240+ .flatMap { connection =>
241+ connection.receiveStream
242+ .through(skipConnectionClosedErrors)
243+ .through(processWebSocketData)
256244 }
257- }
258245
259246 def exec (
260247 podName : String ,
@@ -265,11 +252,26 @@ private[client] class NamespacedPodsApi[F[_]](
265252 stderr : Boolean = true ,
266253 tty : Boolean = false
267254 ): F [(List [ExecStream ], Option [ErrorOrStatus ])] =
268- execStream(podName, container, command, stdin, stdout, stderr, tty).use(_.flatMap(foldStream))
255+ foldStream(execStream(podName, container, command, stdin, stdout, stderr, tty))
256+
257+ private def skipConnectionClosedErrors : Pipe [F , WSDataFrame , WSDataFrame ] =
258+ _.map(_.some)
259+ .recover {
260+ // Need to handle (and ignore) this exception
261+ //
262+ // Because of the "conflict" between the http4s WS client and
263+ // the underlying JDK WS client (which are both high-level clients)
264+ // an extra "Close" frame gets sent to the server, potentially
265+ // after the TCP connection is closed, which causes this exception.
266+ //
267+ // This will be solved in a later version of the http4s (core or jdk).
268+ case e : java.io.IOException if e.getMessage == " closed output" => none
269+ }
270+ .unNone
269271
270272 private def foldStream (
271273 stdoutStream : Stream [F , Either [ExecStream , ErrorOrStatus ]]
272- ) =
274+ ): F [( List [ ExecStream ], Option [ ErrorOrStatus ])] =
273275 stdoutStream.compile.fold((List .empty[ExecStream ], none[ErrorOrStatus ])) { case ((accEvents, accStatus), data) =>
274276 data match {
275277 case Left (event) =>
@@ -281,7 +283,7 @@ private[client] class NamespacedPodsApi[F[_]](
281283
282284 private def foldErrorStream (
283285 stdoutStream : Stream [F , Either [ExecStream , ErrorOrStatus ]]
284- ) =
286+ ): F [( List [ StdErr ], Option [ ErrorOrStatus ])] =
285287 stdoutStream.compile.fold((List .empty[StdErr ], none[ErrorOrStatus ])) { case ((accEvents, accStatus), data) =>
286288 data match {
287289 case Left (event) =>
0 commit comments