-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #233 from coralbg/master
Add support for Ingress api networking.k8s.io/v1
- Loading branch information
Showing
3 changed files
with
375 additions
and
0 deletions.
There are no files selected for viewing
271 changes: 271 additions & 0 deletions
271
client/src/main/scala/skuber/networking/v1/Ingress.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)) | ||
|
||
} | ||
} |