From 17f9c35c3f2c1a7626dce9d76133f52159a8aea1 Mon Sep 17 00:00:00 2001 From: yurique Date: Sat, 25 Nov 2023 01:48:29 +0100 Subject: [PATCH] better raw requests (#260) * better raw requests * readme cruft --- README.md | 85 +++++++++++++++++++ .../kubernetes/client/KubernetesClient.scala | 22 +---- .../goyeau/kubernetes/client/api/RawApi.scala | 44 ++++++++++ .../kubernetes/client/api/RawApiTest.scala | 70 +++++++++++++++ 4 files changed, 201 insertions(+), 20 deletions(-) create mode 100644 kubernetes-client/src/com/goyeau/kubernetes/client/api/RawApi.scala create mode 100644 kubernetes-client/test/src/com/goyeau/kubernetes/client/api/RawApiTest.scala diff --git a/README.md b/README.md index 77f59033..6c3a628f 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,51 @@ kubernetesClient.use { client => } ``` +### Raw requests + +In case a particular K8S API endpoint is not explicitly supported by this library, there is an escape hatch +that you can use in order to run a raw request or open a raw WS connection. + +Here's an example of how you can get a list of nodes using a raw request: + +```scala +import cats.effect.* +import org.http4s.implicits.* +import com.goyeau.kubernetes.client.* +import org.http4s.* + +val kubernetesClient: KubernetesClient[IO] = ??? + +val response: IO[(Status, String)] = + kubernetesClient + .raw.runRequest( + Request[IO]( + uri = uri"/api" / "v1" / "nodes" + ) + ) + .use { response => + response.bodyText.foldMonoid.compile.lastOrError.map { body => + (response.status, body) + } + } +``` + +Similarly, you can open a WS connection (`org.http4s.jdkhttpclient.WSConnectionHighLevel`): + +```scala +import cats.effect.* +import org.http4s.implicits.* +import com.goyeau.kubernetes.client.* +import org.http4s.* +import org.http4s.jdkhttpclient.* + +val connection: Resource[IO, WSConnectionHighLevel[IO]] = + kubernetesClient.raw.connectWS( + WSRequest( + uri = (uri"/api" / "v1" / "my-custom-thing") +? ("watch" -> "true") + ) + ) +``` ## Development @@ -219,6 +264,46 @@ kubernetesClient.use { client => - Java 11 or higher - Docker +### IntelliJ + +Generate a BSP configuration: + +```shell +./mill mill.bsp.BSP/install +``` + +### Compiling + +```shell +./mill kubernetes-client[2.13.10].compile +``` + +### Running the tests + +All tests: + +```shell +./mill kubernetes-client[2.13.10].test +``` + +A specific test: + +```shell +./mill kubernetes-client[2.13.10].test.testOnly 'com.goyeau.kubernetes.client.api.PodsApiTest' +``` + +[minikube](https://minikube.sigs.k8s.io/docs/) has to be installed and running. + +### Before opening a PR: + +Check and fix formatting: + +```shell +./mill __.style +``` + + + ## Related projects diff --git a/kubernetes-client/src/com/goyeau/kubernetes/client/KubernetesClient.scala b/kubernetes-client/src/com/goyeau/kubernetes/client/KubernetesClient.scala index 337a0af0..812d3fa1 100644 --- a/kubernetes-client/src/com/goyeau/kubernetes/client/KubernetesClient.scala +++ b/kubernetes-client/src/com/goyeau/kubernetes/client/KubernetesClient.scala @@ -7,12 +7,10 @@ import com.goyeau.kubernetes.client.api.* import com.goyeau.kubernetes.client.crd.{CrdContext, CustomResource, CustomResourceList} import com.goyeau.kubernetes.client.util.SslContexts import com.goyeau.kubernetes.client.util.cache.{AuthorizationParse, ExecToken} -import com.goyeau.kubernetes.client.operation.* import io.circe.{Decoder, Encoder} -import org.http4s.Request import org.http4s.client.Client import org.http4s.headers.Authorization -import org.http4s.jdkhttpclient.{JdkHttpClient, JdkWSClient, WSClient, WSRequest} +import org.http4s.jdkhttpclient.{JdkHttpClient, JdkWSClient, WSClient} import org.typelevel.log4cats.Logger import java.net.http.HttpClient @@ -57,6 +55,7 @@ class KubernetesClient[F[_]: Async: Logger]( lazy val ingresses: IngressessApi[F] = new IngressessApi(httpClient, config, authorization) lazy val leases: LeasesApi[F] = new LeasesApi(httpClient, config, authorization) lazy val nodes: NodesApi[F] = new NodesApi(httpClient, config, authorization) + lazy val raw: RawApi[F] = new RawApi[F](httpClient, wsClient, config, authorization) def customResources[A: Encoder: Decoder, B: Encoder: Decoder](context: CrdContext)(implicit listDecoder: Decoder[CustomResourceList[A, B]], @@ -64,23 +63,6 @@ class KubernetesClient[F[_]: Async: Logger]( decoder: Decoder[CustomResource[A, B]] ) = new CustomResourcesApi[F, A, B](httpClient, config, authorization, context) - def customRequest( - request: Request[F] - ): F[Request[F]] = - Request[F]( - method = request.method, - uri = config.server.resolve(request.uri), - httpVersion = request.httpVersion, - headers = request.headers, - body = request.body, - attributes = request.attributes - ).withOptionalAuthorization(authorization) - - def customRequest(request: WSRequest): F[WSRequest] = - request - .copy(uri = config.server.resolve(request.uri)) - .withOptionalAuthorization(authorization) - } object KubernetesClient { diff --git a/kubernetes-client/src/com/goyeau/kubernetes/client/api/RawApi.scala b/kubernetes-client/src/com/goyeau/kubernetes/client/api/RawApi.scala new file mode 100644 index 00000000..ebb6b525 --- /dev/null +++ b/kubernetes-client/src/com/goyeau/kubernetes/client/api/RawApi.scala @@ -0,0 +1,44 @@ +package com.goyeau.kubernetes.client.api + +import cats.effect.syntax.all.* +import cats.effect.{Async, Resource} +import com.goyeau.kubernetes.client.KubeConfig +import com.goyeau.kubernetes.client.operation.* +import org.http4s.client.Client +import org.http4s.headers.Authorization +import org.http4s.jdkhttpclient.{WSClient, WSConnectionHighLevel, WSRequest} +import org.http4s.{Request, Response} + +private[client] class RawApi[F[_]]( + httpClient: Client[F], + wsClient: WSClient[F], + config: KubeConfig[F], + authorization: Option[F[Authorization]] +)(implicit F: Async[F]) { + + def runRequest( + request: Request[F] + ): Resource[F, Response[F]] = + Request[F]( + method = request.method, + uri = config.server.resolve(request.uri), + httpVersion = request.httpVersion, + headers = request.headers, + body = request.body, + attributes = request.attributes + ).withOptionalAuthorization(authorization) + .toResource + .flatMap(httpClient.run) + + def connectWS( + request: WSRequest + ): Resource[F, WSConnectionHighLevel[F]] = + request + .copy(uri = config.server.resolve(request.uri)) + .withOptionalAuthorization(authorization) + .toResource + .flatMap { request => + wsClient.connectHighLevel(request) + } + +} diff --git a/kubernetes-client/test/src/com/goyeau/kubernetes/client/api/RawApiTest.scala b/kubernetes-client/test/src/com/goyeau/kubernetes/client/api/RawApiTest.scala new file mode 100644 index 00000000..cb082a5b --- /dev/null +++ b/kubernetes-client/test/src/com/goyeau/kubernetes/client/api/RawApiTest.scala @@ -0,0 +1,70 @@ +package com.goyeau.kubernetes.client.api + +import cats.syntax.all.* +import cats.effect.unsafe.implicits.global +import cats.effect.{Async, IO} +import com.goyeau.kubernetes.client.KubernetesClient +import com.goyeau.kubernetes.client.Utils.retry +import com.goyeau.kubernetes.client.api.ExecStream.{StdErr, StdOut} +import com.goyeau.kubernetes.client.operation.* +import fs2.io.file.{Files, Path} +import fs2.{text, Stream} +import io.k8s.api.core.v1.* +import io.k8s.apimachinery.pkg.apis.meta.v1 +import io.k8s.apimachinery.pkg.apis.meta.v1.{ListMeta, ObjectMeta} +import munit.FunSuite +import org.http4s.{Request, Status, Uri} +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger + +import java.nio.file.Files as JFiles +import scala.util.Random +import org.http4s.implicits.* +import org.http4s.jdkhttpclient.WSConnectionHighLevel + +class RawApiTest extends FunSuite with MinikubeClientProvider[IO] with ContextProvider { + + implicit override lazy val F: Async[IO] = IO.asyncForIO + implicit override lazy val logger: Logger[IO] = Slf4jLogger.getLogger[IO] + + // MinikubeClientProvider will create a namespace with this name, even though it's not used in this test + override lazy val resourceName: String = "raw-api-tests" + + test("list nodes with raw requests") { + kubernetesClient + .use { implicit client => + for { + response <- client.raw + .runRequest( + Request[IO]( + uri = uri"/api" / "v1" / "nodes" + ) + ) + .use { response => + response.bodyText.foldMonoid.compile.lastOrError.map { body => + (response.status, body) + } + } + (status, body) = response + _ = assertEquals( + status, + Status.Ok, + s"non 200 status for get nodes raw request" + ) + nodeList <- F.fromEither( + io.circe.parser.decode[NodeList](body) + ) + _ = assert( + nodeList.kind.contains("NodeList"), + "wrong .kind in the response" + ) + _ = assert( + nodeList.items.nonEmpty, + "empty node list" + ) + } yield () + } + .unsafeRunSync() + } + +}