scala-k8s

Usage

This library is currently available for Scala binary versions 2.12, 2.13 and 3.2 on JVM/JS/Native.
This library is architecured in a microkernel fashion and all the main kubernetes stuff are implemented/generated in pure scala, and integration modules are provided separately.
main modules are:

libraryDependencies ++= Seq(
  "dev.hnaderi" %% "scala-k8s-objects" % "0.17.0", // JVM, JS, Native ; raw k8s objects
  "dev.hnaderi" %% "scala-k8s-client" % "0.17.0", // JVM, JS, Native ; k8s client kernel and requests
  )

The following integrations are currently available:

libraryDependencies ++= Seq(
  "dev.hnaderi" %% "scala-k8s-http4s-ember" % "0.17.0", // JVM, JS, Native ; http4s ember client integration
  "dev.hnaderi" %% "scala-k8s-http4s-netty" % "0.17.0", // JVM ; http4s netty client integration
  "dev.hnaderi" %% "scala-k8s-http4s-blaze" % "0.17.0", // JVM; http4s blaze client integration
  "dev.hnaderi" %% "scala-k8s-http4s-jdk" % "0.17.0", // JVM; http4s jdk-client integration
  "dev.hnaderi" %% "scala-k8s-http4s" % "0.17.0", // JVM, JS, Native ; http4s core and fs2 integration
  "dev.hnaderi" %% "scala-k8s-zio" % "0.17.0", // JVM ; ZIO native integration using zio-http and zio-json 
  "dev.hnaderi" %% "scala-k8s-sttp" % "0.17.0", // JVM, JS, Native ; sttp integration using jawn parser
  "dev.hnaderi" %% "scala-k8s-circe" % "0.17.0", // JVM, JS ; circe integration
  "dev.hnaderi" %% "scala-k8s-json4s" % "0.17.0", // JVM, JS, Native; json4s integration
  "dev.hnaderi" %% "scala-k8s-spray-json" % "0.17.0", // JVM ; spray-json integration
  "dev.hnaderi" %% "scala-k8s-play-json" % "0.17.0", // JVM ; play-json integration
  "dev.hnaderi" %% "scala-k8s-zio-json" % "0.17.0", // JVM, JS ; zio-json integration
  "dev.hnaderi" %% "scala-k8s-jawn" % "0.17.0", // JVM, JS, Native ; jawn integration
  "dev.hnaderi" %% "scala-k8s-manifests" % "0.17.0", // JVM, JS, Native ; yaml manifest reading and generation
  "dev.hnaderi" %% "scala-k8s-scalacheck" % "0.17.0" // JVM, JS, Native; scalacheck instances
)

Manifest and object generation

first off, we'll import the following

import dev.hnaderi.k8s._  // base packages
import dev.hnaderi.k8s.implicits._  // implicit coversions and helpers
import dev.hnaderi.k8s.manifest._  // manifest syntax

every other object definition is under kubernetes packages io.k8s as specified in the spec, you should rely on IDE auto import for those.

Now we can define any kubernetes object

ConfigMap example

val config = ConfigMap(
  metadata = ObjectMeta(
    name = "example",
    namespace = "staging",
    labels = Map(
      Labels.name("example"),
      Labels.instance("one")
    )
  ),
  data = DataMap(
    "some config" -> "some value",
    "config file" -> Data.file(".envrc")
  ),
  binaryData = DataMap.binary(
    "blob" -> Data.file(".gitignore"),
    "blob2" -> Paths.get(".scalafmt.conf"),
    "other inline data" -> "some other data"
  )
)

or even from a whole directory, like kubectl

val config2 = ConfigMap(
  data = DataMap.fromDir(new File("objects/src/test/resources/data"))
)

Deployment example

val deployment = Deployment(
  metadata = ObjectMeta(
    name = "example",
    namespace = "staging"
  ),
  spec = DeploymentSpec(
    selector = LabelSelector(matchLabels = Map("app" -> "example")),
    template = PodTemplateSpec(
      spec = PodSpec(
        containers = Seq(
          Container(
            name = "abc",
            image = "hello-world:latest"
          )
        )
      )
    )
  )
)

Service example

val service = Service(
  metadata = ObjectMeta(
    name = "example",
    namespace = ""
  ),
  spec = ServiceSpec(
    selector = Map("app" -> "example"),
    ports = Seq(ServicePort(port = 80, targetPort = 8080, name = "http"))
  )
)

Manifest example

Now you can merge all of your kubernetes resource definitions in to one manifest

val all : Seq[KObject] = Seq(service, config, deployment)
val manifest = all.asManifest

which will output like this

