From 323421c2a71406d845d67f94ab705c648ac16554 Mon Sep 17 00:00:00 2001 From: David O'Riordan Date: Thu, 31 May 2018 20:53:43 +0100 Subject: [PATCH] Extend pod spec to include missing fields and values --- client/src/main/scala/skuber/Pod.scala | 26 +++++-- .../src/main/scala/skuber/json/package.scala | 73 +++++++++++++++++-- client/src/main/scala/skuber/package.scala | 2 +- .../resources/examplePodExtendedSpec.json | 4 + .../scala/skuber/json/PodFormatSpec.scala | 7 +- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/client/src/main/scala/skuber/Pod.scala b/client/src/main/scala/skuber/Pod.scala index 8dacb516..52f13de7 100644 --- a/client/src/main/scala/skuber/Pod.scala +++ b/client/src/main/scala/skuber/Pod.scala @@ -22,8 +22,7 @@ object Pod { def named(name: String) = Pod(metadata=ObjectMeta(name=name)) def apply(name: String, spec: Pod.Spec) : Pod = Pod(metadata=ObjectMeta(name=name), spec = Some(spec)) - - import DNSPolicy._ + case class Spec( containers: List[Container] = List(), // should have at least one member initContainers: List[Container] = Nil, @@ -31,7 +30,7 @@ object Pod { restartPolicy: RestartPolicy.RestartPolicy = RestartPolicy.Always, terminationGracePeriodSeconds: Option[Int] = None, activeDeadlineSeconds: Option[Int] = None, - dnsPolicy: DNSPolicy.DNSPolicy = ClusterFirst, + dnsPolicy: DNSPolicy.DNSPolicy = DNSPolicy.ClusterFirst, nodeSelector: Map[String, String] = Map(), serviceAccountName: String ="", nodeName: String = "", @@ -39,7 +38,17 @@ object Pod { imagePullSecrets: List[LocalObjectReference] = List(), affinity: Option[Affinity] = None, tolerations: List[Toleration] = List(), - securityContext: Option[Security.Context] = None) { + securityContext: Option[Security.Context] = None, + hostname: Option[String] = None, + hostAliases: List[HostAlias] = Nil, + hostPID: Option[Boolean] = None, + hostIPC: Option[Boolean] = None, + automountServiceAccountToken: Option[Boolean] = None, + priority: Option[Int] = None, + priorityClassName: Option[String] = None, + schedulerName: Option[String] = None, + subdomain: Option[String] = None, + dnsConfig: Option[DNSConfig] = None) { // a few convenience methods for fluently building out a pod spec def addContainer(c: Container) = { this.copy(containers = c :: containers) } @@ -138,6 +147,10 @@ object Pod { case class WeightedPodAffinityTerm(weight: Int, podAffinityTerm: PodAffinityTerm) } + case class HostAlias(ip: String, hostnames: List[String]) + case class DNSConfigOption(name: String, value: String) + case class DNSConfig(nameservers: List[String] = Nil, options: List[DNSConfigOption] = Nil, searches: List[String] = Nil) + case class Status( phase: Option[Phase.Phase] = None, conditions: List[Condition] = Nil, @@ -146,7 +159,10 @@ object Pod { hostIP: Option[String] = None, podIP: Option[String] = None, startTime: Option[Timestamp] = None, - containerStatuses: List[Container.Status] = Nil) + containerStatuses: List[Container.Status] = Nil, + initContainerStatuses: List[Container.Status] = Nil, + qosClass: Option[String] = None, + nominatedNodeName: Option[String] = None) case class Condition( _type : String="Ready", diff --git a/client/src/main/scala/skuber/json/package.scala b/client/src/main/scala/skuber/json/package.scala index c0c7a933..9512a351 100644 --- a/client/src/main/scala/skuber/json/package.scala +++ b/client/src/main/scala/skuber/json/package.scala @@ -59,7 +59,7 @@ package object format { // we make the above formatter methods available on JsPath objects via this implicit conversion implicit def maybeEmptyFormatMethods(path: JsPath) = new MaybeEmpty(path) - + // general formatting for Enumerations - derived from https://gist.github.com/mikesname/5237809 implicit def enumReads[E <: Enumeration](enum: E) : Reads[E#Value] = new Reads[E#Value] { def reads(json: JsValue): JsResult[E#Value] = json match { @@ -673,7 +673,10 @@ package object format { (JsPath \ "hostIP").formatNullable[String] and (JsPath \ "podIP").formatNullable[String] and (JsPath \ "startTime").formatNullable[Timestamp] and - (JsPath \ "containerStatuses").formatMaybeEmptyList[Container.Status] + (JsPath \ "containerStatuses").formatMaybeEmptyList[Container.Status] and + (JsPath \ "initContainerStatuses").formatMaybeEmptyList[Container.Status] and + (JsPath \ "qosClass").formatNullable[String] and + (JsPath \ "nominatedNodeName").formatNullable[String] )(Pod.Status.apply _, unlift(Pod.Status.unapply)) implicit lazy val podFormat : Format[Pod] = ( @@ -728,8 +731,20 @@ package object format { implicit lazy val affinityFormat : Format[Pod.Affinity] = Json.format[Pod.Affinity] - - implicit val podSpecFormat: Format[Pod.Spec] = ( + + implicit val hostAliasFmt: Format[Pod.HostAlias] = Json.format[Pod.HostAlias] + implicit val podDNSConfigOptionFmt: Format[Pod.DNSConfigOption] = Json.format[Pod.DNSConfigOption] + implicit val podDNSConfigFmt: Format[Pod.DNSConfig] = ( + (JsPath \ "nameservers").formatMaybeEmptyList[String] and + (JsPath \ "options").formatMaybeEmptyList[Pod.DNSConfigOption] and + (JsPath \ "searches").formatMaybeEmptyList[String] + )(Pod.DNSConfig.apply _, unlift(Pod.DNSConfig.unapply)) + + // the following ugliness is to do with the Kubernetes pod spec schema expanding until it takes over the entire universe, + // which has finally necessitated a hack to get around Play Json limitations supporting case classes with > 22 members + // (see e.g. https://stackoverflow.com/questions/28167971/scala-case-having-22-fields-but-having-issue-with-play-json-in-scala-2-11-5) + + val podSpecPartOneFormat: OFormat[(List[Container], List[Container], List[Volume], skuber.RestartPolicy.Value, Option[Int], Option[Int], skuber.DNSPolicy.Value, Map[String, String], String, String, Boolean, List[LocalObjectReference], Option[Pod.Affinity], List[Pod.Toleration], Option[Security.Context])] = ( (JsPath \ "containers").format[List[Container]] and (JsPath \ "initContainers").formatMaybeEmptyList[Container] and (JsPath \ "volumes").formatMaybeEmptyList[Volume] and @@ -745,7 +760,55 @@ package object format { (JsPath \ "affinity").formatNullable[Pod.Affinity] and (JsPath \ "tolerations").formatMaybeEmptyList[Pod.Toleration] and (JsPath \ "securityContext").formatNullable[Security.Context] - )(Pod.Spec.apply _, unlift(Pod.Spec.unapply)) + ).tupled + + val podSpecPartTwoFormat: OFormat[(Option[String], List[Pod.HostAlias], Option[Boolean], Option[Boolean], Option[Boolean], Option[Int], Option[String], Option[String], Option[String], Option[Pod.DNSConfig])] = ( + (JsPath \ "hostname").formatNullable[String] and + (JsPath \ "hostAliases").formatMaybeEmptyList[Pod.HostAlias] and + (JsPath \ "hostPID").formatNullable[Boolean] and + (JsPath \ "hostIPC").formatNullable[Boolean] and + (JsPath \ "automountServiceAccountToken").formatNullable[Boolean] and + (JsPath \ "priority").formatNullable[Int] and + (JsPath \ "priorityClassName").formatNullable[String] and + (JsPath \ "schedulerName").formatNullable[String] and + (JsPath \ "subdomain").formatNullable[String] and + (JsPath \ "dnsConfig").formatNullable[Pod.DNSConfig] + ).tupled + + implicit val podSpecFmt: Format[Pod.Spec] = ( + podSpecPartOneFormat and podSpecPartTwoFormat + ).apply({ + case ((conts, initConts, vols, rpol, tgps, adls, dnspol, nodesel, svcac, node, hnet, ips, aff, tol, sc), (host, aliases, pid, ipc, asat, prio, prioc, sched, subd, dnsc)) => + Pod.Spec(conts, initConts, vols, rpol, tgps, adls, dnspol, nodesel, svcac, node, hnet, ips, aff, tol, sc, host, aliases, pid, ipc, asat, prio, prioc, sched, subd, dnsc) + }, s =>( + ( s.containers, + s.initContainers, + s.volumes, + s.restartPolicy, + s.terminationGracePeriodSeconds, + s.activeDeadlineSeconds, + s.dnsPolicy, + s.nodeSelector, + s.serviceAccountName, + s.nodeName, + s.hostNetwork, + s.imagePullSecrets, + s.affinity, + s.tolerations, + s.securityContext + ), + ( s.hostname, + s.hostAliases, + s.hostPID, + s.hostIPC, + s.automountServiceAccountToken, + s.priority, + s.priorityClassName, + s.schedulerName, + s.subdomain, + s.dnsConfig + )) + ) implicit val podTemplSpecFormat: Format[Pod.Template.Spec] = Json.format[Pod.Template.Spec] implicit lazy val podTemplFormat : Format[Pod.Template] = ( diff --git a/client/src/main/scala/skuber/package.scala b/client/src/main/scala/skuber/package.scala index 19b2d98b..4ae9e87a 100644 --- a/client/src/main/scala/skuber/package.scala +++ b/client/src/main/scala/skuber/package.scala @@ -212,7 +212,7 @@ package object skuber { } object DNSPolicy extends Enumeration { type DNSPolicy = Value - val Default,ClusterFirst = Value + val Default,ClusterFirst,ClusterFirstWithHostNet,None = Value } object RestartPolicy extends Enumeration { type RestartPolicy = Value diff --git a/client/src/test/resources/examplePodExtendedSpec.json b/client/src/test/resources/examplePodExtendedSpec.json index f587cf03..777fb601 100644 --- a/client/src/test/resources/examplePodExtendedSpec.json +++ b/client/src/test/resources/examplePodExtendedSpec.json @@ -13,11 +13,15 @@ "serviceAccountName": "my-account", "terminationGracePeriodSeconds": 60, "hostNetwork": true, + "dnsPolicy": "None", "imagePullSecrets": [ { "name": "secret" } ], + "priority": 2, + "hostname": "abc", + "subdomain": "def", "containers": [ { "name": "basic", diff --git a/client/src/test/scala/skuber/json/PodFormatSpec.scala b/client/src/test/scala/skuber/json/PodFormatSpec.scala index e072bf8a..214f2bc6 100644 --- a/client/src/test/scala/skuber/json/PodFormatSpec.scala +++ b/client/src/test/scala/skuber/json/PodFormatSpec.scala @@ -560,7 +560,12 @@ import Pod._ mount.readOnly mustEqual true mount.subPath mustEqual "subpath" - pod.spec.get.securityContext.get.fsGroup == Some(2000) + pod.spec.get.securityContext.get.fsGroup mustEqual Some(2000) + pod.spec.get.priority mustEqual Some(2) + pod.spec.get.hostname mustEqual Some("abc") + pod.spec.get.subdomain mustEqual Some("def") + pod.spec.get.dnsPolicy mustEqual DNSPolicy.None + pod.spec.get.hostNetwork mustEqual true // write and read it back in again and compare val json = Json.toJson(pod)