Skip to content

Commit

Permalink
better raw requests (#260)
Browse files Browse the repository at this point in the history
* better raw requests

* readme cruft
  • Loading branch information
yurique committed Nov 25, 2023
1 parent 63c6f13 commit 17f9c35
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 20 deletions.
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,30 +55,14 @@ 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]],
encoder: Encoder[CustomResource[A, B]],
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}

}
Original file line number Diff line number Diff line change
@@ -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()
}

}

0 comments on commit 17f9c35

Please sign in to comment.