println(manifest)
// apiVersion: v1
// kind: Service
// metadata:
//   namespace: ''
//   name: example
// spec:
//   selector:
//     app: example
//   ports: [{targetPort: 8080, name: http, port: 80}]
// ---
// apiVersion: v1
// kind: ConfigMap
// metadata:
//   namespace: staging
//   labels:
//     app.kubernetes.io/name: example
//     app.kubernetes.io/instance: one
//   name: example
// binaryData:
//   blob: IyBzYnQKdGFyZ2V0Lwpwcm9qZWN0L3BsdWdpbnMvcHJvamVjdC8KYm9vdC8KbGliX21hbmFnZWQvCnNyY19tYW5hZ2VkLwoKIyB2aW0KKi5zdz8KCiMgaW50ZWxsaWoKLmlkZWEvCgojIGlnbm9yZSBbY2VddGFncyBmaWxlcwp0YWdzCgojIG1ldGFscwoubWV0YWxzLwouYnNwLwouYmxvb3AvCm1ldGFscy5zYnQKLnZzY29kZQoKIyBucG0Kbm9kZV9tb2R1bGVzLwovc3BlY2lmaWNhdGlvbnMvCi8uZGlyZW52Lwo=
//   blob2: dmVyc2lvbiA9IDMuOC4xCnJ1bm5lci5kaWFsZWN0ID0gc2NhbGEyMTMKZmlsZU92ZXJyaWRlIHsKICAiZ2xvYjoqKi9saWIvc3JjL21haW4vc2NhbGEvKioiIHsKICAgICBydW5uZXIuZGlhbGVjdCA9IHNjYWxhMwogIH0KfQo=
//   other inline data: c29tZSBvdGhlciBkYXRh
// data:
//   some config: some value
//   config file: |
//     use flake
// ---
// apiVersion: apps/v1
// kind: Deployment
// metadata:
//   namespace: staging
//   name: example
// spec:
//   selector:
//     matchLabels:
//       app: example
//   template:
//     spec:
//       containers: [{image: 'hello-world:latest', name: abc}]
//

Helpers

You can also use helpers to manipulate data models easily

val config3 = config
  .addData("new-key" -> "new value")
  .withImmutable(true)
  .mapMetadata(_.withName("new-config").withNamespace("production"))

All fields have the following helper methods:

println(config3.asManifest)
// apiVersion: v1
// kind: ConfigMap
// metadata:
//   namespace: production
//   labels:
//     app.kubernetes.io/name: example
//     app.kubernetes.io/instance: one
//   name: new-config
// binaryData:
//   blob: IyBzYnQKdGFyZ2V0Lwpwcm9qZWN0L3BsdWdpbnMvcHJvamVjdC8KYm9vdC8KbGliX21hbmFnZWQvCnNyY19tYW5hZ2VkLwoKIyB2aW0KKi5zdz8KCiMgaW50ZWxsaWoKLmlkZWEvCgojIGlnbm9yZSBbY2VddGFncyBmaWxlcwp0YWdzCgojIG1ldGFscwoubWV0YWxzLwouYnNwLwouYmxvb3AvCm1ldGFscy5zYnQKLnZzY29kZQoKIyBucG0Kbm9kZV9tb2R1bGVzLwovc3BlY2lmaWNhdGlvbnMvCi8uZGlyZW52Lwo=
//   blob2: dmVyc2lvbiA9IDMuOC4xCnJ1bm5lci5kaWFsZWN0ID0gc2NhbGEyMTMKZmlsZU92ZXJyaWRlIHsKICAiZ2xvYjoqKi9saWIvc3JjL21haW4vc2NhbGEvKioiIHsKICAgICBydW5uZXIuZGlhbGVjdCA9IHNjYWxhMwogIH0KfQo=
//   other inline data: c29tZSBvdGhlciBkYXRh
// immutable: true
// data:
//   some config: some value
//   config file: |
//     use flake
//   new-key: new value
//

Client

Scala k8s provides a kubernetes client built on top of a generic http client, this allows us to use different http clients based on project ecosystem and other considerations. Being modular and not depending on a specific environment opens the door to extensibility, and also means it does restrict you in any imaginable way and you can choose whatever you want, configure however you want!

The following are some examples that use kubectl proxy for simplicity sake.

Http4s based client

http4s based client support all APIs.

import cats.effect._
import dev.hnaderi.k8s.circe._
import dev.hnaderi.k8s.client._
import dev.hnaderi.k8s.client.http4s.EmberKubernetesClient
import io.circe.Json
import org.http4s.circe._

val buildClient = EmberKubernetesClient[IO].defaultConfig[Json]
// buildClient: Resource[IO, http4s.package.KClient[IO]] = Bind(Bind(Bind(Pure(()),cats.effect.kernel.Resource$$Lambda$11295/0x0000000803112840@5545ef2d),cats.effect.kernel.Resource$$Lambda$11296/0x0000000803128040@26619df2),cats.effect.kernel.Resource$$Lambda$11297/0x0000000803128840@1d1fe7c1)
 
val getNodes = buildClient.use(APIs.nodes.list().send)
// getNodes: IO[io.k8s.api.core.v1.NodeList] = IO(...)

