plugins.jobs.kubernetes.webhooks.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.plugins.jobs.kubernetes
import akka.util.ByteString
import otoroshi.auth.AuthModuleConfig
import otoroshi.env.Env
import otoroshi.models._
import otoroshi.models.{DataExporterConfig, SimpleOtoroshiAdmin, Team, Tenant}
import otoroshi.next.plugins.api.{NgPluginCategory, NgPluginVisibility, NgStep}
import otoroshi.script.{RequestOrigin, RequestSink, RequestSinkContext, Script}
import otoroshi.tcp.TcpService
import otoroshi.utils.syntax.implicits._
import otoroshi.utils.yaml.Yaml
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.{Result, Results}
import otoroshi.ssl.Cert
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class KubernetesAdmissionWebhookCRDValidator extends RequestSink {
val logger = Logger("otoroshi-crd-validator")
override def name: String = "Kubernetes admission validator webhook"
override def description: Option[String] =
Some(
s"""This plugin exposes a webhook to kubernetes to handle manifests validation
""".stripMargin
)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Integrations)
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def matches(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Boolean = {
val config = KubernetesConfig.theConfig(ctx)
(
ctx.request.domain.contentEquals(
s"${config.otoroshiServiceName}.${config.otoroshiNamespace}.svc.${config.clusterDomain}"
) ||
ctx.request.domain.contentEquals(s"${config.otoroshiServiceName}.${config.otoroshiNamespace}.svc")
) &&
ctx.request.path.contentEquals("/apis/webhooks/validation") &&
ctx.origin == RequestOrigin.ReverseProxy &&
ctx.request.method == "POST"
}
def success(uid: String): Future[Result] = {
Results
.Ok(
Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> true
)
)
)
.future
}
def error(uid: String, errors: Seq[(JsPath, Seq[JsonValidationError])]): Future[Result] = {
Results
.Ok(
Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> false,
"status" -> Json.obj(
"code" -> 400,
"message" -> s"Entity format errors: \n\n${errors
.flatMap(err => err._2.map(verr => s" * ${err._1.toString()}: ${verr.message}"))
.mkString("\n")}"
)
)
)
)
.future
}
def regCert(arg1: String, arg2: String, arg3: Cert): Unit = ()
def regApk(arg1: String, arg2: String, arg3: ApiKey): Unit = ()
override def handle(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Future[Result] = {
implicit val mat = env.otoroshiMaterializer
ctx.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
val json: JsValue = ctx.request.contentType match {
case Some(v) if v.contains("application/json") => Json.parse(bodyRaw.utf8String)
case Some(v) if v.contains("application/yaml") => Yaml.parse(bodyRaw.utf8String).get
case _ => Json.parse(bodyRaw.utf8String)
}
val operation = (json \ "request" \ "operation").as[String]
val obj = (json \ "request" \ "object").as[JsObject]
val uid = (json \ "request" \ "uid").as[String]
val version = (obj \ "apiVersion").as[String]
val kind = (obj \ "kind").as[String]
if (version.startsWith("proxy.otoroshi.io/") && operation == "UPDATE" || operation == "CREATE") {
val client = new ClientSupport(new KubernetesClient(KubernetesConfig.theConfig(ctx), env), logger)
kind match {
case "ServiceGroup" => {
env.datastores.serviceGroupDataStore.findAll().flatMap { groups =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeServiceGroup(res.spec, res, groups)
ServiceGroup._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "Organization" => {
env.datastores.tenantDataStore.findAll().flatMap { tenants =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeTenant(res.spec, res, tenants)
Tenant.format.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "Team" => {
env.datastores.teamDataStore.findAll().flatMap { teams =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeTeam(res.spec, res, teams)
Team.format.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "ServiceDescriptor" => {
env.datastores.serviceDescriptorDataStore.findAll().flatMap { services =>
client.client.fetchServices().flatMap { kubeServices =>
client.client.fetchEndpoints().flatMap { kubeEndpoints =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeServiceDescriptor(
res.spec,
res,
kubeServices,
kubeEndpoints,
services,
client.client.config
)
ServiceDescriptor._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
}
}
case "ApiKey" => {
env.datastores.apiKeyDataStore.findAll().flatMap { entities =>
client.client.fetchSecrets().flatMap { secrets =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeApiKey(res.spec, res, secrets, entities, regApk)
ApiKey._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
}
case "GlobalConfig" => {
env.datastores.globalConfigDataStore.singleton().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeGlobalConfig(res.spec, res, entities)
GlobalConfig._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "Certificate" => {
env.datastores.certificatesDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeCert(res.spec, res, entities, regCert)
Cert._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "JwtVerifier" => {
env.datastores.globalJwtVerifierDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeJwtVerifier(res.spec, res, entities)
GlobalJwtVerifier._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "AuthModule" => {
env.datastores.authConfigsDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeAuthModule(res.spec, res, entities)
AuthModuleConfig._fmt(env).reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "Script" => {
env.datastores.scriptDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeScripts(res.spec, res, entities)
Script._fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "TcpService" => {
env.datastores.tcpServiceDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeTcpService(res.spec, res, entities)
TcpService.fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "DataExporter" => {
env.datastores.dataExporterConfigDataStore.findAll().flatMap { entities =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeDataExporter(res.spec, res, entities)
DataExporterConfig.format.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case "Admin" => {
env.datastores.simpleAdminDataStore.findAll().flatMap { admins =>
val res = KubernetesOtoroshiResource(obj)
val json = client.customizeAdmin(res.spec, res, admins)
SimpleOtoroshiAdmin.fmt.reads(json) match {
case JsSuccess(_, _) => success(uid)
case JsError(errors) => error(uid, errors)
}
}
}
case _ =>
Results
.Ok(
Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> false,
"status" -> Json.obj(
"code" -> 404,
"message" -> s"Resource of kind ${kind} unknown !"
)
)
)
)
.future
}
} else {
success(uid)
}
}
}
}
class KubernetesAdmissionWebhookSidecarInjector extends RequestSink {
val logger = Logger("otoroshi-sidecar-injector")
override def name: String = "Kubernetes sidecar injector webhook"
override def description: Option[String] =
Some(
s"""This plugin exposes a webhook to kubernetes to inject otoroshi-sidecar in pods
""".stripMargin
)
override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.Integrations)
override def steps: Seq[NgStep] = Seq(NgStep.TransformRequest)
override def matches(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Boolean = {
val config = KubernetesConfig.theConfig(ctx)
(
ctx.request.domain.contentEquals(
s"${config.otoroshiServiceName}.${config.otoroshiNamespace}.svc.${config.clusterDomain}"
) ||
ctx.request.domain.contentEquals(s"${config.otoroshiServiceName}.${config.otoroshiNamespace}.svc")
) &&
ctx.request.path.contentEquals("/apis/webhooks/inject") &&
ctx.origin == RequestOrigin.ReverseProxy &&
ctx.request.method == "POST"
}
override def handle(ctx: RequestSinkContext)(implicit env: Env, ec: ExecutionContext): Future[Result] = {
implicit val mat = env.otoroshiMaterializer
ctx.body.runFold(ByteString.empty)(_ ++ _).flatMap { bodyRaw =>
val json: JsValue = ctx.request.contentType match {
case Some(v) if v.contains("application/json") => Json.parse(bodyRaw.utf8String)
case Some(v) if v.contains("application/yaml") => Yaml.parse(bodyRaw.utf8String).get
case _ => Json.parse(bodyRaw.utf8String)
}
val operation = (json \ "request" \ "operation").as[String]
val obj = (json \ "request" \ "object").as[JsObject]
// obj.prettify.debugPrintln
val uid = (json \ "request" \ "uid").as[String]
val version = (obj \ "apiVersion").as[String]
val inject =
obj.select("metadata").select("labels").select("otoroshi.io/sidecar").asOpt[String].contains("inject")
if (inject) {
Try {
val conf = KubernetesConfig.theConfig(ctx)
val meta = obj.select("metadata").as[JsObject]
val apikey = meta.select("annotations").select("otoroshi.io/sidecar-apikey").as[String]
val backendCert = meta.select("annotations").select("otoroshi.io/sidecar-backend-cert").as[String]
val clientCert = meta.select("annotations").select("otoroshi.io/sidecar-client-cert").as[String]
val tokenSecret =
meta.select("annotations").select("otoroshi.io/token-secret").asOpt[String].getOrElse("secret")
val expectedDN =
meta.select("annotations").select("otoroshi.io/expected-dn").asOpt[String].getOrElse("cn=none")
val localPort = obj
.select("spec")
.select("containers")
.select(0)
.select("ports")
.select(0)
.select("containerPort")
.asOpt[Int]
.getOrElse(8081)
val image = conf.image.getOrElse("maif/otoroshi-sidecar:latest")
val template = conf.templates.select("webhooks")
val containerPort = conf.templates.select("containerPort")
val otoroshi = template.select("otoroshi")
val ports = template.select("ports")
val flags = template.select("flags")
val localPortV = ports.select("local").asOpt[Int].getOrElse(localPort)
if (localPortV == 80) {
throw new RuntimeException("The application service cannot use port 80 !")
}
val containerPortV = containerPort.select("value").asOpt[Int].getOrElse(8443)
def envVariable(name: String, value: String): JsValue = Json.obj("name" -> name, "value" -> value)
def envVariableInt(name: String, value: Int): JsValue = Json.obj("name" -> name, "value" -> value.toString)
def envVariableBool(name: String, value: Boolean): JsValue =
Json.obj("name" -> name, "value" -> value.toString)
def volumeMount(name: String, path: String): JsValue =
Json.obj("name" -> name, "mountPath" -> path, "readOnly" -> true)
def secretVolume(volumeName: String, secretName: String): JsValue =
Json.obj("name" -> volumeName, "secret" -> Json.obj("secretName" -> secretName))
val base64patch = Json
.arr(
Json.obj(
"op" -> "add",
"path" -> "/spec/containers/-",
"value" -> Json.obj(
"image" -> image,
"imagePullPolicy" -> "Always",
"name" -> "otoroshi-sidecar",
"ports" -> Json.arr(
Json.obj(
"name" -> JsString(containerPort.select("name").asOpt[String].getOrElse("https")),
"containerPort" -> containerPortV
)
// Json.obj(
// "name" -> "dns",
// "containerPort" -> 53
// )
),
// "securityContext" -> Json.obj(
// "allowPrivilegeEscalation" -> false,
// "capabilities" -> Json.obj(
// "add" -> Json.arr(
// "NET_BIND_SERVICE"
// ),
// "drop" -> Json.arr(
// "all"
// ),
// "readOnlyRootFilesystem" -> true
// )
// ),
"env" -> Json.arr(
envVariable("TOKEN_SECRET", tokenSecret),
envVariable("OTOROSHI_DOMAIN", otoroshi.select("domain").asOpt[String].getOrElse(conf.meshDomain)),
envVariable(
"OTOROSHI_HOST",
otoroshi
.select("host")
.asOpt[String]
.getOrElse(s"${conf.otoroshiServiceName}.${conf.otoroshiNamespace}.svc.${conf.clusterDomain}")
),
envVariableInt("OTOROSHI_PORT", otoroshi.select("port").asOpt[Int].getOrElse(8443)),
envVariableInt("LOCAL_PORT", ports.select("local").asOpt[Int].getOrElse(localPort)),
envVariableInt("EXTERNAL_PORT", ports.select("external").asOpt[Int].getOrElse(8443)),
envVariableInt("INTERNAL_PORT", ports.select("internal").asOpt[Int].getOrElse(8080)),
envVariableBool("REQUEST_CERT", flags.select("requestCert").asOpt[Boolean].getOrElse(true)),
envVariableBool("ENABLE_ORIGIN_CHECK", flags.select("originCheck").asOpt[Boolean].getOrElse(true)),
envVariableBool(
"DISABLE_TOKENS_CHECK",
!flags.select("tokensCheck").asOpt[Boolean].getOrElse(true)
),
envVariableBool("DISPLAY_ENV", flags.select("displayEnv").asOpt[Boolean].getOrElse(false)),
envVariableBool("ENABLE_TRACE", flags.select("tlsTrace").asOpt[Boolean].getOrElse(false)),
envVariableBool(
"ENABLE_CLIENT_CERT_CHECK",
flags.select("clientCertCheck").asOpt[Boolean].getOrElse(true)
),
envVariable("EXPECTED_DN", expectedDN)
),
"volumeMounts" -> Json.arr(
volumeMount("apikey-volume", "/var/run/secrets/kubernetes.io/otoroshi.io/apikeys"),
volumeMount("backend-cert-volume", "/var/run/secrets/kubernetes.io/otoroshi.io/certs/backend"),
volumeMount("client-cert-volume", "/var/run/secrets/kubernetes.io/otoroshi.io/certs/client")
)
)
),
/*Json.obj(
"op" -> "add",
"path" -> "/spec/containers/-",
"value" -> Json.obj(
"image" -> "andyshinn/dnsmasq:latest",
"imagePullPolicy" -> "Always",
"name" -> "otoroshi-sidecar-dns",
"ports" -> Json.arr(
Json.obj(
"name" -> "dns",
"containerPort" -> 53
)
),
"securityContext" -> Json.obj(
"allowPrivilegeEscalation" -> false,
"capabilities" -> Json.obj(
"add" -> Json.arr(
"NET_BIND_SERVICE"
),
"drop" -> Json.arr(
"all"
),
"readOnlyRootFilesystem" -> true
)
),
"command" -> Json.arr("dnsmasq")
//"command" -> Json.arr("dnsmasq", "-k", "--conf-file=/etc/dnsmasq-custom.conf")
//"command" -> Json.arr("dnsmasq", "-k", "--address=/otoroshi.mesh/127.0.0.1", "--resolv-file=/etc/resolv.back.conf", "--no-daemon")
//"command" -> Json.arr("dnsmasq", "-k", "--address=/otoroshi.mesh/127.0.0.1", "--server=`cat /etc/resolv.back.conf | grep nameserver | awk '{print $2}'`", "--no-daemon")
)
),*/
Json.obj(
"op" -> "add",
"path" -> "/spec/initContainers",
"value" -> Json.arr()
),
Json.obj(
"op" -> "add",
"path" -> "/spec/initContainers/-",
"value" -> Json.obj(
"image" -> image.replace("otoroshi-sidecar:", "otoroshi-sidecar-init:"),
"imagePullPolicy" -> "Always",
"name" -> "otoroshi-sidecar-init",
"securityContext" -> Json.obj(
"capabilities" -> Json.obj(
"add" -> Json.arr(
"NET_ADMIN"
)
),
"privileged" -> true
),
"env" -> Json.arr(
envVariableInt("FROM", 80),
envVariableInt("TO", ports.select("internal").asOpt[Int].getOrElse(8080))
)
)
),
// Json.obj(
// "op" -> "add",
// "path" -> "/spec/dnsConfig",
// "value" -> Json.obj(
// "nameservers" -> Json.arr(
// "127.0.0.1"
// )
// )
// ),
Json.obj(
"op" -> "add",
"path" -> "/spec/volumes/-",
"value" -> secretVolume("apikey-volume", apikey)
),
Json.obj(
"op" -> "add",
"path" -> "/spec/volumes/-",
"value" -> secretVolume("backend-cert-volume", backendCert)
),
Json.obj(
"op" -> "add",
"path" -> "/spec/volumes/-",
"value" -> secretVolume("client-cert-volume", clientCert)
)
) /*.debug(a => a.prettify.debugPrintln)*/
.stringify
.base64
val patch = Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> true,
"patchType" -> "JSONPatch",
"patch" -> base64patch
)
)
Results.Ok(patch)
} match {
case Success(r) => r.future
case Failure(e) =>
Results
.Ok(
Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> false,
"status" -> Json.obj(
"code" -> 400,
"message" -> s"${e.getMessage}"
)
)
)
)
.future
}
} else {
Results
.Ok(
Json.obj(
"apiVersion" -> "admission.k8s.io/v1",
"kind" -> "AdmissionReview",
"response" -> Json.obj(
"uid" -> uid,
"allowed" -> true
)
)
)
.future
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy