diff --git a/README.md b/README.md index fab49282..0ea926c6 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,23 @@ See the [programming guide](docs/GUIDE.md) for more details. ## Example Usage -This example creates a nginx service (accessed via port 30001 on each Kubernetes cluster node) that is backed by five nginx replicas. +This example creates a nginx service (accessed via port 30001 on each Kubernetes cluster node) that is backed by a deployment of five nginx replicas. ```scala import skuber._ import skuber.json.format._ +import skuber.apps.v1.Deployment +import LabelSelector.dsl._ -val nginxSelector = Map("app" -> "nginx") -val nginxContainer = Container("nginx",image="nginx").exposePort(80) -val nginxController= ReplicationController("nginx",nginxContainer,nginxSelector) +val nginxSelector = "app" is "nginx" +val nginxContainer = Container(name = "nginx", image = "nginx").exposePort(80) +val nginxTemplate = Pod.Template.Spec.named("nginx").addContainer(nginxContainer).addLabel("app" -> "nginx") +val nginxDeployment = Deployment(name) .withReplicas(5) + .withTemplate(nginxTemplate) + .withLabelSelector(nginxSelector) val nginxService = Service("nginx") - .withSelector(nginxSelector) + .withSelector("app" -> "nginx") .exposeOnNodePort(30001 -> 80) // Some standard Akka implicits that are required by the skuber v2 client API @@ -41,13 +46,14 @@ implicit val dispatcher = system.dispatcher // Initialise skuber client val k8s = k8sInit +// Create the service and the deployment on the Kubernetes cluster val createOnK8s = for { svc <- k8s create nginxService - rc <- k8s create nginxController -} yield (rc,svc) + dep <- k8s create nginxDeployment +} yield (dep,svc) createOnK8s onComplete { - case Success(_) => System.out.println("Successfully created nginx replication controller & service on Kubernetes cluster") + case Success(_) => System.out.println("Successfully created nginx deployment & service on Kubernetes cluster") case Failure(ex) => System.err.println("Encountered exception trying to create resources on Kubernetes cluster: " + ex) } @@ -100,13 +106,11 @@ The quickest way to get started with Skuber: - Try one or more of the examples: if you have cloned this repository run `sbt` in the top-level directory to start sbt in interactive mode and then: ```bash -> project examples - -> run -[warn] Multiple main classes detected. Run 'show discoveredMainClasses' to see the list +sbt:root> project examples +sbt:skuber-examples> run Multiple main classes detected, select one to run: - + [1] skuber.examples.customresources.CreateCRD [2] skuber.examples.deployment.DeploymentExamples [3] skuber.examples.fluent.FluentExamples @@ -114,9 +118,12 @@ Multiple main classes detected, select one to run: [5] skuber.examples.ingress.NginxIngress [6] skuber.examples.job.PrintPiJob [7] skuber.examples.list.ListExamples - [8] skuber.examples.scale.ScaleExamples + [8] skuber.examples.patch.PatchExamples + [9] skuber.examples.podlogs.PodLogExample + [10] skuber.examples.scale.ScaleExamples + [11] skuber.examples.watch.WatchExamples -Enter number: +Enter number: ``` For other Kubernetes setups, see the [Configuration guide](docs/Configuration.md) for details on how to tailor the configuration for your clusters security, namespace and connectivity requirements. diff --git a/client/src/it/scala/skuber/DeploymentSpec.scala b/client/src/it/scala/skuber/DeploymentSpec.scala index 1282188f..37da55ba 100644 --- a/client/src/it/scala/skuber/DeploymentSpec.scala +++ b/client/src/it/scala/skuber/DeploymentSpec.scala @@ -2,8 +2,8 @@ package skuber import org.scalatest.Matchers import org.scalatest.concurrent.{Eventually, ScalaFutures} -import skuber.ext.Deployment -import skuber.json.ext.format._ +import skuber.LabelSelector.IsEqualRequirement +import skuber.apps.v1.Deployment import scala.concurrent.duration._ import scala.concurrent.{Await, Future} @@ -59,8 +59,9 @@ class DeploymentSpec extends K8SFixture with Eventually with Matchers { def getNginxContainer(version: String): Container = Container(name = "nginx", image = "nginx:" + version).exposePort(80) def getNginxDeployment(name: String, version: String): Deployment = { + import LabelSelector.dsl._ val nginxContainer = getNginxContainer(version) val nginxTemplate = Pod.Template.Spec.named("nginx").addContainer(nginxContainer).addLabel("app" -> "nginx") - Deployment(name).withTemplate(nginxTemplate) + Deployment(name).withTemplate(nginxTemplate).withLabelSelector("app" is "nginx") } } diff --git a/client/src/main/scala/skuber/Skuber/package.scala b/client/src/main/scala/skuber/Skuber/package.scala deleted file mode 100644 index 3c5a91c2..00000000 --- a/client/src/main/scala/skuber/Skuber/package.scala +++ /dev/null @@ -1,9 +0,0 @@ -package skuber - -/** - * @author David O'Riordan - */ -package object Skuber { - - -} \ No newline at end of file diff --git a/client/src/main/scala/skuber/apps/StatefulSet.scala b/client/src/main/scala/skuber/apps/StatefulSet.scala index 168092bf..a226ea15 100644 --- a/client/src/main/scala/skuber/apps/StatefulSet.scala +++ b/client/src/main/scala/skuber/apps/StatefulSet.scala @@ -96,29 +96,29 @@ object StatefulSet { implicit val statefulSetPodPcyMgmtFmt: Format[StatefulSet.PodManagementPolicyType.PodManagementPolicyType] = Format(enumReads(StatefulSet.PodManagementPolicyType, StatefulSet.PodManagementPolicyType.OrderedReady), enumWrites) implicit val statefulSetRollUp: Format[StatefulSet.RollingUpdateStrategy] = Json.format[StatefulSet.RollingUpdateStrategy] implicit val statefulSetUpdStrFmt: Format[StatefulSet.UpdateStrategy] = ( - (JsPath \ "type").formatEnum(StatefulSet.UpdateStrategyType, Some(StatefulSet.UpdateStrategyType.RollingUpdate)) and - (JsPath \ "rollingUpdate").formatNullable[StatefulSet.RollingUpdateStrategy] - )(StatefulSet.UpdateStrategy.apply _,unlift(StatefulSet.UpdateStrategy.unapply)) + (JsPath \ "type").formatEnum(StatefulSet.UpdateStrategyType, Some(StatefulSet.UpdateStrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[StatefulSet.RollingUpdateStrategy] + )(StatefulSet.UpdateStrategy.apply _,unlift(StatefulSet.UpdateStrategy.unapply)) implicit val statefulSetSpecFmt: Format[StatefulSet.Spec] = ( - (JsPath \ "replicas").formatNullable[Int] and - (JsPath \ "serviceName").formatNullable[String] and - (JsPath \ "selector").formatNullableLabelSelector and - (JsPath \ "template").format[Pod.Template.Spec] and - (JsPath \ "volumeClaimTemplates").formatMaybeEmptyList[PersistentVolumeClaim] and - (JsPath \ "podManagmentPolicy").formatNullableEnum(StatefulSet.PodManagementPolicyType) and - (JsPath \ "updateStrategy").formatNullable[StatefulSet.UpdateStrategy] and - (JsPath \ "revisionHistoryLimit").formatNullable[Int] - )(StatefulSet.Spec.apply _, unlift(StatefulSet.Spec.unapply)) + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "serviceName").formatNullable[String] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").format[Pod.Template.Spec] and + (JsPath \ "volumeClaimTemplates").formatMaybeEmptyList[PersistentVolumeClaim] and + (JsPath \ "podManagmentPolicy").formatNullableEnum(StatefulSet.PodManagementPolicyType) and + (JsPath \ "updateStrategy").formatNullable[StatefulSet.UpdateStrategy] and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] + )(StatefulSet.Spec.apply _, unlift(StatefulSet.Spec.unapply)) implicit val statefulSetCondFmt: Format[StatefulSet.Condition] = Json.format[StatefulSet.Condition] implicit val statefulSetStatusFmt: Format[StatefulSet.Status] = Json.format[StatefulSet.Status] implicit lazy val statefulSetFormat: Format[StatefulSet] = ( objFormat and - (JsPath \ "spec").formatNullable[StatefulSet.Spec] and - (JsPath \ "status").formatNullable[StatefulSet.Status] - ) (StatefulSet.apply _, unlift(StatefulSet.unapply)) + (JsPath \ "spec").formatNullable[StatefulSet.Spec] and + (JsPath \ "status").formatNullable[StatefulSet.Status] + )(StatefulSet.apply _, unlift(StatefulSet.unapply)) implicit val statefulSetListFormat: Format[StatefulSetList] = ListResourceFormat[StatefulSet] } diff --git a/client/src/main/scala/skuber/apps/v1/DaemonSet.scala b/client/src/main/scala/skuber/apps/v1/DaemonSet.scala new file mode 100644 index 00000000..67b7a5fa --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1/DaemonSet.scala @@ -0,0 +1,120 @@ +package skuber.apps.v1 + +/** + * @author David O'Riordan + */ + +import skuber.ResourceSpecification.{Names, Scope} +import skuber.{IntOrString, LabelSelector, NonCoreResourceSpecification, ObjectMeta, ObjectResource, Pod, ResourceDefinition, Timestamp} + +case class DaemonSet(val kind: String ="DaemonSet", + override val apiVersion: String = appsAPIVersion, + val metadata: ObjectMeta, + spec: Option[DaemonSet.Spec] = None, + status: Option[DaemonSet.Status] = None) + extends ObjectResource { + + lazy val copySpec = this.spec.getOrElse(new DaemonSet.Spec) + + def withTemplate(template: Pod.Template.Spec) = this.copy(spec=Some(copySpec.copy(template=Some(template)))) + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=Some(sel)))) +} + +object DaemonSet { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1", + scope = Scope.Namespaced, + names=Names( + plural = "daemonsets", + singular = "daemonset", + kind = "DaemonSet", + shortNames = List("ds") + ) + ) + implicit val dsDef = new ResourceDefinition[DaemonSet] { def spec=specification } + implicit val dsListDef = new ResourceDefinition[DaemonSetList] { def spec=specification } + + def apply(name: String) = new DaemonSet(metadata=ObjectMeta(name=name)) + + case class Spec( + minReadySeconds: Int = 0, + selector: Option[LabelSelector] = None, + template: Option[Pod.Template.Spec] = None, + updateStrategy: Option[UpdateStrategy] = None, + revisionHistoryLimit: Option[Int] = None + ) + + object UpdateStrategyType extends Enumeration { + type UpdateStrategyType = Value + val OnDelete, RollingUpdate = Value + } + + sealed trait UpdateStrategy { + def _type: UpdateStrategyType.UpdateStrategyType + def rollingUpdate: Option[RollingUpdate] + } + + object UpdateStrategy { + private[skuber] case class StrategyImpl(_type: UpdateStrategyType.UpdateStrategyType, rollingUpdate: Option[RollingUpdate]) extends UpdateStrategy + def apply: UpdateStrategy = StrategyImpl(_type=UpdateStrategyType.RollingUpdate, rollingUpdate=Some(RollingUpdate())) + def apply(_type: UpdateStrategyType.UpdateStrategyType,rollingUpdate: Option[RollingUpdate]) : UpdateStrategy = StrategyImpl(_type, rollingUpdate) + def apply(rollingUpdate: RollingUpdate) : UpdateStrategy = StrategyImpl(_type=UpdateStrategyType.RollingUpdate, rollingUpdate=Some(rollingUpdate)) + def unapply(strategy: UpdateStrategy): Option[(UpdateStrategyType.UpdateStrategyType, Option[RollingUpdate])] = + Some(strategy._type,strategy.rollingUpdate) + } + + case class RollingUpdate(maxUnavailable: IntOrString = Left(1)) + + case class Condition( + _type: String, + status: String, + reason: Option[String]=None, + message: Option[String]=None, + lastTransitionTime: Option[Timestamp]=None) + + case class Status( + currentNumberScheduled: Int, + numberMisscheduled: Int, + desiredNumberScheduled: Int, + numberReady: Int, + observedGeneration: Option[Long], + updatedNumberScheduled: Option[Int], + numberAvailable: Option[Int], + numberUnavailable:Option[Int], + collisionCount:Option[Long], + conditions: List[Condition]) + + // json formatters + import play.api.libs.json.{Json,Format, JsPath} + import play.api.libs.functional.syntax._ + import skuber.json.format._ + + implicit val condFmt: Format[Condition] = Json.format[Condition] + implicit val rollingUpdFmt: Format[RollingUpdate] = ( + (JsPath \ "maxUnavailable").formatMaybeEmptyIntOrString(Left(1)).inmap(mu => RollingUpdate(mu), (ru: RollingUpdate) => ru.maxUnavailable) + ) + + implicit val updateStrategyFmt: Format[UpdateStrategy] = ( + (JsPath \ "type").formatEnum(UpdateStrategyType, Some(UpdateStrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[RollingUpdate] + )(UpdateStrategy.apply _, unlift(UpdateStrategy.unapply)) + + implicit val daemonsetStatusFmt: Format[Status] = Json.format[Status] + implicit val daemonsetSpecFmt: Format[Spec] = ( + (JsPath \ "minReadySeconds").formatMaybeEmptyInt() and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").formatNullable[Pod.Template.Spec] and + (JsPath \ "updateStrategy").formatNullable[UpdateStrategy] and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] + )(Spec.apply, unlift(Spec.unapply)) + + implicit lazy val daemonsetFmt: Format[DaemonSet] = ( + objFormat and + (JsPath \ "spec").formatNullable[Spec] and + (JsPath \ "status").formatNullable[Status] + ) (DaemonSet.apply _, unlift(DaemonSet.unapply)) + +} + diff --git a/client/src/main/scala/skuber/apps/v1/Deployment.scala b/client/src/main/scala/skuber/apps/v1/Deployment.scala new file mode 100644 index 00000000..d6f8f26a --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1/Deployment.scala @@ -0,0 +1,187 @@ +package skuber.apps.v1 + +/** + * @author David O'Riordan + */ + +import skuber.LabelSelector.IsEqualRequirement +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +case class Deployment( + val kind: String ="Deployment", + override val apiVersion: String = appsAPIVersion, + val metadata: ObjectMeta = ObjectMeta(), + val spec: Option[Deployment.Spec] = None, + val status: Option[Deployment.Status] = None) + extends ObjectResource +{ + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + + lazy val copySpec = this.spec.getOrElse(new Deployment.Spec(selector=LabelSelector(), template=Pod.Template.Spec())) + + def withReplicas(count: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(count)))) + + def withTemplate(template: Pod.Template.Spec) = { + val updatedSpec = copySpec.copy(template = template) + this.copy(spec = Some(updatedSpec)) + } + + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=sel))) + + def getPodSpec = for { + spec <- this.spec + template = spec.template + spec <- template.spec + } yield spec + + /* + * A common deployment upgrade scenario would be to add or upgrade a single container e.g. update to a new image version + * This supports that by adding the specified container if one of same name not specified already, or just replacing + * the existing one of the same name if applicable. + * The modified Deployment can then be updated on Kubernetes to instigate the upgrade. + */ + def updateContainer(newContainer: Container): Deployment = { + val containers = getPodSpec map { _.containers } + val updatedContainers = containers map { list => + val existing = list.find(_.name==newContainer.name) + existing match { + case Some(_) => list.collect { + case c if c.name==newContainer.name => newContainer + case c => c + } + case None => newContainer::list + } + } + val newContainerList = updatedContainers.getOrElse(List(newContainer)) + val updatedPodSpec = getPodSpec.getOrElse(Pod.Spec()) + val newPodSpec = updatedPodSpec.copy(containers=newContainerList) + val updatedTemplate=copySpec.template.copy(spec=Some(newPodSpec)) + + this.copy(spec=Some(copySpec.copy(template=updatedTemplate))) + } +} + +object Deployment { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1", + scope = Scope.Namespaced, + names=Names( + plural = "deployments", + singular = "deployment", + kind = "Deployment", + shortNames = List("deploy") + ) + ) + implicit val deployDef = new ResourceDefinition[Deployment] { def spec=specification } + implicit val deployListDef = new ResourceDefinition[DeploymentList] { def spec=specification } + implicit val scDef = new Scale.SubresourceSpec[Deployment] { override def apiVersion = appsAPIVersion } + + def apply(name: String) = new Deployment(metadata=ObjectMeta(name=name)) + + case class Spec( + replicas: Option[Int] = Some(1), + selector: LabelSelector, + template: Pod.Template.Spec, + strategy: Option[Strategy] = None, + minReadySeconds: Int = 0, + revisionHistoryLimit: Option[Int] = None, + paused: Boolean = false, + progressDeadlineSeconds: Option[Int] = None) { + + def getStrategy: Strategy = strategy.getOrElse(Strategy.apply) + } + + object StrategyType extends Enumeration { + type StrategyType = Value + val Recreate, RollingUpdate = Value + } + + sealed trait Strategy { + def _type: StrategyType.StrategyType + def rollingUpdate: Option[RollingUpdate] + } + + object Strategy { + private[skuber] case class StrategyImpl(_type: StrategyType.StrategyType, rollingUpdate: Option[RollingUpdate]) extends Strategy + val Recreate = StrategyImpl(_type=StrategyType.Recreate, None) + def apply: Strategy = StrategyImpl(_type=StrategyType.RollingUpdate, rollingUpdate=Some(RollingUpdate())) + def apply(_type: StrategyType.StrategyType,rollingUpdate: Option[RollingUpdate] = None) : Strategy = StrategyImpl(_type, rollingUpdate) + def apply(rollingUpdate: RollingUpdate) : Strategy = StrategyImpl(_type=StrategyType.RollingUpdate, rollingUpdate=Some(rollingUpdate)) + def unapply(strategy: Strategy): Option[(StrategyType.StrategyType, Option[RollingUpdate])] = + Some(strategy._type,strategy.rollingUpdate) + } + + case class RollingUpdate( + maxUnavailable: IntOrString = Left(1), + maxSurge: IntOrString = Left(1)) + + case class Condition( + `type`:String, + status:String, + lastUpdateTime: Option[Timestamp], + lastTransitionTime:Option[Timestamp], + reason:Option[String], + message:Option[String]) + + case class Status( + replicas: Int=0, + updatedReplicas: Int=0, + readyReplicas: Int=0, + availableReplicas: Int = 0, + unavailableReplicas: Int = 0, + observedGeneration: Int = 0, + collisionCount: Option[Int] = None, + conditions: List[Condition] = Nil) + + // json formatters + + import play.api.libs.json.{Json, Format, JsPath} + import play.api.libs.functional.syntax._ + import skuber.json.format._ + + implicit val condFmt: Format[Condition] = Json.format[Condition] + implicit val depStatusFmt: Format[Status] = ( + (JsPath \ "replicas").formatMaybeEmptyInt() and + (JsPath \ "updatedReplicas").formatMaybeEmptyInt() and + (JsPath \ "readydReplicas").formatMaybeEmptyInt() and + (JsPath \ "availableReplicas").formatMaybeEmptyInt() and + (JsPath \ "unavailableReplicas").formatMaybeEmptyInt() and + (JsPath \ "observedGeneration").formatMaybeEmptyInt() and + (JsPath \ "collisionCount").formatNullable[Int] and + (JsPath \ "conditions").formatMaybeEmptyList[Condition] + )(Status.apply _, unlift(Status.unapply)) + + implicit val rollingUpdFmt: Format[RollingUpdate] = ( + (JsPath \ "maxUnavailable").formatMaybeEmptyIntOrString(Left(1)) and + (JsPath \ "maxSurge").formatMaybeEmptyIntOrString(Left(1)) + )(RollingUpdate.apply _, unlift(RollingUpdate.unapply)) + + implicit val depStrategyFmt: Format[Strategy] = ( + (JsPath \ "type").formatEnum(StrategyType, Some(StrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[RollingUpdate] + )(Strategy.apply _, unlift(Strategy.unapply)) + + implicit val depSpecFmt: Format[Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "selector").formatLabelSelector and + (JsPath \ "template").format[Pod.Template.Spec] and + (JsPath \ "strategy").formatNullable[Strategy] and + (JsPath \ "minReadySeconds").formatMaybeEmptyInt() and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] and + (JsPath \ "paused").formatMaybeEmptyBoolean() and + (JsPath \ "progressDeadlineSeconds").formatNullable[Int] + )(Spec.apply _, unlift(Spec.unapply)) + + implicit lazy val depFormat: Format[Deployment] = ( + objFormat and + (JsPath \ "spec").formatNullable[Spec] and + (JsPath \ "status").formatNullable[Status] + )(Deployment.apply _, unlift(Deployment.unapply)) + + implicit val deployListFormat: Format[DeploymentList] = ListResourceFormat[Deployment] +} + + diff --git a/client/src/main/scala/skuber/apps/v1/ReplicaSet.scala b/client/src/main/scala/skuber/apps/v1/ReplicaSet.scala new file mode 100644 index 00000000..b69650d5 --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1/ReplicaSet.scala @@ -0,0 +1,130 @@ +package skuber.apps.v1 + + +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +/** + * @author David O'Riordan + */ +case class ReplicaSet( + val kind: String ="ReplicaSet", + override val apiVersion: String = appsAPIVersion, + val metadata: ObjectMeta = ObjectMeta(), + spec: Option[ReplicaSet.Spec] = None, + status: Option[ReplicaSet.Status] = None) + extends ObjectResource { + + lazy val copySpec = this.spec.getOrElse(new ReplicaSet.Spec(selector=LabelSelector(), template=Pod.Template.Spec())) + + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + def addLabel(label: Tuple2[String, String]) : ReplicaSet = this.copy(metadata = metadata.copy(labels = metadata.labels + label)) + def addLabels(newLabels: Map[String, String]) : ReplicaSet = this.copy(metadata = metadata.copy(labels = metadata.labels ++ newLabels)) + def addAnnotation(anno: Tuple2[String, String]) : ReplicaSet = this.copy(metadata = metadata.copy(annotations = metadata.annotations + anno)) + def addAnnotations(annos: Map[String, String]) : ReplicaSet = this.copy(metadata = metadata.copy(annotations = metadata.annotations ++ annos)) + + def withReplicas(n: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(n)))) + + + def withSelector(s: LabelSelector) : ReplicaSet = this.copy(spec=Some(copySpec.copy(selector = s))) + def withSelector(s: Tuple2[String,String]) : ReplicaSet = withSelector(LabelSelector(LabelSelector.IsEqualRequirement(s._1,s._2))) + + /* + * Set the template. This will set the selector from the template labels, if they exist + * and the selector is empty + */ + def withTemplate(t: Pod.Template.Spec) = { + val withTmpl = this.copy(spec = Some(copySpec.copy(template = t))) + val withSelector = (t.metadata.labels, spec.map{_.selector}) match { + case (labels, selector) if (!labels.isEmpty && !selector.equals(LabelSelector())) => + val reqs = labels map { label: (String, String) => + LabelSelector.IsEqualRequirement(label._1, label._2) + } + val selector = LabelSelector(reqs.toSeq: _*) + withTmpl.withSelector(selector) + case _ => withTmpl + } + // copy template labels into RS labels, if not already set + (metadata.labels,t.metadata.labels) match { + case (curr,default) if (curr.isEmpty && !default.isEmpty) => + withSelector.addLabels(default) + case _ => withSelector + } + } + + /* + * Set the template from a given Pod spec and optional set of labels + * If the selector isn't already set then this will generate it from the labels. + */ + def withPodSpec(t: Pod.Spec, labels: Map[String, String]=Map()) = { + val template = new Pod.Template.Spec(metadata=ObjectMeta(labels=labels),spec=Some(t)) + withTemplate(template) + } +} + +object ReplicaSet { + + val specification=NonCoreResourceSpecification( + group = Some("extensions"), + version = "v1", + scope = Scope.Namespaced, + names = Names( + plural = "replicasets", + singular = "replicaset", + kind = "ReplicaSet", + shortNames = List("rs") + ) + ) + implicit val rsDef = new ResourceDefinition[ReplicaSet] { def spec=specification } + implicit val rsListDef = new ResourceDefinition[ReplicaSetList] { def spec=specification } + implicit val scDef = new Scale.SubresourceSpec[ReplicaSet] { override def apiVersion: String = "extensions/v1beta1"} + + def apply(name: String) : ReplicaSet = ReplicaSet(metadata=ObjectMeta(name=name)) + def apply(name: String, spec: ReplicaSet.Spec) : ReplicaSet = + ReplicaSet(metadata=ObjectMeta(name=name), spec = Some(spec)) + def apply(name:String, container: Container) : ReplicaSet = { + val podSpec=Pod.Spec(containers=List(container)) + ReplicaSet(name, podSpec, Map[String,String]()) + } + def apply( + name:String, + podSpec: Pod.Spec, + labels: Map[String,String]) : ReplicaSet = + { + val meta=ObjectMeta(name=name, labels = labels) + ReplicaSet(metadata=meta).withPodSpec(podSpec, labels) + } + + case class Spec( + replicas: Option[Int]=Some(1), + minReadySeconds: Option[Int] = None, + selector: LabelSelector, + template: Pod.Template.Spec) + + case class Status( + replicas: Int, + fullyLabeledReplicas: Option[Int], + observerdGeneration: Option[Int]) + + // json formatters + + import play.api.libs.json.{Json, Format, JsPath} + import play.api.libs.functional.syntax._ + import skuber.json.format._ + + implicit val replsetSpecFormat: Format[ReplicaSet.Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "minReadySeconds").formatNullable[Int] and + (JsPath \ "selector").formatLabelSelector and + (JsPath \ "template").format[Pod.Template.Spec] + )(ReplicaSet.Spec.apply _, unlift(ReplicaSet.Spec.unapply)) + + implicit val replsetStatusFormat = Json.format[ReplicaSet.Status] + + implicit lazy val replsetFormat: Format[ReplicaSet] = ( + objFormat and + (JsPath \ "spec").formatNullable[ReplicaSet.Spec] and + (JsPath \ "status").formatNullable[ReplicaSet.Status] + )(ReplicaSet.apply _, unlift(ReplicaSet.unapply)) + +} diff --git a/client/src/main/scala/skuber/apps/v1/StatefulSet.scala b/client/src/main/scala/skuber/apps/v1/StatefulSet.scala new file mode 100644 index 00000000..604330d5 --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1/StatefulSet.scala @@ -0,0 +1,124 @@ +package skuber.apps.v1 + +/** + * @author David O'Riordan + */ + +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath, Json} +import skuber.json.format._ // reuse some core skuber json formatters + +case class StatefulSet(override val kind: String ="StatefulSet", + override val apiVersion: String = appsAPIVersion, + metadata: ObjectMeta, + spec: Option[StatefulSet.Spec] = None, + status: Option[StatefulSet.Status] = None) extends ObjectResource +{ + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + + lazy val copySpec = this.spec.getOrElse(new StatefulSet.Spec(template = Pod.Template.Spec())) + private val rollingUpdateStrategy = StatefulSet.UpdateStrategy(`type`=StatefulSet.UpdateStrategyType.RollingUpdate, None) + private def rollingUpdateStrategy(partition: Int)= + StatefulSet.UpdateStrategy(`type`=StatefulSet.UpdateStrategyType.RollingUpdate,Some(StatefulSet.RollingUpdateStrategy(partition))) + + def withReplicas(count: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(count)))) + def withServiceName(serviceName: String) = this.copy(spec=Some(copySpec.copy(serviceName=Some(serviceName)))) + def withTemplate(template: Pod.Template.Spec) = this.copy(spec=Some(copySpec.copy(template=template))) + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=Some(sel)))) + def withRollingUpdateStrategyPartition(partition:Int) = this.copy(spec=Some(copySpec.copy(updateStrategy = Some(rollingUpdateStrategy(partition))))) + def withVolumeClaimTemplate(claim: PersistentVolumeClaim) = { + val spec = copySpec.withVolumeClaimTemplate(claim) + this.copy(spec=Some(spec)) + } +} + +object StatefulSet { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1", + scope = Scope.Namespaced, + names=Names( + plural = "statefulsets", + singular = "statefulset", + kind = "StatefulSet", + shortNames = List() + ) + ) + implicit val stsDef = new ResourceDefinition[StatefulSet] { def spec=specification } + implicit val stsListDef = new ResourceDefinition[StatefulSetList] { def spec=specification } + implicit val scDef = new Scale.SubresourceSpec[StatefulSet] { override def apiVersion = appsAPIVersion } + + def apply(name: String): StatefulSet = StatefulSet(metadata=ObjectMeta(name=name)) + + object PodManagementPolicyType extends Enumeration { + type PodManagementPolicyType = Value + val OrderedReady,Parallel = Value + } + + object UpdateStrategyType extends Enumeration { + type UpdateStrategyType = Value + val OnDelete,RollingUpdate = Value + } + + case class UpdateStrategy(`type`: UpdateStrategyType.UpdateStrategyType, rollingUpdate: Option[RollingUpdateStrategy]=None) + case class RollingUpdateStrategy(partition: Int) + + case class Spec(replicas: Option[Int] = Some(1), + serviceName: Option[String] = None, + selector: Option[LabelSelector] = None, + template: Pod.Template.Spec, + volumeClaimTemplates: List[PersistentVolumeClaim] = Nil, + podManagmentPolicy: Option[PodManagementPolicyType.PodManagementPolicyType] = None, + updateStrategy: Option[UpdateStrategy] = None, + revisionHistoryLimit: Option[Int] = None) + { + def withVolumeClaimTemplate(claim: PersistentVolumeClaim) = copy(volumeClaimTemplates = claim :: volumeClaimTemplates) + } + + case class Condition(`type`:String,status:String,lastTransitionTime:Option[Timestamp],reason:Option[String],message:Option[String]) + + case class Status(observedGeneration: Option[Int], + replicas: Int, + readyReplicas: Option[Int], + updatedReplicas: Option[Int], + currentRevision: Option[String], + updateRevision: Option[String], + collisionCount: Option[Int], + conditions: Option[List[Condition]]) + + // json formatters + + implicit val statefulSetPodPcyMgmtFmt: Format[StatefulSet.PodManagementPolicyType.PodManagementPolicyType] = Format(enumReads(StatefulSet.PodManagementPolicyType, StatefulSet.PodManagementPolicyType.OrderedReady), enumWrites) + implicit val statefulSetRollUp: Format[StatefulSet.RollingUpdateStrategy] = Json.format[StatefulSet.RollingUpdateStrategy] + implicit val statefulSetUpdStrFmt: Format[StatefulSet.UpdateStrategy] = ( + (JsPath \ "type").formatEnum(StatefulSet.UpdateStrategyType, Some(StatefulSet.UpdateStrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[StatefulSet.RollingUpdateStrategy] + )(StatefulSet.UpdateStrategy.apply _,unlift(StatefulSet.UpdateStrategy.unapply)) + + implicit val statefulSetSpecFmt: Format[StatefulSet.Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "serviceName").formatNullable[String] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").format[Pod.Template.Spec] and + (JsPath \ "volumeClaimTemplates").formatMaybeEmptyList[PersistentVolumeClaim] and + (JsPath \ "podManagmentPolicy").formatNullableEnum(StatefulSet.PodManagementPolicyType) and + (JsPath \ "updateStrategy").formatNullable[StatefulSet.UpdateStrategy] and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] + )(StatefulSet.Spec.apply _, unlift(StatefulSet.Spec.unapply)) + + implicit val statefulSetCondFmt: Format[StatefulSet.Condition] = Json.format[StatefulSet.Condition] + implicit val statefulSetStatusFmt: Format[StatefulSet.Status] = Json.format[StatefulSet.Status] + + implicit lazy val statefulSetFormat: Format[StatefulSet] = ( + objFormat and + (JsPath \ "spec").formatNullable[StatefulSet.Spec] and + (JsPath \ "status").formatNullable[StatefulSet.Status] + ) (StatefulSet.apply _, unlift(StatefulSet.unapply)) + + implicit val statefulSetListFormat: Format[StatefulSetList] = ListResourceFormat[StatefulSet] +} + diff --git a/client/src/main/scala/skuber/apps/v1/package.scala b/client/src/main/scala/skuber/apps/v1/package.scala new file mode 100644 index 00000000..8a7fc704 --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1/package.scala @@ -0,0 +1,17 @@ +package skuber.apps + +import skuber.ListResource + +/** + * @author David O'Riordan + * + * This package supports the "apps + */ +package object v1 { + val appsAPIVersion = "apps/v1" + + type StatefulSetList = ListResource[skuber.apps.v1.StatefulSet] + type DeploymentList = ListResource[skuber.apps.v1.Deployment] + type ReplicaSetList = ListResource[skuber.apps.v1.ReplicaSet] + type DaemonSetList = ListResource[skuber.apps.v1.DaemonSet] +} diff --git a/client/src/main/scala/skuber/apps/v1beta1/Deployment.scala b/client/src/main/scala/skuber/apps/v1beta1/Deployment.scala new file mode 100644 index 00000000..b5d474ba --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1beta1/Deployment.scala @@ -0,0 +1,158 @@ +package skuber.apps.v1beta1 + +/** + * @author David O'Riordan + */ + +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +case class Deployment( + val kind: String ="Deployment", + override val apiVersion: String = appsAPIVersion, + val metadata: ObjectMeta = ObjectMeta(), + val spec: Option[Deployment.Spec] = None, + val status: Option[Deployment.Status] = None) + extends ObjectResource +{ + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + + lazy val copySpec = this.spec.getOrElse(new Deployment.Spec) + + def withReplicas(count: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(count)))) + def withTemplate(template: Pod.Template.Spec) = this.copy(spec=Some(copySpec.copy(template=Some(template)))) + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=Some(sel)))) + + def getPodSpec = for { + spec <- this.spec + template <- spec.template + spec <- template.spec + } yield spec + + /* + * A common deployment upgrade scenario would be to add or upgrade a single container e.g. update to a new image version + * This supports that by adding the specified container if one of same name not specified already, or just replacing + * the existing one of the same name if applicable. + * The modified Deployment can then be updated on Kubernetes to instigate the upgrade. + */ + def updateContainer(newContainer: Container): Deployment = { + val containers = getPodSpec map { _.containers } + val updatedContainers = containers map { list => + val existing = list.find(_.name==newContainer.name) + existing match { + case Some(_) => list.collect { + case c if c.name==newContainer.name => newContainer + case c => c + } + case None => newContainer::list + } + } + val newContainerList = updatedContainers.getOrElse(List(newContainer)) + val updatedPodSpec = getPodSpec.getOrElse(Pod.Spec()) + val newPodSpec = updatedPodSpec.copy(containers=newContainerList) + val updatedTemplate=copySpec.template.getOrElse(Pod.Template.Spec()).copy(spec=Some(newPodSpec)) + + this.copy(spec=Some(copySpec.copy(template=Some(updatedTemplate)))) + } +} + +object Deployment { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1beta1", + scope = Scope.Namespaced, + names=Names( + plural = "deployments", + singular = "deployment", + kind = "Deployment", + shortNames = List("deploy") + ) + ) + implicit val deployDef = new ResourceDefinition[Deployment] { def spec= specification } + implicit val deployListDef = new ResourceDefinition[DeploymentList] { def spec= specification } + implicit val scDef = new Scale.SubresourceSpec[Deployment] { override def apiVersion = appsAPIVersion } + + def apply(name: String) = new Deployment(metadata=ObjectMeta(name=name)) + + case class Spec( + replicas: Option[Int] = Some(1), + selector: Option[LabelSelector] = None, + template: Option[Pod.Template.Spec] = None, + strategy: Option[Strategy] = None, + minReadySeconds: Int = 0) { + + def getStrategy: Strategy = strategy.getOrElse(Strategy.apply) + } + + object StrategyType extends Enumeration { + type StrategyType = Value + val Recreate, RollingUpdate = Value + } + + sealed trait Strategy { + def _type: StrategyType.StrategyType + def rollingUpdate: Option[RollingUpdate] + } + + object Strategy { + private[skuber] case class StrategyImpl(_type: StrategyType.StrategyType, rollingUpdate: Option[RollingUpdate]) extends Strategy + def apply: Strategy = StrategyImpl(_type=StrategyType.Recreate, rollingUpdate=None) + def apply(_type: StrategyType.StrategyType,rollingUpdate: Option[RollingUpdate]) : Strategy = StrategyImpl(_type, rollingUpdate) + def apply(rollingUpdate: RollingUpdate) : Strategy = StrategyImpl(_type=StrategyType.RollingUpdate, rollingUpdate=Some(rollingUpdate)) + def unapply(strategy: Strategy): Option[(StrategyType.StrategyType, Option[RollingUpdate])] = + Some(strategy._type,strategy.rollingUpdate) + } + + case class RollingUpdate( + maxUnavailable: IntOrString = Left(1), + maxSurge: IntOrString = Left(1)) + + case class Status( + replicas: Int=0, + updatedReplicas: Int=0, + availableReplicas: Int = 0, + unavailableReplicas: Int = 0, + observedGeneration: Int = 0) + + // json formatters + + import play.api.libs.functional.syntax._ + import play.api.libs.json.{Format, JsPath, Json} + import skuber.json.format._ // reuse some core skuber json formatters + + implicit val depStatusFmt: Format[Status] = ( + (JsPath \ "replicas").formatMaybeEmptyInt() and + (JsPath \ "updatedReplicas").formatMaybeEmptyInt() and + (JsPath \ "availableReplicas").formatMaybeEmptyInt() and + (JsPath \ "unavailableReplicas").formatMaybeEmptyInt() and + (JsPath \ "observedGeneration").formatMaybeEmptyInt() + )(Status.apply _, unlift(Status.unapply)) + + implicit val rollingUpdFmt: Format[RollingUpdate] = ( + (JsPath \ "maxUnavailable").formatMaybeEmptyIntOrString(Left(1)) and + (JsPath \ "maxSurge").formatMaybeEmptyIntOrString(Left(1)) + )(RollingUpdate.apply _, unlift(RollingUpdate.unapply)) + + implicit val depStrategyFmt: Format[Strategy] = ( + (JsPath \ "type").formatEnum(StrategyType, Some(StrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[RollingUpdate] + )(Strategy.apply _, unlift(Strategy.unapply)) + + implicit val depSpecFmt: Format[Deployment.Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").formatNullable[Pod.Template.Spec] and + (JsPath \ "strategy").formatNullable[Deployment.Strategy] and + (JsPath \ "minReadySeconds").formatMaybeEmptyInt() + )(Spec.apply _, unlift(Spec.unapply)) + + implicit lazy val depFormat: Format[Deployment] = ( + objFormat and + (JsPath \ "spec").formatNullable[Spec] and + (JsPath \ "status").formatNullable[Status] + )(Deployment.apply _, unlift(Deployment.unapply)) + + implicit val deployListFormat: Format[DeploymentList] = ListResourceFormat[Deployment] +} + diff --git a/client/src/main/scala/skuber/apps/v1beta1/package.scala b/client/src/main/scala/skuber/apps/v1beta1/package.scala index 4880ff30..ededb4f1 100644 --- a/client/src/main/scala/skuber/apps/v1beta1/package.scala +++ b/client/src/main/scala/skuber/apps/v1beta1/package.scala @@ -5,6 +5,7 @@ import skuber.ListResource package object v1beta1 { val appsAPIVersion = "apps/v1beta1" + type DeploymentList = ListResource[skuber.apps.v1beta1.Deployment] type StatefulSetList = ListResource[skuber.apps.v1beta1.StatefulSet] } diff --git a/client/src/main/scala/skuber/apps/v1beta2/Deployment.scala b/client/src/main/scala/skuber/apps/v1beta2/Deployment.scala new file mode 100644 index 00000000..7e36fb59 --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1beta2/Deployment.scala @@ -0,0 +1,158 @@ +package skuber.apps.v1beta2 + +/** + * @author David O'Riordan + */ + +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +case class Deployment( + val kind: String ="Deployment", + override val apiVersion: String = appsAPIVersion, + val metadata: ObjectMeta = ObjectMeta(), + val spec: Option[Deployment.Spec] = None, + val status: Option[Deployment.Status] = None) + extends ObjectResource +{ + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + + lazy val copySpec = this.spec.getOrElse(new Deployment.Spec) + + def withReplicas(count: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(count)))) + def withTemplate(template: Pod.Template.Spec) = this.copy(spec=Some(copySpec.copy(template=Some(template)))) + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=Some(sel)))) + + def getPodSpec = for { + spec <- this.spec + template <- spec.template + spec <- template.spec + } yield spec + + /* + * A common deployment upgrade scenario would be to add or upgrade a single container e.g. update to a new image version + * This supports that by adding the specified container if one of same name not specified already, or just replacing + * the existing one of the same name if applicable. + * The modified Deployment can then be updated on Kubernetes to instigate the upgrade. + */ + def updateContainer(newContainer: Container): Deployment = { + val containers = getPodSpec map { _.containers } + val updatedContainers = containers map { list => + val existing = list.find(_.name==newContainer.name) + existing match { + case Some(_) => list.collect { + case c if c.name==newContainer.name => newContainer + case c => c + } + case None => newContainer::list + } + } + val newContainerList = updatedContainers.getOrElse(List(newContainer)) + val updatedPodSpec = getPodSpec.getOrElse(Pod.Spec()) + val newPodSpec = updatedPodSpec.copy(containers=newContainerList) + val updatedTemplate=copySpec.template.getOrElse(Pod.Template.Spec()).copy(spec=Some(newPodSpec)) + + this.copy(spec=Some(copySpec.copy(template=Some(updatedTemplate)))) + } +} + +object Deployment { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1beta2", + scope = Scope.Namespaced, + names=Names( + plural = "deployments", + singular = "deployment", + kind = "Deployment", + shortNames = List("deploy") + ) + ) + implicit val deployDef = new ResourceDefinition[Deployment] { def spec= specification } + implicit val deployListDef = new ResourceDefinition[DeploymentList] { def spec= specification } + implicit val scDef = new Scale.SubresourceSpec[Deployment] { override def apiVersion = appsAPIVersion } + + def apply(name: String) = new Deployment(metadata=ObjectMeta(name=name)) + + case class Spec( + replicas: Option[Int] = Some(1), + selector: Option[LabelSelector] = None, + template: Option[Pod.Template.Spec] = None, + strategy: Option[Strategy] = None, + minReadySeconds: Int = 0) { + + def getStrategy: Strategy = strategy.getOrElse(Strategy.apply) + } + + object StrategyType extends Enumeration { + type StrategyType = Value + val Recreate, RollingUpdate = Value + } + + sealed trait Strategy { + def _type: StrategyType.StrategyType + def rollingUpdate: Option[RollingUpdate] + } + + object Strategy { + private[skuber] case class StrategyImpl(_type: StrategyType.StrategyType, rollingUpdate: Option[RollingUpdate]) extends Strategy + def apply: Strategy = StrategyImpl(_type=StrategyType.Recreate, rollingUpdate=None) + def apply(_type: StrategyType.StrategyType,rollingUpdate: Option[RollingUpdate]) : Strategy = StrategyImpl(_type, rollingUpdate) + def apply(rollingUpdate: RollingUpdate) : Strategy = StrategyImpl(_type=StrategyType.RollingUpdate, rollingUpdate=Some(rollingUpdate)) + def unapply(strategy: Strategy): Option[(StrategyType.StrategyType, Option[RollingUpdate])] = + Some(strategy._type,strategy.rollingUpdate) + } + + case class RollingUpdate( + maxUnavailable: IntOrString = Left(1), + maxSurge: IntOrString = Left(1)) + + case class Status( + replicas: Int=0, + updatedReplicas: Int=0, + availableReplicas: Int = 0, + unavailableReplicas: Int = 0, + observedGeneration: Int = 0) + + // json formatters + + import play.api.libs.functional.syntax._ + import play.api.libs.json.{Format, JsPath, Json} + import skuber.json.format._ // reuse some core skuber json formatters + + implicit val depStatusFmt: Format[Status] = ( + (JsPath \ "replicas").formatMaybeEmptyInt() and + (JsPath \ "updatedReplicas").formatMaybeEmptyInt() and + (JsPath \ "availableReplicas").formatMaybeEmptyInt() and + (JsPath \ "unavailableReplicas").formatMaybeEmptyInt() and + (JsPath \ "observedGeneration").formatMaybeEmptyInt() + )(Status.apply _, unlift(Status.unapply)) + + implicit val rollingUpdFmt: Format[RollingUpdate] = ( + (JsPath \ "maxUnavailable").formatMaybeEmptyIntOrString(Left(1)) and + (JsPath \ "maxSurge").formatMaybeEmptyIntOrString(Left(1)) + )(RollingUpdate.apply _, unlift(RollingUpdate.unapply)) + + implicit val depStrategyFmt: Format[Strategy] = ( + (JsPath \ "type").formatEnum(StrategyType, Some(StrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[RollingUpdate] + )(Strategy.apply _, unlift(Strategy.unapply)) + + implicit val depSpecFmt: Format[Deployment.Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").formatNullable[Pod.Template.Spec] and + (JsPath \ "strategy").formatNullable[Deployment.Strategy] and + (JsPath \ "minReadySeconds").formatMaybeEmptyInt() + )(Spec.apply _, unlift(Spec.unapply)) + + implicit lazy val depFormat: Format[Deployment] = ( + objFormat and + (JsPath \ "spec").formatNullable[Spec] and + (JsPath \ "status").formatNullable[Status] + )(Deployment.apply _, unlift(Deployment.unapply)) + + implicit val deployListFormat: Format[DeploymentList] = ListResourceFormat[Deployment] +} + diff --git a/client/src/main/scala/skuber/apps/v1beta2/StatefulSet.scala b/client/src/main/scala/skuber/apps/v1beta2/StatefulSet.scala new file mode 100644 index 00000000..602709f1 --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1beta2/StatefulSet.scala @@ -0,0 +1,125 @@ +package skuber.apps.v1beta2 + +/** + * @author David O'Riordan + */ + +import skuber.ResourceSpecification.{Names, Scope} +import skuber._ + +case class StatefulSet(override val kind: String ="StatefulSet", + override val apiVersion: String = appsAPIVersion, + metadata: ObjectMeta, + spec: Option[StatefulSet.Spec] = None, + status: Option[StatefulSet.Status] = None) extends ObjectResource +{ + def withResourceVersion(version: String) = this.copy(metadata = metadata.copy(resourceVersion=version)) + + lazy val copySpec = this.spec.getOrElse(new StatefulSet.Spec(template = Pod.Template.Spec())) + private val rollingUpdateStrategy = StatefulSet.UpdateStrategy(`type`=StatefulSet.UpdateStrategyType.RollingUpdate, None) + private def rollingUpdateStrategy(partition: Int)= + StatefulSet.UpdateStrategy(`type`=StatefulSet.UpdateStrategyType.RollingUpdate,Some(StatefulSet.RollingUpdateStrategy(partition))) + + def withReplicas(count: Int) = this.copy(spec=Some(copySpec.copy(replicas=Some(count)))) + def withServiceName(serviceName: String) = this.copy(spec=Some(copySpec.copy(serviceName=Some(serviceName)))) + def withTemplate(template: Pod.Template.Spec) = this.copy(spec=Some(copySpec.copy(template=template))) + def withLabelSelector(sel: LabelSelector) = this.copy(spec=Some(copySpec.copy(selector=Some(sel)))) + def withRollingUpdateStrategyPartition(partition:Int) = this.copy(spec=Some(copySpec.copy(updateStrategy = Some(rollingUpdateStrategy(partition))))) + def withVolumeClaimTemplate(claim: PersistentVolumeClaim) = { + val spec = copySpec.withVolumeClaimTemplate(claim) + this.copy(spec=Some(spec)) + } +} + +object StatefulSet { + + val specification=NonCoreResourceSpecification ( + group=Some("apps"), + version="v1beta2", + scope = Scope.Namespaced, + names=Names( + plural = "statefulsets", + singular = "statefulset", + kind = "StatefulSet", + shortNames = List() + ) + ) + implicit val stsDef = new ResourceDefinition[StatefulSet] { def spec=specification } + implicit val stsListDef = new ResourceDefinition[StatefulSetList] { def spec=specification } + implicit val scDef = new Scale.SubresourceSpec[StatefulSet] { override def apiVersion = appsAPIVersion } + + def apply(name: String): StatefulSet = StatefulSet(metadata=ObjectMeta(name=name)) + + object PodManagementPolicyType extends Enumeration { + type PodManagementPolicyType = Value + val OrderedReady,Parallel = Value + } + + object UpdateStrategyType extends Enumeration { + type UpdateStrategyType = Value + val OnDelete,RollingUpdate = Value + } + + case class UpdateStrategy(`type`: UpdateStrategyType.UpdateStrategyType, rollingUpdate: Option[RollingUpdateStrategy]=None) + case class RollingUpdateStrategy(partition: Int) + + case class Spec(replicas: Option[Int] = Some(1), + serviceName: Option[String] = None, + selector: Option[LabelSelector] = None, + template: Pod.Template.Spec, + volumeClaimTemplates: List[PersistentVolumeClaim] = Nil, + podManagmentPolicy: Option[PodManagementPolicyType.PodManagementPolicyType] = None, + updateStrategy: Option[UpdateStrategy] = None, + revisionHistoryLimit: Option[Int] = None) + { + def withVolumeClaimTemplate(claim: PersistentVolumeClaim) = copy(volumeClaimTemplates = claim :: volumeClaimTemplates) + } + + case class Condition(`type`:String,status:String,lastTransitionTime:Option[Timestamp],reason:Option[String],message:Option[String]) + + case class Status(observedGeneration: Option[Int], + replicas: Int, + readyReplicas: Option[Int], + updatedReplicas: Option[Int], + currentRevision: Option[String], + updateRevision: Option[String], + collisionCount: Option[Int], + conditions: Option[List[Condition]]) + + // json formatters + + import play.api.libs.functional.syntax._ + import play.api.libs.json.{Format, JsPath, Json} + import skuber.json.format._ // reuse some core skuber json formatters + + implicit val statefulSetPodPcyMgmtFmt: Format[StatefulSet.PodManagementPolicyType.PodManagementPolicyType] = Format(enumReads(StatefulSet.PodManagementPolicyType, StatefulSet.PodManagementPolicyType.OrderedReady), enumWrites) + implicit val statefulSetRollUp: Format[StatefulSet.RollingUpdateStrategy] = Json.format[StatefulSet.RollingUpdateStrategy] + implicit val statefulSetUpdStrFmt: Format[StatefulSet.UpdateStrategy] = ( + (JsPath \ "type").formatEnum(StatefulSet.UpdateStrategyType, Some(StatefulSet.UpdateStrategyType.RollingUpdate)) and + (JsPath \ "rollingUpdate").formatNullable[StatefulSet.RollingUpdateStrategy] + )(StatefulSet.UpdateStrategy.apply _,unlift(StatefulSet.UpdateStrategy.unapply)) + + implicit val statefulSetSpecFmt: Format[StatefulSet.Spec] = ( + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "serviceName").formatNullable[String] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").format[Pod.Template.Spec] and + (JsPath \ "volumeClaimTemplates").formatMaybeEmptyList[PersistentVolumeClaim] and + (JsPath \ "podManagmentPolicy").formatNullableEnum(StatefulSet.PodManagementPolicyType) and + (JsPath \ "updateStrategy").formatNullable[StatefulSet.UpdateStrategy] and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] + )(StatefulSet.Spec.apply _, unlift(StatefulSet.Spec.unapply)) + + implicit val statefulSetCondFmt: Format[StatefulSet.Condition] = Json.format[StatefulSet.Condition] + implicit val statefulSetStatusFmt: Format[StatefulSet.Status] = Json.format[StatefulSet.Status] + + implicit lazy val statefulSetFormat: Format[StatefulSet] = ( + objFormat and + (JsPath \ "spec").formatNullable[StatefulSet.Spec] and + (JsPath \ "status").formatNullable[StatefulSet.Status] + ) (StatefulSet.apply _, unlift(StatefulSet.unapply)) + + implicit val statefulSetListFormat: Format[StatefulSetList] = ListResourceFormat[StatefulSet] +} + + diff --git a/client/src/main/scala/skuber/apps/v1beta2/package.scala b/client/src/main/scala/skuber/apps/v1beta2/package.scala new file mode 100644 index 00000000..f13ec70a --- /dev/null +++ b/client/src/main/scala/skuber/apps/v1beta2/package.scala @@ -0,0 +1,16 @@ +package skuber.apps + + +import skuber.ListResource + +/** + * @author David O'Riordan + * + * This package supports the "apps + */ +package object v1beta2 { + val appsAPIVersion = "apps/v1beta2" + + type StatefulSetList = ListResource[skuber.apps.v1beta2.StatefulSet] + type DeploymentList = ListResource[skuber.apps.v1beta2.Deployment] +} diff --git a/client/src/main/scala/skuber/json/ext/format/package.scala b/client/src/main/scala/skuber/json/ext/format/package.scala index df20d7fa..e374f108 100644 --- a/client/src/main/scala/skuber/json/ext/format/package.scala +++ b/client/src/main/scala/skuber/json/ext/format/package.scala @@ -58,32 +58,32 @@ package object format { implicit val daemonSetUpdateStrategyFmt: Format[DaemonSet.UpdateStrategy] = Json.format[DaemonSet.UpdateStrategy] implicit val daemonsetStatusFmt: Format[DaemonSet.Status] = Json.format[DaemonSet.Status] implicit val daemonsetSpecFmt: Format[DaemonSet.Spec] = ( - (JsPath \ "minReadySeconds").formatMaybeEmptyInt() and - (JsPath \ "selector").formatNullableLabelSelector and - (JsPath \ "template").formatNullable[Pod.Template.Spec] and - (JsPath \ "updateStrategy").formatNullable[DaemonSet.UpdateStrategy] and - (JsPath \ "revisionHistoryLimit").formatNullable[Int] - )(DaemonSet.Spec.apply, unlift(DaemonSet.Spec.unapply)) + (JsPath \ "minReadySeconds").formatMaybeEmptyInt() and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").formatNullable[Pod.Template.Spec] and + (JsPath \ "updateStrategy").formatNullable[DaemonSet.UpdateStrategy] and + (JsPath \ "revisionHistoryLimit").formatNullable[Int] + )(DaemonSet.Spec.apply, unlift(DaemonSet.Spec.unapply)) implicit lazy val daemonsetFmt: Format[DaemonSet] = ( objFormat and - (JsPath \ "spec").formatNullable[DaemonSet.Spec] and - (JsPath \ "status").formatNullable[DaemonSet.Status] - ) (DaemonSet.apply _, unlift(DaemonSet.unapply)) + (JsPath \ "spec").formatNullable[DaemonSet.Spec] and + (JsPath \ "status").formatNullable[DaemonSet.Status] + ) (DaemonSet.apply _, unlift(DaemonSet.unapply)) implicit val replsetSpecFormat: Format[ReplicaSet.Spec] = ( - (JsPath \ "replicas").formatNullable[Int] and - (JsPath \ "selector").formatNullableLabelSelector and - (JsPath \ "template").formatNullable[Pod.Template.Spec] - )(ReplicaSet.Spec.apply _, unlift(ReplicaSet.Spec.unapply)) + (JsPath \ "replicas").formatNullable[Int] and + (JsPath \ "selector").formatNullableLabelSelector and + (JsPath \ "template").formatNullable[Pod.Template.Spec] + )(ReplicaSet.Spec.apply _, unlift(ReplicaSet.Spec.unapply)) implicit val replsetStatusFormat = Json.format[ReplicaSet.Status] implicit lazy val replsetFormat: Format[ReplicaSet] = ( - objFormat and - (JsPath \ "spec").formatNullable[ReplicaSet.Spec] and - (JsPath \ "status").formatNullable[ReplicaSet.Status] - ) (ReplicaSet.apply _, unlift(ReplicaSet.unapply)) + objFormat and + (JsPath \ "spec").formatNullable[ReplicaSet.Spec] and + (JsPath \ "status").formatNullable[ReplicaSet.Status] + ) (ReplicaSet.apply _, unlift(ReplicaSet.unapply)) implicit val ingressBackendFmt: Format[Ingress.Backend] = Json.format[Ingress.Backend] @@ -98,29 +98,29 @@ package object format { implicit val ingressTLSFmt: Format[Ingress.TLS] = Json.format[Ingress.TLS] implicit val ingressSpecFormat: Format[Ingress.Spec] = ( - (JsPath \ "backend").formatNullable[Ingress.Backend] and - (JsPath \ "rules").formatMaybeEmptyList[Ingress.Rule] and - (JsPath \ "tls").formatMaybeEmptyList[Ingress.TLS] - )(Ingress.Spec.apply _, unlift(Ingress.Spec.unapply)) + (JsPath \ "backend").formatNullable[Ingress.Backend] and + (JsPath \ "rules").formatMaybeEmptyList[Ingress.Rule] and + (JsPath \ "tls").formatMaybeEmptyList[Ingress.TLS] + )(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 - ) + (JsPath \ "ingress").formatMaybeEmptyList[Ingress.Status.LoadBalancer.Ingress].inmap( + ings => Ingress.Status.LoadBalancer(ings), + lb => lb.ingress + ) ) implicit val ingressStatusFormat = 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)) + objFormat and + (JsPath \ "spec").formatNullable[Ingress.Spec] and + (JsPath \ "status").formatNullable[Ingress.Status] +) (Ingress.apply _, unlift(Ingress.unapply)) implicit val daesetListFmt: Format[DaemonSetList] = ListResourceFormat[DaemonSet] implicit val replsetListFmt: Format[ReplicaSetList] = ListResourceFormat[ReplicaSet] diff --git a/client/src/test/scala/skuber/apps/DeploymentSpec.scala b/client/src/test/scala/skuber/apps/DeploymentSpec.scala index 4d9e0dda..eb934c2e 100644 --- a/client/src/test/scala/skuber/apps/DeploymentSpec.scala +++ b/client/src/test/scala/skuber/apps/DeploymentSpec.scala @@ -2,7 +2,6 @@ package skuber.apps import org.specs2.mutable.Specification import play.api.libs.json.Json - import skuber.LabelSelector.dsl._ import skuber._ import skuber.json.apps.format._ diff --git a/client/src/test/scala/skuber/apps/v1/DeploymentSpec.scala b/client/src/test/scala/skuber/apps/v1/DeploymentSpec.scala new file mode 100644 index 00000000..ff989a44 --- /dev/null +++ b/client/src/test/scala/skuber/apps/v1/DeploymentSpec.scala @@ -0,0 +1,104 @@ +package skuber.apps.v1 + +import org.specs2.mutable.Specification +import play.api.libs.json.Json +import skuber.LabelSelector.dsl._ +import skuber._ + +/** + * @author David O'Riordan + */ +class DeploymentSpec extends Specification { + "This is a unit specification for the skuber apps/v1 Deployment class. ".txt + + "A Deployment object can be constructed from a name and pod template spec" >> { + val container=Container(name="example",image="example") + val template=Pod.Template.Spec.named("example").addContainer(container) + val deployment=Deployment("example") + .withReplicas(200) + .withTemplate(template) + deployment.spec.get.template mustEqual template + deployment.spec.get.replicas mustEqual Some(200) + deployment.name mustEqual "example" + deployment.status mustEqual None + } + + + "A Deployment object can be written to Json and then read back again successfully" >> { + val container=Container(name="example",image="example") + val template=Pod.Template.Spec.named("example").addContainer(container) + val deployment=Deployment("example") + .withTemplate(template) + .withLabelSelector(LabelSelector("live" doesNotExist, "microservice", "tier" is "cache", "env" isNotIn List("dev", "test"))) + + + val readDepl = Json.fromJson[Deployment](Json.toJson(deployment)).get + readDepl mustEqual deployment + } + + "A Deployment object properly writes with zero replicas" >> { + val deployment=Deployment("example").withReplicas(0) + + val writeDepl = Json.toJson(deployment) + (writeDepl \ "spec" \ "replicas").asOpt[Int] must beSome(0) + } + + "A Deployment object can be read directly from a JSON string" >> { + val deplJsonStr = """ +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "nginx-deployment" + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels" : { + "domain": "www.example.com", + "proxies": "microservices" + }, + "matchExpressions": [ + {"key": "env", "operator": "NotIn", "values": ["dev"]}, + {"key": "tier","operator": "In", "values": ["frontend"]} + ] + }, + "strategy": { + "rollingUpdate": { + "maxUnavailable": 1 + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.7.9", + "ports": [ + { + "containerPort": 80 + } + ] + } + ] + } + } + } +} +""" + val depl = Json.parse(deplJsonStr).as[Deployment] + depl.kind mustEqual "Deployment" + depl.name mustEqual "nginx-deployment" + depl.spec.get.replicas mustEqual Some(3) + depl.spec.get.template.metadata.labels mustEqual Map("app" -> "nginx") + depl.spec.get.template.spec.get.containers.length mustEqual 1 + depl.spec.get.selector.requirements.size mustEqual 4 + depl.spec.get.selector.requirements.find(r => (r.key == "env")) mustEqual Some("env" isNotIn List("dev")) + depl.spec.get.selector.requirements.find(r => (r.key == "domain")) mustEqual Some("domain" is "www.example.com") + } +} \ No newline at end of file diff --git a/client/src/test/scala/skuber/apps/v1/StatefulSetSpec.scala b/client/src/test/scala/skuber/apps/v1/StatefulSetSpec.scala new file mode 100644 index 00000000..2b8a0bd0 --- /dev/null +++ b/client/src/test/scala/skuber/apps/v1/StatefulSetSpec.scala @@ -0,0 +1,77 @@ +package skuber.apps.v1 + +import org.specs2.mutable.Specification +import play.api.libs.json._ +import skuber.LabelSelector.dsl._ +import skuber._ + +class StatefulSetSpec extends Specification { + "This is a unit specification for the skuber apps/v1 StatefulSet class. ".txt + + "A StatefulSet object can be constructed from a name and pod template spec" >> { + val container=Container(name="example",image="example") + val template=Pod.Template.Spec.named("example").addContainer(container) + val stateSet=StatefulSet("example") + .withReplicas(200) + .withServiceName("nginx-service") + .withTemplate(template) + .withVolumeClaimTemplate(PersistentVolumeClaim("hello")) + stateSet.spec.get.template mustEqual template + stateSet.spec.get.serviceName mustEqual Some("nginx-service") + stateSet.spec.get.replicas must beSome(200) + stateSet.spec.get.volumeClaimTemplates.size mustEqual 1 + stateSet.name mustEqual "example" + stateSet.status mustEqual None + } + + "A StatefulSet object can be written to Json and then read back again successfully" >> { + val container=Container(name="example",image="example") + val template=Pod.Template.Spec.named("example").addContainer(container) + val stateSet=StatefulSet("example") + .withTemplate(template) + .withLabelSelector(LabelSelector("live" doesNotExist, "microservice", "tier" is "cache", "env" isNotIn List("dev", "test"))) + + + val readSSet = Json.fromJson[StatefulSet](Json.toJson(stateSet)).get + readSSet mustEqual stateSet + } + + "A StatefulSet object properly writes with zero replicas" >> { + val sset=StatefulSet("example").withReplicas(0) + + val writeSSet = Json.toJson(sset) + (writeSSet \ "spec" \ "replicas").asOpt[Int] must beSome(0) + } + + "A StatefulSet object can be read directly from a JSON string" >> { + import scala.io.Source + val ssJsonSource=Source.fromURL(getClass.getResource("/exampleStatefulSet.json")) + val ssetJsonStr = ssJsonSource.mkString + val stateSet = Json.parse(ssetJsonStr).as[StatefulSet] + + stateSet.kind mustEqual "StatefulSet" + stateSet.name mustEqual "nginx-stateset" + stateSet.spec.get.replicas must beSome(7) + stateSet.spec.get.updateStrategy.get.`type` mustEqual StatefulSet.UpdateStrategyType.RollingUpdate + stateSet.spec.get.updateStrategy.get.rollingUpdate.get.partition mustEqual(5) + stateSet.spec.get.volumeClaimTemplates.size mustEqual 1 + stateSet.spec.get.serviceName.get mustEqual "nginx-service" + stateSet.spec.get.template.metadata.labels mustEqual Map("domain" -> "www.example.com","proxies" -> "microservices") + val podSpec=stateSet.spec.get.template.spec.get + podSpec.containers.length mustEqual 1 + val container=podSpec.containers(0) + container.resources.get.requests.get("cpu").get mustEqual Resource.Quantity("500m") + container.lifecycle.get.preStop.get mustEqual ExecAction(List("/bin/sh", "-c", "PID=$(pidof java) && kill $PID && while ps -p $PID > /dev/null; do sleep 1; done")) + container.readinessProbe.get.action mustEqual ExecAction(List("/bin/sh", "-c", "./ready.sh")) + container.readinessProbe.get.initialDelaySeconds mustEqual 15 + container.readinessProbe.get.timeoutSeconds mustEqual 5 + stateSet.spec.get.selector.get.requirements.size mustEqual 4 + stateSet.spec.get.selector.get.requirements.find(r => (r.key == "env")) mustEqual Some("env" isNotIn List("dev")) + stateSet.spec.get.selector.get.requirements.find(r => (r.key == "domain")) mustEqual Some("domain" is "www.example.com") + + // write and read back in again, should be unchanged + val json = Json.toJson(stateSet) + val readSS = Json.fromJson[StatefulSet](json).get + readSS mustEqual stateSet + } +} diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 0204b623..695c5e41 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -8,25 +8,37 @@ The Skuber data model is a representation of the Kubernetes types / kinds in Sca The Skuber data model for the the original core Kubernetes API group (which manages many of the most fundamental Kubernetes kinds) is defined in the top-level package, so they can be easily imported into your application: - import skuber._ +```scala +import skuber._ +``` This also imports many other common types and aliases that are generally useful. Example of more specific core API kind imports: - - import skuber.{Service,ServiceList,ReplicationController} + +```scala +import skuber.{Service,ServiceList,Pod} +``` Newer (non-core) API group classes are contained in subpackages associated with each API group. For example`skuber.ext` for the extensions API group or `skuber.rbac` for the rbac API group. Example specific imports for these kinds: - import skuber.ext.{DaemonSet,Ingress} - import skuber.batch.{Job,CronJob} +```scala +import skuber.ext.DaemonSet +import skuber.batch.{Job,CronJob} +``` + +In the specific case of the `apps` group, which includes Workload types such as `Deployment` and `StatefulSet`, there are subpackages for each version of the group, with `v1` being the latest: + +```scala +import skuber.apps.v1.Deployment +``` The model can be divided into categories which correspond to those in the Kubernetes API: - [Object kinds](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#objects): These represent persistent entities in Kubernetes. All object kinds are mapped to case classes that extend the `ObjectResource` abstract class. The `ObjectResource` class defines the common fields, notably `metadata` (such as name, namespace, uid, labels etc.). The concrete classes extending ObjectResource typically define [spec and status](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status) nested fields whose classes are defined in the companion object (e.g. `Pod.Spec`, `ReplicationController.Status`). -Object kind classes include `Namespace`, `Pod`,`Node`, `Service`, `Endpoints`, `Event`, `ReplicationController`, `PersistentVolume`, `PersistentVolumeClaim`, `ServiceAccount`, `LimitRange`, `Resource.Quota`, `Secret`,`Deployment`,`HorizontalPodAutoScaler`,and `Ingress` amongst others. - [List kinds](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#lists-and-simple-kinds): These represent lists of object resources, and in skuber are typically returned by one of the `list` API methods. All list kinds are mapped to a `ListResource[O]` case class supporting access to basic metadata and the object kind specific items in the list. + There are thus list kinds for each object kind e.g. `ListResource[Pod]`,`ListResource[Node]`, and skuber also defines type aliases defined for each supported list kind e.g. `PodList`,`NodeList`. - [Simple kinds](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#lists-and-simple-kinds) @@ -35,56 +47,64 @@ There are thus list kinds for each object kind e.g. `ListResource[Pod]`,`ListRes A combination of generic Scala case class features and Skuber-defined fluent API methods make building out even relatively complex specifications for creation or modification on Kubernetes straightforward. The following (which can be found under the examples project) illustrates just a small part of the API: - val prodLabel = "env" -> "production" - val prodInternalZoneLabel = "zone" -> "prod-internal" +```scala +val prodLabel = "env" -> "production" - val prodInternalSelector = Map(prodLabel, prodInternalZoneLabel) +val prodInternalSelector = Map(prodLabel, prodInternalZoneLabel) - val prodCPU = 1 // 1 KCU - val prodMem = "0.5Gi" // 0.5GiB (gibibytes) +val prodCPU = 1 // 1 KCU +val prodMem = "0.5Gi" // 0.5GiB (gibibytes) - val prodContainer=Container(name="nginx-prod", image="nginx"). - limitCPU(prodCPU). - limitMemory(prodMem). - exposePort(80) +val prodContainer=Container(name="nginx-prod", image="nginx") + .limitCPU(prodCPU) + .limitMemory(prodMem) + .exposePort(80) - val internalProdPodSpec=Pod.Spec(containers=List(prodContainer), - nodeSelector=Map(prodInternalZoneLabel)) - - val internalProdController=ReplicationController("nginx-prod-int"). - addLabels(prodInternalSelector). - withSelector(prodInternalSelector). - withReplicas(8). - withPodSpec(internalProdPodSpec) +val internalProdTemplate = Pod.Template.Spec + .named("nginx-prod-internal") + .addContainer(prodContainer) + .addLabels(prodInternalSelector) + +val internalProdDeployment = Deployment("nginx-prod-int") + .withSelector(prodInternalSelector) + .withReplicas(8) + .withTemplate(internalProdTemplate) +``` The unit tests in the skuber subproject contains more examples, along with the examples subproject itself. ## JSON Mapping Kubernetes defines specific JSON representations of its types. Skuber implements Play JSON read/write [converters](https://www.playframework.com/documentation/2.4.x/ScalaJsonCombinators) for mapping between the model classes and their JSON representations. These implicit converters (formatters) can be made available to your application via import statements, for example, to import all formatters for the core API group: - - import skuber.json.format._ -Similiarly, subpackages of `skuber.json` contain formatters for non-core API group kinds such as `Deployment` etc. +```scala +import skuber.json.format._ +``` -There are many available examples of JSON representations of Kubernetes objects, for example [this file](https://github.com/kubernetes/kubernetes/blob/master/examples/guestbook-go/guestbook-controller.json) specifies a replication controller for the main Kubernetes project Guestbook example. To convert that JSON representation into a Skuber `ReplicationController` object: +Similiarly, subpackages of `skuber.json` contain formatters for non-core API groups such as `rbac` etc. - import skuber.json.format._ - import skuber.ReplicationController +Some of the more recently added subpackages in skuber - for example `apps/v1` - include the Json formatters in the companion objects of the model case classes so there is no need for these types to explicitly import their formatters. - import play.api.libs.json.Json - import scala.io.Source +There are many available examples of JSON representations of Kubernetes objects, for example [this file](https://github.com/kubernetes/examples/blob/master/guestbook/frontend-deployment.yaml) specifies a Deployment for the main Kubernetes project Guestbook example. To convert that JSON representation into a Skuber `Deployment` object: - val controllerURL = "https://raw.githubusercontent.com/kubernetes/kubernetes/master/examples/guestbook-go/guestbook-controller.json" - val controllerJson = Source.fromURL(controllerURL).mkString - val controller = Json.parse(controllerJson).as[ReplicationController] - println("Name: " + controller.name) - println("Replicas: " + controller.replicas) +```scala +import skuber.apps.v1.Deployment -Equally it is straightforward to do the reverse and generate a JSON value from a Skuber model object: +import play.api.libs.json.Json +import scala.io.Source - val json = Json.toJson(controller) +val deploymentURL = "https://raw.githubusercontent.com/kubernetes/examples/master/guestbook/frontend-deployment.yaml" +val deploymentJson = Source.fromURL(deploymentURL).mkString +val deployment = Json.parse(deploymentJson).as[Deployment] +println("Name: " + deployment.name) +println("Replicas: " + deployment.spec.flatMap(_.replicas).getOrElse(1)) +``` +Equally it is straightforward to do the reverse and generate a JSON value from a Skuber model object: + +```scala + val json = Json.toJson(deployment) +``` ## API ### The API basics @@ -92,191 +112,208 @@ Equally it is straightforward to do the reverse and generate a JSON value from a These are the basic steps to use the Skuber API: - Import the API definitions from the appropriate package(s) -- Import the implicit JSON formatters from the appropriate package(s). The API uses these to read/write the request and response data. +- Import the implicit JSON formatters from the appropriate package(s) as described above. The API uses these to read/write the request and response data. - Declare some additional Akka implicit values as shown below (this is basically to configure the Akka HTTP client which Skuber v2 uses under the hood) - Create a request context by calling `k8sInit` - this establishes the connection and namespace details for requests to the API - Invoke the required requests using the context. - The requests generally return their results (usually object or list kinds) asynchronously via `Future`s. -For example, the following creates the Replication Controller we just parsed above on our Kubernetes cluster: - - import skuber._ - import skuber.json.format._ +For example, the following creates a pod on our Kubernetes cluster: +```scala +import skuber._ +import skuber.json.format._ - import akka.actor.ActorSystem - import akka.stream.ActorMaterializer - implicit val system = ActorSystem() - implicit val materializer = ActorMaterializer() - implicit val dispatcher = system.dispatcher +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +implicit val system = ActorSystem() +implicit val materializer = ActorMaterializer() +implicit val dispatcher = system.dispatcher - val k8s = k8sInit - - k8s create controller +val k8s = k8sInit +val pod: Pod = ??? // read a Pod definition from some file or other source +k8s create pod +``` When finished making requests the application should call `close` on the request context. Note that this call no longer closes connection resources since Skuber migrated to using Akka, because the use of application-supplied implicit Akka actor systems means Skuber cannot be sure that other application components are not also using the same actor system. Therefore the application should explicitly perform any required Akka cleanup, e.g. - - k8s.close - system.terminate - + +```scala +k8s.close +system.terminate +``` ### API Method Summary Create a resource on Kubernetes from a Skuber object kind: - - val rcFut = k8s create controller - rcFut onSuccess { case rc => - println("Created controller, Kubernetes assigned resource version is " rc.metadata.resourceVersion) - } +```scala +val rcFut = k8s create controller +rcFut onSuccess { case rc => + println("Created controller, Kubernetes assigned resource version is " rc.metadata.resourceVersion) +} +``` Get a Kubernetes object kind resource by type and name: - - val rcFut = k8s get[ReplicationController] "guestbook" - rcFut onSuccess { case rc => println("Current replica count = " + rc.status.get.replicas) } +```scala +val depFut = k8s get[Deployment] "guestbook" +depFut onSuccess { case dep => println("Current replica count = " + dep.status.get.replicas) } +``` Get a list of all Kubernetes objects of a given list kind in the current namespace: - - val rcListFut = k8s list[ReplicationControllerList]() - rcListFut onSuccess { case rcList => rcList foreach { rc => println(rc.name) } } +```scala +val depListFut = k8s list[DeploymentList]() +depListFut onSuccess { case depList => depList foreach { dep => println(dep.name) } } +``` As above, but for a specified namespace: - - val ksysPods: Future[PodList] = k8s listInNamespace[PodList]("kube-system") +```scala +val ksysPods: Future[PodList] = k8s listInNamespace[PodList]("kube-system") +``` Get lists of all Kubernetes objects of a given list kind for all namespaces in the cluster, mapped by namespace: - - val allPodsMapFut: Future[Map[String, PodList]] = k8s listByNamespace[PodList]() - +```scala +val allPodsMapFut: Future[Map[String, PodList]] = k8s listByNamespace[PodList]() +``` (See the ListExamples example for examples of the above list operations) Update a Kubernetes object kind resource: - - val upscaledController = controller.withReplicas(5) - val rcFut = k8s update upscaledController - rcFut onSuccess { case rc => - println("Updated controller, Kubernetes assigned resource version is " + rc.metadata.resourceVersion) - } +```scala +val upscaledDeployment = deployment.withReplicas(5) +val depFut = k8s update upscaledDeployment +depFut onSuccess { case dep => + println("Updated deployment, Kubernetes assigned resource version is " + dep.metadata.resourceVersion) +} +``` Delete a Kubernetes object: - - val rmFut = k8s delete[ReplicationController] "guestbook" - rmFut onSuccess { case _ => println("Controller removed") } - +```scala +val rmFut = k8s delete[Deployment] "guestbook" +rmFut onSuccess { case _ => println("Deployment removed") } +``` (There is also a `deleteWithOptions` call that enables options such as propagation policy to be passed with a Delete operation.) Patch a Kubernetes object using a [JSON merge patch](https://tools.ietf.org/html/rfc7386): - - val patchStr="""{ "spec": { "replicas" : 1 } }""" - val stsFut = k8s.jsonMergePatch(myStatefulSet, patchStr) - -See also the `PatchExamples` example. - -Note: There is no patch support yet for the other two (`json patch` and `strategic merge patch`) [strategies](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#patch-operations) +```scala +val patchStr="""{ "spec": { "replicas" : 1 } }""" +val stsFut = k8s.jsonMergePatch(myStatefulSet, patchStr) +``` +See also the `PatchExamples` example. Note: There is no patch support yet for the other two (`json patch` and `strategic merge patch`) [strategies](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#patch-operations) + +Get the logs of a pod (as an Akka Streams Source): +```scala + val helloWorldLogsSource: Future[Source[ByteString, _]] = k8s.getPodLogSource("hello-world-pod", Pod.LogQueryParams()) +``` + +Directly scale the number of replicas of a deployment or stateful set: +```scala +k8s.scale[StatefulSet]("database", 5) +``` ### Error Handling Any call to the Skuber API methods that results in a non-OK status response from Kubernetes will cause the result of the Future returned by the method to be set to a `Failure` with an exception of class `K8SException`. This exception has a `status` field that encapsulates the data returned in the [Status](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#response-status-kind) object if Kubernetes has returned one, which it generally does when returning a non-OK status. This exception can be handled in the appropriate manner for your use case by using the standard Scala Future failure handling mechanisms. For example, sometimes you may want to ignore a NOT_FOUND (404) error when attempting to delete an object, because it is normal and ok if it has already been deleted: - - val deleteResult = (k8s delete[ReplicationController] c.name) recover { - case ex: K8SException if (ex.status.code.contains(404)) => // ok - no action required - } - deleteResult onFailure { - case ex: K8SException => - log.error("Error when deleting " + c.name + ", reason: " + ex.status.reason.getOrElse("")) - } - +```scala +val deleteResult = (k8s delete[ReplicationController] c.name) recover { + case ex: K8SException if (ex.status.code.contains(404)) => // ok - no action required +} +deleteResult onFailure { + case ex: K8SException => + log.error("Error when deleting " + c.name + ", reason: " + ex.status.reason.getOrElse("")) +} +``` The above code basically causes a 404 error to be silently ignored and the overall result will be a `Success`, other errors will still be propagated as `Failures` in `deleteResult`, which results in the error reason being logged. The `Status` class is defined as follows: - - case class Status( - // ... metadata fields here ... - // error details below: - status: Option[String] = None, - message: Option[String] = None, - reason: Option[String] = None, - details: Option[Any] = None, - code: Option[Int] = None // HTTP status code - ) +```scala +case class Status( + // ... metadata fields here ... + // error details below: + status: Option[String] = None, + message: Option[String] = None, + reason: Option[String] = None, + details: Option[Any] = None, + code: Option[Int] = None // HTTP status code +) +``` ### Reactive Watch API Kubernetes supports the ability for API clients to watch events on specified resources - as changes occur to the resource(s) on the cluster, Kubernetes sends details of the updates to the watching client. Skuber v2 now uses Akka streams for this (instead of Play iteratees as used in the Skuber v1.x releases), so the `watch[O]` API calls return `Future[Source[O]]` objects which can then be plugged into Akka flows. - - import skuber._ - import skuber.json.format._ - - import akka.actor.ActorSystem - import akka.stream.ActorMaterializer - import akka.stream.scaladsl.Sink +```scala +import skuber._ +import skuber.json.format._ +import skuber.apps.v1.Deployment + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import akka.stream.scaladsl.Sink - - object WatchExamples { - implicit val system = ActorSystem() - implicit val materializer = ActorMaterializer() - implicit val dispatcher = system.dispatcher - val k8s = k8sInit - - val frontendReplicaCountMonitor = Sink.foreach[K8SWatchEvent[ReplicationController]] { frontendEvent => - println("Current frontend replicas: " + frontendEvent._object.status.get.replicas) - } - for { - frontendRC <- k8s.get[ReplicationController]("frontend") - frontendRCWatch <- k8s.watch(frontendRC) - done <- frontendRCWatch.runWith(frontendReplicaCountMonitor) - } yield done - // ... - } - -The above example creates a Watch on the frontend replication controller, and feeds the resulting events into an Akka sink that simply prints out the replica count from the current version of the controller as included in each event. To test the above code, call the watchFrontendScaling method to create the watch and then separately run a number of [kubectl scale](https://kubernetes.io/docs/tutorials/kubernetes-basics/scale-interactive/) commands to set different replica counts on the frontend - for example: - - kubectl scale --replicas=1 rc frontend - - kubectl scale --replicas=10 rc frontend - - kubectl scale --replicas=0 rc frontend +object WatchExamples { + implicit val system = ActorSystem() + implicit val materializer = ActorMaterializer() + implicit val dispatcher = system.dispatcher + val k8s = k8sInit + + val frontendReplicaCountMonitor = Sink.foreach[K8SWatchEvent[Deployment]] { frontendEvent => + println("Current frontend replicas: " + frontendEvent._object.status.get.replicas) + } + for { + frontend <- k8s.get[Deployment]("frontend") + frontendWatch <- k8s.watch(frontend) + done <- frontendWatch.runWith(frontendReplicaCountMonitor) + } yield done + // ... +} +``` + +The above example creates a Watch on the frontend deployment, and feeds the resulting events into an Akka sink that simply prints out the replica count from the current version of the deployment as included in each event. To test the above code, call the watchFrontendScaling method to create the watch and then separately run a number of [kubectl scale](https://kubernetes.io/docs/tutorials/kubernetes-basics/scale-interactive/) commands to set different replica counts on the frontend - for example: +```bash +kubectl scale --replicas=1 deployment/frontend +kubectl scale --replicas=10 deployment/frontend +kubectl scale --replicas=0 deployment/frontend +``` You should see updated replica counts being printed out by the sink as the scaling progresses. The [reactive guestbook](../examples/src/main/scala/skuber/examples/guestbook) example also uses the watch API to support monitoring the progress of deployment steps by watching the status of replica counts. Additionally you can watch all events related to a specific kind - for example the following can be found in the same example: - - def watchPodPhases = { - // ... - - val podPhaseMonitor = Sink.foreach[K8SWatchEvent[Pod]] { podEvent => - val pod = podEvent._object - val phase = pod.status flatMap { _.phase } - println(podEvent._type + " => Pod '" + pod.name + "' .. phase = " + phase.getOrElse("")) - } - - for { - currPodList <- k8s.list[PodList]() - latestPodVersion = currPodList.metadata.map { _.resourceVersion } - currPodsWatch <- k8s.watchAll[Pod](sinceResourceVersion = latestPodVersion) // ignore historic events - done <- currPodsWatch.runWith(podPhaseMonitor) - } yield done - - // ... - } - +```scala +def watchPodPhases = { + // ... + + val podPhaseMonitor = Sink.foreach[K8SWatchEvent[Pod]] { podEvent => + val pod = podEvent._object + val phase = pod.status flatMap { _.phase } + println(podEvent._type + " => Pod '" + pod.name + "' .. phase = " + phase.getOrElse("")) + } + + for { + currPodList <- k8s.list[PodList]() + latestPodVersion = currPodList.metadata.map { _.resourceVersion } + currPodsWatch <- k8s.watchAll[Pod](sinceResourceVersion = latestPodVersion) // ignore historic events + done <- currPodsWatch.runWith(podPhaseMonitor) + } yield done + // ... +} +``` The watch can be demonstrated by calling `watchPodPhases` to start watching all pods, then in the background run the reactive guestbook example: you should see events being reported as guestbook pods are deleted, created and modified during the run. Note that both of the examples above watch only those events which have a later resource version than the latest applicable when the watch was created - this ensures that only current events are sent to the watch, historic ones are ignored. This is often what you want, but sometimes - especially where events are being used to update important state in your application - you want to make sure you don't miss any events, even in the case where your watch has been stopped and restarted. In this case you can keep a record of the latest resource version processed in a database of some sort and then if/when the watch gets restarted you can specify that resource version in the API call to start the watch: - - k8s.watch[Pod]("myPod", sinceResourceVersion=lastProcessedResourceVersion) +```scala +k8s.watch[Pod]("myPod", sinceResourceVersion=lastProcessedResourceVersion) +``` ### Extensions API Group -Along with the core API group, the extensions API group is probably the most commonly used as it traditionally contains some key types. +The extensions API group traditionally contains some key types. Although in more recent versions of Kubernetes many of these have been migrated to other groups, this group is still supported and widely used. For example, to use the `HorizontalPodAutoscaler` kind: +```scala +import skuber.ext.HorizontalPodAutoscaler +import skuber.json.ext.format._ // imports the implicit JSON formatters required to use extensions group resources +``` - import skuber.ext.HorizontalPodAutoscaler - import skuber.json.ext.format._ // imports the implicit JSON formatters required to use extensions group resources - -The currently supported extensions group kinds include `Deployment`,`ReplicaSet`,`HorizaontalPodAutoscaler`, `Ingress`, `DaemonSet`, together with their list kinds. +The currently supported extensions group kinds include `Deployment`,`ReplicaSet`,`HorizontalPodAutoscaler`, `Ingress`, `DaemonSet`, together with their list kinds. ***Deployment*** @@ -285,53 +322,54 @@ A Skuber client can create and update `Deployment` objects on the cluster to hav The following example emulates that described [here](http://kubernetes.io/docs/user-guide/deployments/). Initial creation of the deployment: - - val nginxLabel = "app" -> "nginx" - val nginxContainer = Container("nginx",image="nginx:1.7.9").exposePort(80) +```scala +val nginxLabel = "app" -> "nginx" +val nginxContainer = Container("nginx",image="nginx:1.7.9").exposePort(80) - val nginxTemplate = Pod.Template.Spec - .named("nginx") - .addContainer(nginxContainer) - .addLabel(nginxLabel) +val nginxTemplate = Pod.Template.Spec + .named("nginx") + .addContainer(nginxContainer) + .addLabel(nginxLabel) - val desiredCount = 5 - val nginxDeployment = Deployment("nginx-deployment") - .withReplicas(desiredCount) - .withTemplate(nginxTemplate) +val desiredCount = 5 +val nginxDeployment = Deployment("nginx-deployment") + .withReplicas(desiredCount) + .withTemplate(nginxTemplate) - println("Creating nginx deployment") - val createdDeplFut = k8s create nginxDeployment +println("Creating nginx deployment") +val createdDeplFut = k8s create nginxDeployment +``` Use `kubectl get deployments` to see the status of the newly created Deployment, and `kubectl get rc` will show a new replication controller which manages the creation of the required pods. Later an update can be posted - in this example the nginx version will be updated to 1.9.1: - - val newContainer = Container("nginx",image="nginx:1.9.1").exposePort(80) - val existingDeployment = k8s get[Deployment] "nginx-deployment" - val updatedDeployment = existingDeployment.updateContainer(newContainer) - k8s update updatedDeployment +```scala +val newContainer = Container("nginx",image="nginx:1.9.1").exposePort(80) +val existingDeployment = k8s get[Deployment] "nginx-deployment" +val updatedDeployment = existingDeployment.updateContainer(newContainer) +k8s update updatedDeployment +``` As no explicit deployment strategy has been selected, the default strategy will be used which will result in a rolling update of the nginx pods - again, you can use `kubectl get` commands to view the status of the deployment, replication controllers and pods as the update progresses. The `DeploymentExamples` example runs the above steps. -If you need to support versions of Kubernetes before v1.6 then continue to use `ext.Deployment`, otherwise use `apps.Deployment` (see below) which is the strategic long-term replacement for `ext.Deployment`. +If you need to support versions of Kubernetes before v1.6 then continue to use `ext.Deployment`, otherwise use `skuber.apps..Deployment` (see below) - which version to use depends on your Kubernetes version but for version 1.9 of Kubernetes (or later) use `skuber.apps.v1.Deployment`. As the Kubernetes long-term strategy is to use more specific API groups rather then the generic extensions group, other classes in the `ext` subpackage are also likely to be migrated in future to reflect changes in Kubernetes. ***HorizontalPodAutoscaler*** A skuber client can also manage `HorizontalPodAutoscaler` objects in order to autoscale a replication controller or deployment. A fluent API approach enables minimum replica count, maximum replica count and CPU utilisation target to be readily specified. For example: - - // following autoscales 'controller' with min replicas of 2, max replicas of 8 - // and a target CPU utilisation of 80% - val hpas = HorizontalPodAutoscaler.scale(controller). - withMinReplicas(2). - withMaxReplicas(8). - withCPUTargetUtilization(80) - k8s create[HorizontalPodAutoscaler] hpas - -The other standard Skuber API methods (`update`, `delete` etc.) can also be used with this type. (Note: the corresponding *list type* will be supported shortly) +```scala +// following autoscales 'controller' with min replicas of 2, max replicas of 8 +// and a target CPU utilisation of 80% +val hpas = HorizontalPodAutoscaler.scale(controller) + .withMinReplicas(2) + .withMaxReplicas(8) + .withCPUTargetUtilization(80) +k8s create[HorizontalPodAutoscaler] hpas +``` ***Ingress*** @@ -341,9 +379,9 @@ The `NginxIngress` example illustrates creation and testing of an ingress, using ***ReplicaSet*** -ReplicaSet is the expected long-term successor of ReplicationController in the Kubernetes project. It is currently different only in supporting both equality and set based label selectors (ReplicationController only support equality-based ones). +ReplicaSet is the strategic successor of ReplicationController in the Kubernetes project. It is currently different only in supporting both equality and set based label selectors (ReplicationController only support equality-based ones). -The `NginxIngress` example uses a ReplicaSet to manage the ingress controller. +ReplicaSet is most commonly used implicitly with Deployment types, but can be used explicitly as well - the `NginxIngress` example explicitly uses a ReplicaSet to manage the ingress controller. ### Other API groups @@ -351,15 +389,9 @@ Aside from the `core` and `extensions` groups, more recent Kubernetes kinds tend ***apps*** -Currently contains the `Deployment` and `StatefulSet` types: +The `apps` package supports recent versions of Workload types - use the `ext` package instead if you are on an older Kubernetes version that doesn't support the `apps` group. -- Deployment - -Essentially equivalent to `ext.Deployment` at present, but requires Kubernetes clusters that are at v1.6 or later. Long-term strategic replacement for `ext.Deployment`. - -- StatefulSet - -This can be used to manage stateful applications on Kubernetes - such as databases - analagous to the way in which `ReplicaSet` is used to manage stateless applications. +The `apps` package contains subpackages for each supported version of the `apps` group: `v1beta1`,`v1beta2` and `v1`. Each subpackage contains at least `Deployment` and `StatefulSet`, while the `v1` (GA) version also contains `DaemonSet` and `ReplicaSet`. ***batch*** @@ -383,19 +415,21 @@ Supports `NetworkPolicy` resources (for Kubernetes v1.7 and above) - see Kuberne ## Label Selectors As alluded to above, newer API types such as ReplicaSets and Deployments support set-based as well as equality-based [label selectors](http://kubernetes.io/docs/user-guide/labels/#label-selectors). -For such types, Skuber now supports a mini-DSL to build selectors: - - import skuber._ - import skuber.LabelSelector.dsl._ - - val sel = LabelSelector( - "tier" is "frontend", - "release" doesNotExist, - "env" isNotIn List("production", "staging")) +For such types, Skuber supports a mini-DSL to build selectors: +```scala +import skuber.LabelSelector +import LabelSelector.dsl._ +import skuber.apps.v1.Deployment + +val sel = LabelSelector( + "tier" is "frontend", + "release" doesNotExist, + "env" isNotIn List("production", "staging") +) - // now the label selector can be used with certain types - val depl = Deployment("exampleDeployment").withSelector(sel) - +// now the label selector can be used with certain types +val depl = Deployment("exampleDeployment").withSelector(sel) +``` ## Programmatic configuration @@ -405,4 +439,4 @@ The configuration object has the same information as a kubeconfig file - in fact The unit tests have an example of a K8SConfiguration object being parsed from an input stream that contains the data in kubeconfig file format. -Additionally a Typesafe Config object can optionally be passed programmatically as a second parameter to the initialisation call - currently this only supports specifying your own Akka dispatcher (execution context for the Akka http client request processingi by Skuber) +Additionally a Typesafe Config object can optionally be passed programmatically as a second parameter to the initialisation call - currently this only supports specifying your own Akka dispatcher (execution context for the Akka http client request processing by Skuber)