val watchNodes = fs2.Stream.resource(buildClient).flatMap(APIs.nodes.list().listen)
// watchNodes: fs2.Stream[[x]IO[x], WatchEvent[io.k8s.api.core.v1.Node]] = Stream(..)

val getConfigMaps = 
  buildClient.use(client=>
    APIs
      .namespace("kube-system")
      .configmaps
      .get("kube-proxy")
      .send(client)
  )
// getConfigMaps: IO[ConfigMap] = IO(...)

ZIO based client

Currently, ZIO based client does not support streaming watch APIs, it will support as soon as zio-http supports streaming responses

import dev.hnaderi.k8s.client.APIs
import dev.hnaderi.k8s.client.ZIOKubernetesClient

val client = ZIOKubernetesClient.make("http://localhost:8001")
val nodes = ZIOKubernetesClient.send(APIs.nodes.list())

Sttp based client

import dev.hnaderi.k8s.circe._
import dev.hnaderi.k8s.client.APIs
import dev.hnaderi.k8s.client.SttpJdkURLClientBuilder
import sttp.client3.circe._

val client = SttpJdkURLClientBuilder.defaultConfig[Json]

val nodes = APIs.nodes.list().send(client)
nodes.body.items.flatMap(_.metadata).flatMap(_.name).foreach(println)

API calls

Working with requests

Requests are plain data, so you can manipulate or pass them like any normal data

import dev.hnaderi.k8s.client.APIs

val sysConfig = APIs
  .namespace("kube-system")
  .configmaps
// sysConfig: apis.corev1.ConfigMapAPI = ConfigMapAPI("kube-system")

val defaultConfig = sysConfig.copy(namespace = "default")
// defaultConfig: apis.corev1.ConfigMapAPI = ConfigMapAPI("default")

Advanced requests

For doing simple strategical merge patches:

val patch1 = APIs
  .namespace("default")
  .configmaps
  .patch(
    "test",
    ConfigMap(metadata = ObjectMeta(labels = Map("new" -> "label")))
  )
// patch1: apis.corev1.ConfigMapAPI.GenericPatch[ConfigMap] = GenericPatch(test,default,ConfigMap(None,None,None,Some(ObjectMeta(None,None,None,None,None,None,None,None,None,None,None,Some(Map(new -> label)),None,None,None))),StrategicMerge,None,None,None,None)

For doing Json patch:

// You need to import pointer instances
import dev.hnaderi.k8s.client.implicits._

val patch2 = APIs
  .namespace("default")
  .configmaps
  .jsonPatch("test")(
    JsonPatch[ConfigMap].builder
      .add(_.metadata.labels.at("new"), "label")
      .move(_.metadata.labels.at("a"), _.metadata.labels.at("b"))
      .remove(_.data.at("to-delete"))
  )
// patch2: apis.corev1.ConfigMapAPI.GenericPatch[JsonPatch[ConfigMap, io.k8s.api.core.v1.ConfigMapPointer]] = GenericPatch(test,default,dev.hnaderi.k8s.client.JsonPatch@741fe7bc,JsonPatch,None,None,None,None)

Server side apply:

val patch3 = APIs
  .namespace("default")
  .configmaps
  .serverSideApply("test", ConfigMap(), fieldManager = "my-operator")
// patch3: apis.corev1.ConfigMapAPI.ServerSideApply = ServerSideApply(test,default,ConfigMap(None,None,None,None),my-operator,None,None,None)

Or json merge patches:

val patch4 = APIs
  .namespace("default")
  .configmaps
  .patch(
    "test",
    ConfigMap(metadata = ObjectMeta(labels = Map("new" -> "label"))),
    patch = PatchType.Merge
  )
// patch4: apis.corev1.ConfigMapAPI.GenericPatch[ConfigMap] = GenericPatch(test,default,ConfigMap(None,None,None,Some(ObjectMeta(None,None,None,None,None,None,None,None,None,None,None,Some(Map(new -> label)),None,None,None))),Merge,None,None,None,None)

Your own custom type merge, for times that you need all the control:

type CustomMerge = String // Your custom object to be send to kubernetes
val customMergeObject : CustomMerge = ""
// customMergeObject: CustomMerge = 
// You need to define encoder for your type
// implicit val customMergeObjectEncoder : Encoder[CustomMerge] = ???

val patch5 = APIs
  .namespace("default")
  .configmaps
  .patchGeneric(
    "test",
    customMergeObject,
    patch = PatchType.Merge
  )
// patch5: apis.corev1.ConfigMapAPI.GenericPatch[CustomMerge] = GenericPatch(test,default,,Merge,None,None,None,None)

Implementing new requests

you can also implement your own requests easily, however if you need a request that is widely used and is standard, please open an issue or better, a pull request, so everyone can use it.

import dev.hnaderi.k8s.client._

type CustomResource = String
case class MyCustomRequest(name: String) extends GetRequest[CustomResource](
  s"/apis/my.custom-resource.io/$name"
)