From 9c7852d36f595e1598ad5232322776856dceffca Mon Sep 17 00:00:00 2001 From: coralbg <65090199+coralbg@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:04:20 +0400 Subject: [PATCH 1/5] Create Ingress.scala --- .../scala/skuber/networking/v1/Ingress.scala | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 client/src/main/scala/skuber/networking/v1/Ingress.scala diff --git a/client/src/main/scala/skuber/networking/v1/Ingress.scala b/client/src/main/scala/skuber/networking/v1/Ingress.scala new file mode 100644 index 00000000..96289e08 --- /dev/null +++ b/client/src/main/scala/skuber/networking/v1/Ingress.scala @@ -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 _, unlift(Ingress.ServiceType.unapply)) + + implicit val ingressBackendFmt: Format[Ingress.Backend] = ( + (JsPath \ "service").formatNullable[Ingress.ServiceType] and + (JsPath \ "resource").formatNullable[String] + ) (Ingress.Backend.apply _, unlift(Ingress.Backend.unapply)) + + implicit val ingressPathFmt: Format[Ingress.Path] = ( + (JsPath \ "path").formatMaybeEmptyString() and + (JsPath \ "backend").format[Ingress.Backend] and + (JsPath \ "pathType").formatEnum(PathType, Some(PathType.ImplementationSpecific)) + ) (Ingress.Path.apply _, unlift(Ingress.Path.unapply)) + + 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 _, unlift(Ingress.Spec.unapply)) + + + 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 _, unlift(Ingress.unapply)) + + implicit val ingressListFmt: Format[IngressList] = ListResourceFormat[Ingress] + +} From 2f2b9d51d6afba58b30b6157e6ef0b8631b088c0 Mon Sep 17 00:00:00 2001 From: coralbg <65090199+coralbg@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:05:29 +0400 Subject: [PATCH 2/5] Create package.scala --- client/src/main/scala/skuber/networking/v1/package.scala | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 client/src/main/scala/skuber/networking/v1/package.scala diff --git a/client/src/main/scala/skuber/networking/v1/package.scala b/client/src/main/scala/skuber/networking/v1/package.scala new file mode 100644 index 00000000..1becf82c --- /dev/null +++ b/client/src/main/scala/skuber/networking/v1/package.scala @@ -0,0 +1,9 @@ +package skuber.networking + +import skuber.ListResource + +package object v1 { + val ingressAPIVersion = "networking.k8s.io/v1" + + type IngressList = ListResource[Ingress] +} From 9b3ae4f4ab510de843a550cb46c93a103e1a4247 Mon Sep 17 00:00:00 2001 From: coralbg <65090199+coralbg@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:07:06 +0400 Subject: [PATCH 3/5] Create IngressSpec.scala --- .../scala/skuber/network/v1/IngressSpec.scala | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 client/src/test/scala/skuber/network/v1/IngressSpec.scala diff --git a/client/src/test/scala/skuber/network/v1/IngressSpec.scala b/client/src/test/scala/skuber/network/v1/IngressSpec.scala new file mode 100644 index 00000000..7ae8c10d --- /dev/null +++ b/client/src/test/scala/skuber/network/v1/IngressSpec.scala @@ -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 + )) + + } +} From 0ac581e1d47ef2265feaafd4831204e286d76a93 Mon Sep 17 00:00:00 2001 From: coralbg <65090199+coralbg@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:19:50 +0400 Subject: [PATCH 4/5] Update Ingress.scala --- client/src/main/scala/skuber/networking/v1/Ingress.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/scala/skuber/networking/v1/Ingress.scala b/client/src/main/scala/skuber/networking/v1/Ingress.scala index 96289e08..5106ed7f 100644 --- a/client/src/main/scala/skuber/networking/v1/Ingress.scala +++ b/client/src/main/scala/skuber/networking/v1/Ingress.scala @@ -233,7 +233,7 @@ object Ingress { implicit val ingressPathFmt: Format[Ingress.Path] = ( (JsPath \ "path").formatMaybeEmptyString() and (JsPath \ "backend").format[Ingress.Backend] and - (JsPath \ "pathType").formatEnum(PathType, Some(PathType.ImplementationSpecific)) + (JsPath \ "pathType").formatEnum(PathType, PathType.ImplementationSpecific.toString) ) (Ingress.Path.apply _, unlift(Ingress.Path.unapply)) implicit val ingressHttpRuledFmt: Format[Ingress.HttpRule] = Json.format[Ingress.HttpRule] From 03058ad73aad98ef1bd67103efca82432f8f0edb Mon Sep 17 00:00:00 2001 From: coralbg <65090199+coralbg@users.noreply.github.com> Date: Thu, 15 Sep 2022 02:26:10 +0400 Subject: [PATCH 5/5] Update Ingress.scala --- .../src/main/scala/skuber/networking/v1/Ingress.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/main/scala/skuber/networking/v1/Ingress.scala b/client/src/main/scala/skuber/networking/v1/Ingress.scala index 5106ed7f..ff073696 100644 --- a/client/src/main/scala/skuber/networking/v1/Ingress.scala +++ b/client/src/main/scala/skuber/networking/v1/Ingress.scala @@ -223,18 +223,18 @@ object Ingress { implicit val ingressServiceFmt: Format[Ingress.ServiceType] = ( (JsPath \ "name").format[String] and (JsPath \ "port").format[Ingress.Port] - ) (Ingress.ServiceType.apply _, unlift(Ingress.ServiceType.unapply)) + ) (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 _, unlift(Ingress.Backend.unapply)) + ) (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 _, unlift(Ingress.Path.unapply)) + ) (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] @@ -246,7 +246,7 @@ object Ingress { (JsPath \ "rules").formatMaybeEmptyList[Ingress.Rule] and (JsPath \ "tls").formatMaybeEmptyList[Ingress.TLS] and (JsPath \ "ingressClassName").formatNullable[String] - ) (Ingress.Spec.apply _, unlift(Ingress.Spec.unapply)) + ) (Ingress.Spec.apply _, i => (i.backend, i.rules, i.tls, i.ingressClassName)) implicit val ingrlbingFormat: Format[Ingress.Status.LoadBalancer.Ingress] = @@ -264,7 +264,7 @@ object Ingress { objFormat and (JsPath \ "spec").formatNullable[Ingress.Spec] and (JsPath \ "status").formatNullable[Ingress.Status] - ) (Ingress.apply _, unlift(Ingress.unapply)) + ) (Ingress.apply _, i => (i.kind, i.apiVersion, i.metadata, i.spec, i.status)) implicit val ingressListFmt: Format[IngressList] = ListResourceFormat[Ingress]