Skip to content

Commit

Permalink
Merge pull request #233 from coralbg/master
Browse files Browse the repository at this point in the history
Add support for Ingress api networking.k8s.io/v1
  • Loading branch information
hagay3 authored Sep 14, 2022
2 parents 98405b0 + 03058ad commit 0697542
Show file tree
Hide file tree
Showing 3 changed files with 375 additions and 0 deletions.
271 changes: 271 additions & 0 deletions client/src/main/scala/skuber/networking/v1/Ingress.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package skuber.networking.v1

import play.api.libs.json.{Format, JsPath, Json}
import skuber.ResourceSpecification.{Names, Scope}
import skuber.networking.v1.Ingress.PathType.{ImplementationSpecific, PathType}
import skuber.{NameablePort, NonCoreResourceSpecification, ObjectMeta, ObjectResource, ResourceDefinition}

import scala.util.Try

case class Ingress(
kind: String = "Ingress",
override val apiVersion: String = ingressAPIVersion,
metadata: ObjectMeta = ObjectMeta(),
spec: Option[Ingress.Spec] = None,
status: Option[Ingress.Status] = None)
extends ObjectResource {

import Ingress.Backend

lazy val copySpec: Ingress.Spec = this.spec.getOrElse(new Ingress.Spec)

/**
* Fluent API method for building out ingress rules e.g.
* {{{
* val ingress = Ingress("microservices").
* addHttpRule("foo.bar.com",
* ImplementationSpecific,
* Map("/order" -> "orderService:80",
* "inventory" -> "inventoryService:80")).
* addHttpRule("foo1.bar.com",
* Map("/ship" -> "orderService:80",
* "inventory" -> "inventoryService:80")).
* }}}
*/
def addHttpRule(host: String, pathType: PathType, pathsMap: Map[String, String]): Ingress =
addHttpRule(Some(host), pathType: PathType, pathsMap)

/**
* Fluent API method for building out ingress rules without host e.g.
* {{{
* val ingress = Ingress("microservices").
* addHttpRule(ImplementationSpecific,
* Map("/order" -> "orderService:80",
* "inventory" -> "inventoryService:80")).
* addHttpRule(ImplementationSpecific,
* Map("/ship" -> "orderService:80",
* "inventory" -> "inventoryService:80")).
* }}}
*/
def addHttpRule(pathType: PathType, pathsMap: Map[String, String]): Ingress =
addHttpRule(Option.empty, pathType: PathType, pathsMap)

/**
* Fluent API method for building out ingress rules e.g.
* {{{
* val ingress =
* Ingress("microservices")
* .addHttpRule("foo.bar.com",
* ImplementationSpecific,
* "/order" -> "orderService:80",
* "inventory" -> "inventoryService:80")
* .addHttpRule("foo1.bar.com",
* ImplementationSpecific,
* "/ship" -> "orderService:80",
* "inventory" -> "inventoryService:80").
* }}}
*/
def addHttpRule(host: String, pathType: PathType, pathsMap: (String, String)*): Ingress =
addHttpRule(Some(host), pathType: PathType, pathsMap.toMap)

/**
* Fluent API method for building out ingress rules without host e.g.
* {{{
* val ingress =
* Ingress("microservices")
* .addHttpRule(ImplementationSpecific,
* "/order" -> "orderService:80",
* "inventory" -> "inventoryService:80")
* .addHttpRule(ImplementationSpecific,
* "/ship" -> "orderService:80",
* "inventory" -> "inventoryService:80").
* }}}
*/
def addHttpRule(pathType: PathType, pathsMap: (String, String)*): Ingress =
addHttpRule(Option.empty, pathType, pathsMap.toMap)

private val backendSpec = "(\\S+):(\\S+)".r

/**
* Fluent API method for building out ingress rules e.g.
* {{{
* val ingress =
* Ingress("microservices")
* .addHttpRule(Some("foo.bar.com"),
* Exact,
* Map("/order" -> "orderService:80",
* "inventory" -> "inventoryService:80"))
* .addHttpRule(None,
* ImplementationSpecific,
* Map("/ship" -> "orderService:80",
* "inventory" -> "inventoryService:80")).
* }}}
*/
def addHttpRule(host: Option[String], pathType: PathType, pathsMap: Map[String, String]): Ingress = {
val paths: List[Ingress.Path] = pathsMap.map {
case (path: String, backendService: String) =>
backendService match {
case backendSpec(serviceName, servicePort) =>
Ingress.Path(
path,
Ingress.Backend(
Option(Ingress.ServiceType(serviceName, Ingress.Port(number = toNameablePort(servicePort))))
),
pathType
)
case _ =>
throw new Exception(
s"invalid backend format: expected 'serviceName:servicePort' (got '$backendService', for host: $host)"
)
}

}.toList
val httpRule = Ingress.HttpRule(paths)
val rule = Ingress.Rule(host, httpRule)
val withRuleSpec = copySpec.copy(rules = copySpec.rules :+ rule)

this.copy(spec = Some(withRuleSpec))
}

/**
* set the default backend i.e. if no ingress rule matches the incoming traffic then it gets routed to the specified service
*
* @param serviceNameAndPort - service name and port as 'serviceName:servicePort'
* @return copy of this Ingress with default backend set
*/
def withDefaultBackendService(serviceNameAndPort: String): Ingress = {
serviceNameAndPort match {
case backendSpec(serviceName, servicePort) =>
withDefaultBackendService(serviceName, toNameablePort(servicePort))
case _ =>
throw new Exception(s"invalid default backend format: expected 'serviceName:servicePort' (got '$serviceNameAndPort')")
}
}

/**
* set the default backend i.e. if no ingress rule matches the incoming traffic then it gets routed to the specified service
*
* @param serviceName - service name
* @param servicePort - service port
* @return copy of this Ingress with default backend set
*/
def withDefaultBackendService(serviceName: String, servicePort: NameablePort): Ingress = {
val be = Backend(Option(Ingress.ServiceType(serviceName, Ingress.Port(number = servicePort))))
this.copy(spec = Some(copySpec.copy(backend = Some(be))))
}

def addAnnotations(newAnnos: Map[String, String]): Ingress =
this.copy(metadata = this.metadata.copy(annotations = this.metadata.annotations ++ newAnnos))

private def toNameablePort(port: String): NameablePort =
Try(port.toInt).toEither.left.map(_ => port).swap
}

object Ingress {
val specification: NonCoreResourceSpecification = NonCoreResourceSpecification(
apiGroup = "networking.k8s.io",
version = "v1",
scope = Scope.Namespaced,
names = Names(
plural = "ingresses",
singular = "ingress",
kind = "Ingress",
shortNames = List("ing")
)
)

implicit val ingDef : ResourceDefinition[Ingress] = new ResourceDefinition[Ingress] {
def spec: NonCoreResourceSpecification = specification
}
implicit val ingListDef: ResourceDefinition[IngressList] = new ResourceDefinition[IngressList] {
def spec: NonCoreResourceSpecification = specification
}

def apply(name: String): Ingress = Ingress(metadata = ObjectMeta(name = name))

case class Port(name: Option[String] = None, number: NameablePort)
case class ServiceType(name: String, port: Port)

// Backend contains either service or resource
case class Backend(service: Option[ServiceType] = None, resource: Option[String] = None)
case class Path(path: String, backend: Backend, pathType: PathType = ImplementationSpecific)
case class HttpRule(paths: List[Path] = List())
case class Rule(host: Option[String], http: HttpRule)
case class TLS(hosts: List[String] = List(), secretName: Option[String] = None)

object PathType extends Enumeration {
type PathType = Value
val ImplementationSpecific, Exact, Prefix = Value
}

case class Spec(
backend: Option[Backend] = None,
rules: List[Rule] = List(),
tls: List[TLS] = List(),
ingressClassName: Option[String] = None)

case class Status(loadBalancer: Option[Status.LoadBalancer] = None)

object Status {
case class LoadBalancer(ingress: List[LoadBalancer.Ingress])
object LoadBalancer {
case class Ingress(ip: Option[String] = None, hostName: Option[String] = None)
}
}

// json formatters

import play.api.libs.functional.syntax._
import skuber.json.format._

implicit val ingressPortFmt: Format[Ingress.Port] = Json.format[Ingress.Port]

implicit val ingressServiceFmt: Format[Ingress.ServiceType] = (
(JsPath \ "name").format[String] and
(JsPath \ "port").format[Ingress.Port]
) (Ingress.ServiceType.apply _, i => (i.name, i.port))

implicit val ingressBackendFmt: Format[Ingress.Backend] = (
(JsPath \ "service").formatNullable[Ingress.ServiceType] and
(JsPath \ "resource").formatNullable[String]
) (Ingress.Backend.apply _, i => (i.service, i.resource))

implicit val ingressPathFmt: Format[Ingress.Path] = (
(JsPath \ "path").formatMaybeEmptyString() and
(JsPath \ "backend").format[Ingress.Backend] and
(JsPath \ "pathType").formatEnum(PathType, PathType.ImplementationSpecific.toString)
) (Ingress.Path.apply _, i => (i.path, i.backend, i.pathType))

implicit val ingressHttpRuledFmt: Format[Ingress.HttpRule] = Json.format[Ingress.HttpRule]
implicit val ingressRuleFmt : Format[Ingress.Rule] = Json.format[Ingress.Rule]
implicit val ingressTLSFmt : Format[Ingress.TLS] = Json.format[Ingress.TLS]


implicit val ingressSpecFormat: Format[Ingress.Spec] = (
(JsPath \ "defaultBackend").formatNullable[Ingress.Backend] and
(JsPath \ "rules").formatMaybeEmptyList[Ingress.Rule] and
(JsPath \ "tls").formatMaybeEmptyList[Ingress.TLS] and
(JsPath \ "ingressClassName").formatNullable[String]
) (Ingress.Spec.apply _, i => (i.backend, i.rules, i.tls, i.ingressClassName))


implicit val ingrlbingFormat: Format[Ingress.Status.LoadBalancer.Ingress] =
Json.format[Ingress.Status.LoadBalancer.Ingress]

implicit val ingrlbFormat: Format[Ingress.Status.LoadBalancer] =
(JsPath \ "ingress").formatMaybeEmptyList[Ingress.Status.LoadBalancer.Ingress].inmap(
ings => Ingress.Status.LoadBalancer(ings),
lb => lb.ingress
)

implicit val ingressStatusFormat: Format[Ingress.Status] = Json.format[Ingress.Status]

implicit lazy val ingressFormat: Format[Ingress] = (
objFormat and
(JsPath \ "spec").formatNullable[Ingress.Spec] and
(JsPath \ "status").formatNullable[Ingress.Status]
) (Ingress.apply _, i => (i.kind, i.apiVersion, i.metadata, i.spec, i.status))

implicit val ingressListFmt: Format[IngressList] = ListResourceFormat[Ingress]

}
9 changes: 9 additions & 0 deletions client/src/main/scala/skuber/networking/v1/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package skuber.networking

