lson.core_2.11.0.9.71.source-code.KubernetesClient.scala Maven / Gradle / Ivy
The newest version!
package nelson
import argonaut._
import argonaut.Argonaut._
import journal.Logger
import org.http4s.AuthScheme
import org.http4s.Credentials.Token
import org.http4s.Uri
import org.http4s.{Method, Request}
import org.http4s.argonaut._
import org.http4s.client.Client
import org.http4s.headers.Authorization
import scalaz.{Foldable, Monoid}
import scalaz.concurrent.Task
import scalaz.Scalaz._
import nelson.Datacenter.StackName
import nelson.Manifest.{EnvironmentVariable, Plan, Ports, Port}
import nelson.docker.Docker.Image
import nelson.health._
/**
* A bare bones Kubernetes client used for impelementing a Kubernetes
* [[nelson.scheduler.SchedulerOp]] and [[nelson.loadbalancers.LoadBalancerOp]].
*
* This should really be a proper library.. at some point.
*
* See: https://kubernetes.io/docs/api-reference/v1.8/
*/
final class KubernetesClient(endpoint: Uri, client: Client, serviceAccountToken: String) {
import KubernetesClient.cascadeDeletionPolicy
import KubernetesJson._
def createDeployment(namespace: String, stackName: StackName, image: Image, plan: Plan, ports: Option[Ports]): Task[Json] = {
val json = KubernetesJson.deployment(namespace,stackName, image, plan, ports)
val request = addCreds(Request(Method.POST, deploymentUri(namespace)))
client.expect[Json](request.withBody(json))
}
def createService(namespace: String, stackName: StackName, ports: Option[Ports]): Task[Json] = {
val json = KubernetesJson.service(namespace, stackName, ports)
val request = addCreds(Request(Method.POST, serviceUri(namespace)))
client.expect[Json](request.withBody(json))
}
def createCronJob(namespace: String, stackName: StackName, image: Image, plan: Plan, cronExpr: String): Task[Json] = {
val json = KubernetesJson.cronJob(namespace, stackName, image, plan, cronExpr)
val request = addCreds(Request(Method.POST, cronJobUri(namespace)))
client.expect[Json](request.withBody(json))
}
def createJob(namespace: String, stackName: StackName, image: Image, plan: Plan): Task[Json] = {
val json = KubernetesJson.job(namespace, stackName, image, plan)
val request = addCreds(Request(Method.POST, jobUri(namespace)))
client.expect[Json](request.withBody(json))
}
def deleteDeployment(namespace: String, name: String): Task[Json] = {
val request = addCreds(Request(Method.DELETE, deploymentUri(namespace) / name))
client.expect[Json](request.withBody(cascadeDeletionPolicy))
}
def deleteService(namespace: String, name: String): Task[Json] = {
val request = addCreds(Request(Method.DELETE, serviceUri(namespace) / name))
client.expect[Json](request.withBody(cascadeDeletionPolicy))
}
def deleteCronJob(namespace: String, name: String): Task[Json] = {
val request = addCreds(Request(Method.DELETE, cronJobUri(namespace) / name))
client.expect[Json](request.withBody(cascadeDeletionPolicy))
}
def deleteJob(namespace: String, name: String): Task[Json] = {
val request = addCreds(Request(Method.DELETE, jobUri(namespace) / name))
client.expect[Json](request.withBody(cascadeDeletionPolicy))
}
def deploymentSummary(namespace: String, name: String): Task[DeploymentStatus] = {
val request = addCreds(Request(Method.GET, deploymentUri(namespace) / name / "status"))
client.expect[DeploymentStatus](request)(jsonOf[DeploymentStatus])
}
def cronJobSummary(namespace: String, name: String): Task[JobStatus] = {
// CronJob status doesn't give very useful information so we leverage Nelson-specific
// information (the stackName label applied to cron jobs deployed by Nelson) to get status information
val selector = s"stackName=${name}"
val selectedUri = jobUri(namespace).withQueryParam("labelSelector", selector)
val request = addCreds(Request(Method.GET, selectedUri))
val decoder: DecodeJson[List[JobStatus]] = DecodeJson(c => (c --\ "items").as[List[JobStatus]])
client.expect[List[JobStatus]](request)(jsonOf(decoder)).map((jss: List[JobStatus])=> Foldable[List].fold(jss))
}
def jobSummary(namespace: String, name: String): Task[JobStatus] = {
val request = addCreds(Request(Method.GET, jobUri(namespace) / name / "status"))
client.expect[JobStatus](request)(jsonOf[JobStatus])
}
def listPods(namespace: String, labelSelectors: Map[String, String]): Task[List[HealthStatus]] = {
val selectors = labelSelectors.map { case (k, v) => s"${k}=${v}"}.mkString(",")
val uri = podUri(namespace).withQueryParam("labelSelector", selectors)
val request = addCreds(Request(Method.GET, uri))
implicit val statusDecoder = healthStatusDecoder
val decoder: DecodeJson[List[HealthStatus]] = DecodeJson(c => (c --\ "items").as[List[HealthStatus]])
client.expect[List[HealthStatus]](request)(jsonOf(decoder))
}
private def addCreds(req: Request): Request =
req.putHeaders(Authorization(Token(AuthScheme.Bearer, serviceAccountToken)))
private def podUri(ns: String): Uri =
endpoint / "api" / "v1" / "namespaces" / ns / "pods"
private def deploymentUri(ns: String): Uri =
endpoint / "apis" / "apps" / "v1beta2" / "namespaces" / ns / "deployments"
private def serviceUri(ns: String): Uri =
endpoint / "api" / "v1" / "namespaces" / ns / "services"
private def cronJobUri(ns: String): Uri =
endpoint / "apis" / "batch" / "v1beta1" / "namespaces" / ns / "cronjobs"
private def jobUri(ns: String): Uri =
endpoint / "apis" / "batch" / "v1" / "namespaces" / ns / "jobs"
}
object KubernetesClient {
// Cascade deletes - deleting a Deployment should delete the associated ReplicaSet and Pods
// See: https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/
private val cascadeDeletionPolicy = argonaut.Json(
"kind" := "DeleteOptions",
"apiVersion" := "v1",
"propagationPolicy" := "Foreground"
)
}
object KubernetesJson {
def deployment(
namespace: String,
stackName: StackName,
image: Image,
plan: Plan,
ports: Option[Ports]
): Json =
argonaut.Json(
"apiVersion" := "apps/v1beta2",
"kind" := "Deployment",
"metadata" := argonaut.Json(
"name" := stackName.toString,
"namespace" := namespace,
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
),
"spec" := argonaut.Json(
"replicas" := plan.environment.desiredInstances.getOrElse(1),
"selector" := argonaut.Json(
"matchLabels" := argonaut.Json("stackName" := stackName.toString)
),
"template" := argonaut.Json(
"metadata" := argonaut.Json(
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
),
"spec" := argonaut.Json(
"containers" := List(
argonaut.Json(
"name" := stackName.toString,
"image" := image.toString,
"ports" := containerPortsJson(ports.toList.flatMap(_.nel.list))
)
)
)
)
)
)
def service(namespace: String, stackName: StackName, ports: Option[Ports]): Json =
argonaut.Json(
"apiVersion" := "v1",
"kind" := "Service",
"metadata" := argonaut.Json(
"name" := stackName.toString,
"namespace" := namespace,
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
),
"spec" := argonaut.Json(
"selector" := argonaut.Json("stackName" := stackName.toString),
"ports" := servicePortsJson(ports.toList.flatMap(_.nel.list)),
"type" := "ClusterIP"
)
)
def cronJob(
namespace: String,
stackName: StackName,
image: Image,
plan: Plan,
cronExpr: String
): Json =
argonaut.Json(
"apiVersion" := "batch/v1beta1",
"kind" := "CronJob",
"metadata" := argonaut.Json(
"name" := stackName.toString,
"namespace" := namespace,
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
),
"spec" := argonaut.Json(
"schedule" := cronExpr,
"jobTemplate" := argonaut.Json.jObject(jobSpecJson(stackName, image, plan))
)
)
def job(
namespace: String,
stackName: StackName,
image: Image,
plan: Plan
): Json =
argonaut.Json.jObject(combineJsonObject(JsonObject.from(List(
"apiVersion" := "batch/v1",
"kind" := "Job",
"metadata" := argonaut.Json(
"name" := stackName.toString,
"namespace" := namespace,
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
)
)), jobSpecJson(stackName, image, plan)))
private def jobSpecJson(
stackName: StackName,
image: Image,
plan: Plan
): JsonObject = {
val backoffLimit = plan.environment.retries.fold(JsonObject.empty) { retries =>
JsonObject.single("backoffLimit", retries.asJson)
}
JsonObject.from(List(
"spec" := argonaut.Json.jObject(combineJsonObject(JsonObject.from(List(
"completions" := plan.environment.desiredInstances.getOrElse(1),
"template" := argonaut.Json(
"metadata" := argonaut.Json(
"name" := stackName.toString,
"labels" := argonaut.Json(
"stackName" := stackName.toString,
"serviceName" := stackName.serviceType,
"version" := stackName.version.toString,
"nelson" := "true"
)
),
"spec" := argonaut.Json(
"containers" := List(
argonaut.Json(
"name" := stackName.toString,
"image" := image.toString
)
),
"restartPolicy" := "OnFailure"
)
)
)), backoffLimit))
))
}
// Status seems to be largely undocumented in the K8s docs, so your best bet is to stare at
// https://github.com/kubernetes/kubernetes/tree/master/api/openapi-spec
// Recommend using 'jq' for your sanity
final case class DeploymentStatus(
availableReplicas: Option[Int],
unavailableReplicas: Option[Int]
)
object DeploymentStatus {
implicit val deploymentStatusDecoder: DecodeJson[DeploymentStatus] =
DecodeJson(c => {
val status = c --\ "status"
for {
availableReplicas <- (status --\ "availableReplicas").as[Option[Int]]
unavailableReplicas <- (status --\ "unavailableReplicas").as[Option[Int]]
} yield DeploymentStatus(availableReplicas, unavailableReplicas)
})
}
final case class JobStatus(
active: Option[Int],
failed: Option[Int],
succeeded: Option[Int]
)
object JobStatus {
implicit val jobStatusDecoder: DecodeJson[JobStatus] =
DecodeJson(c => {
val status = c --\ "status"
for {
active <- (status --\ "active").as[Option[Int]]
failed <- (status --\ "failed").as[Option[Int]]
succeeded <- (status --\ "succeeded").as[Option[Int]]
} yield JobStatus(active, failed, succeeded)
})
implicit val jobStatusMonoid: Monoid[JobStatus] = new Monoid[JobStatus] {
def append(f1: JobStatus, f2: => JobStatus): JobStatus =
JobStatus(
active = f1.active |+| f2.active,
failed = f1.failed |+| f2.failed,
succeeded = f1.succeeded |+| f2.succeeded
)
def zero: JobStatus = JobStatus(None, None, None)
}
}
private def combineJsonObject(x: JsonObject, y: JsonObject): JsonObject =
JsonObject.from(x.toList ++ y.toList)
private def containerPortsJson(ports: List[Port]): List[Json] =
ports.map { port =>
argonaut.Json(
"name" := port.ref,
"containerPort" := port.port
)
}
private def servicePortsJson(ports: List[Port]): List[Json] =
ports.map { port =>
argonaut.Json(
"name" := port.ref,
"port" := port.port,
"targetPort" := port.port
)
}
val healthStatusDecoder: DecodeJson[HealthStatus] =
DecodeJson(c =>
for {
name <- (c --\ "metadata" --\ "name").as[String]
status <- (c --\ "status" --\ "phase").as[String].map(parsePodHealthCheck)
node <- (c --\ "spec" --\ "nodeName").as[String]
details <- (c --\ "status" --\ "message").as[Option[String]]
} yield HealthStatus(name, status, node, details)
)
// https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
private def parsePodHealthCheck(s: String): HealthCheck = s match {
case "Running" => Passing
case "Failed" => Failing
case _ => Unknown
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy