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 designed 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:
objects
raw kubernetes objects, which has no dependencyclient
raw kubernetes client and requests, requests can also be extended in user land easily!
libraryDependencies ++= Seq(
"dev.hnaderi" %% "scala-k8s-objects" % "0.21.0", // JVM, JS, Native ; raw k8s objects
"dev.hnaderi" %% "scala-k8s-client" % "0.21.0", // JVM, JS, Native ; k8s client kernel and requests
)
The following integrations are currently available:
libraryDependencies ++= Seq(
"dev.hnaderi" %% "scala-k8s-http4s-ember" % "0.21.0", // JVM, JS, Native ; http4s ember client integration
"dev.hnaderi" %% "scala-k8s-http4s-netty" % "0.21.0", // JVM ; http4s netty client integration
"dev.hnaderi" %% "scala-k8s-http4s-blaze" % "0.21.0", // JVM; http4s blaze client integration
"dev.hnaderi" %% "scala-k8s-http4s-jdk" % "0.21.0", // JVM; http4s jdk-client integration
"dev.hnaderi" %% "scala-k8s-http4s" % "0.21.0", // JVM, JS, Native ; http4s core and fs2 integration
"dev.hnaderi" %% "scala-k8s-zio" % "0.21.0", // JVM ; ZIO native integration using zio-http and zio-json
"dev.hnaderi" %% "scala-k8s-sttp" % "0.21.0", // JVM, JS, Native ; sttp integration using jawn parser
"dev.hnaderi" %% "scala-k8s-circe" % "0.21.0", // JVM, JS ; circe integration
"dev.hnaderi" %% "scala-k8s-json4s" % "0.21.0", // JVM, JS, Native; json4s integration
"dev.hnaderi" %% "scala-k8s-spray-json" % "0.21.0", // JVM ; spray-json integration
"dev.hnaderi" %% "scala-k8s-play-json" % "0.21.0", // JVM ; play-json integration
"dev.hnaderi" %% "scala-k8s-zio-json" % "0.21.0", // JVM, JS ; zio-json integration
"dev.hnaderi" %% "scala-k8s-jawn" % "0.21.0", // JVM, JS, Native ; jawn integration
"dev.hnaderi" %% "scala-k8s-manifests" % "0.21.0", // JVM, JS, Native ; yaml manifest reading and generation
"dev.hnaderi" %% "scala-k8s-scalacheck" % "0.21.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 conversions 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 into 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: dmVyc2lvbiA9IDMuOC41CnJ1bm5lci5kaWFsZWN0ID0gc2NhbGEyMTMKZmlsZU92ZXJyaWRlIHsKICAiZ2xvYjoqKi9saWIvc3JjL21haW4vc2NhbGEvKioiIHsKICAgICBydW5uZXIuZGlhbGVjdCA9IHNjYWxhMwogIH0KfQo=
// 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:
withFieldName
that acts like a setteraddFieldName
for lists and maps, adds new valuesmapFieldName
transforms using a function
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: dmVyc2lvbiA9IDMuOC41CnJ1bm5lci5kaWFsZWN0ID0gc2NhbGEyMTMKZmlsZU92ZXJyaWRlIHsKICAiZ2xvYjoqKi9saWIvc3JjL21haW4vc2NhbGEvKioiIHsKICAgICBydW5uZXIuZGlhbGVjdCA9IHNjYWxhMwogIH0KfQo=
// 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$11012/0x0000000802f50040@2bf7c7f9),cats.effect.kernel.Resource$$Lambda$11013/0x0000000802f51040@6e00685a),cats.effect.kernel.Resource$$Lambda$11014/0x0000000802f51840@5a8f93bb)
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@274b9b52,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)
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"
)