import skuber.ListResource

package object v1 {
val ingressAPIVersion = "networking.k8s.io/v1"

type IngressList = ListResource[Ingress]
}
95 changes: 95 additions & 0 deletions client/src/test/scala/skuber/network/v1/IngressSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package skuber.network.v1

import org.specs2.mutable.Specification
import play.api.libs.json._
import skuber._
import skuber.networking.v1.Ingress
import skuber.networking.v1.Ingress.PathType._


class IngressSpec extends Specification {
"This is a unit specification for the skuber Ingress class from v1 api version. ".txt

"An Ingress object can be written to Json and then read back again successfully" >> {
val ingress = Ingress("example")
.addHttpRule("example.com", Exact, Map(
"/" -> "service:80",
"/about" -> "another-service:http"
))

val readIng = Json.fromJson[Ingress](Json.toJson(ingress)).get
readIng mustEqual ingress
}

"An Ingress object with empty Path can be read directly from a JSON string" >> {
val ingJsonStr =
"""
|{
| "apiVersion": "networking.k8s.io/v1",
| "kind": "Ingress",
| "metadata": {
| "creationTimestamp": "2017-04-02T19:39:34Z",
| "generation": 3,
| "labels": {
| "app": "ingress"
| },
| "name": "example-ingress",
| "namespace": "default",
| "resourceVersion": "1313499",
| "selfLink": "/apis/extensions/v1/namespaces/default/ingresses/example",
| "uid": "192dd131-17dc-11e7-bd9c-0a5e79684354"
| },
| "spec": {
| "rules": [
| {
| "host": "example.com",
| "http": {
| "paths": [
| {
| "backend": {
| "service": {
| "name": "example-svc",
| "port": {
| "number": 8080
| }
| },
| "pathType": "Exact"
| }
| }
| ]
| }
| }
| ],
| "tls": [
| {
| "hosts": ["abc","def"]
| }
| ],
| "ingressClassName": "nginx"
| },
| "status": {
| "loadBalancer": {
| "ingress": [
| {
| "hostname": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111.us-east-1.elb.amazonaws.com"
| }
| ]
| }
| }
|}""".stripMargin

val ing = Json.parse(ingJsonStr).as[Ingress]
ing.kind mustEqual "Ingress"
ing.name mustEqual "example-ingress"

ing.spec.get.rules.head.host must beSome("example.com")
ing.spec.get.rules.head.http.paths must_== List(
Ingress.Path(path = "", backend = Ingress.Backend(Some(Ingress.ServiceType("example-svc", Ingress.Port(number = 8080))))),
)
ing.spec.get.tls must_== List(Ingress.TLS(
hosts = List("abc", "def"),
secretName = None
))

}
}

0 comments on commit 0697542

Please sign in to comment.