app/services/kubernetes.scala (85 lines of code) (raw):

package services import akka.actor.ActorSystem import akka.stream.Materializer import models.errors.{ConflictError, GenericError, LightweightError} import models.{DeployedImageInfo, DockerImage} import org.slf4j.LoggerFactory import play.api.Configuration import skuber._ import skuber.apps.v1.{Deployment, DeploymentList} import java.io.File import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} import scala.io.Source @Singleton class kubernetes @Inject() (config:Configuration) (implicit system:ActorSystem, mat:Materializer) { implicit val ec:ExecutionContext = system.dispatcher private val logger = LoggerFactory.getLogger(getClass) val k8s = k8sInit val x = getInClusterNamespace .getOrElse(config.getOptional[String]("kubernetes.default-namespace")) /** * if we can't pick up the in-cluster namespace, fall back to a pre-configured one */ val namespace:String = (getInClusterNamespace, config.getOptional[String]("kubernetes.default-namespace")) match { case (Some(clusterNS),_) => clusterNS case (_, Some(configuredNS))=>configuredNS case _=> throw new RuntimeException("Namespace is not configured properly, either run from inside a kubernetes cluster or set the 'default-namespace' configuration item under 'kubernetes'.") } def getInClusterNamespace = { val internalFile = new File("/var/run/secrets/kubernetes.io/serviceaccount/namespace") if(internalFile.exists()) { val dataSource = Source.fromFile(internalFile, "UTF-8") val content = dataSource.getLines().reduce(_ + _) dataSource.close() Some(content) } else { None } } def getDeploymentInfo(deploymentName:String) = { (k8s getInNamespace[Deployment](deploymentName, namespace)).map(DeployedImageInfo.fromDeployment) } def getDeploymentMetadata(deploymentName:String) = { k8s getInNamespace[Deployment](deploymentName, namespace) } def listDeployments() = { (k8s listInNamespace[DeploymentList] namespace) .map(listContent=>listContent.items) } private def extractContainers(deployment:Deployment) = for { spec <- deployment.spec templateSpec <- spec.template.spec } yield (templateSpec.containers, templateSpec.initContainers) private def updateContainersList(source:List[Container], to:DockerImage) = source.map(c=>{ if(c.image.startsWith(to.imageName)) { c.copy(image = to.toString) } else { c } }) def performUpdate(deployment:Deployment, to:DockerImage):Future[Either[LightweightError, Deployment]] = extractContainers(deployment) match { case Some((containers, initContainers))=> val updatedContainers = updateContainersList(containers, to) val updatedInitContainers = updateContainersList(initContainers, to) if(updatedContainers==containers) { logger.error(s"Can't update ${deployment.metadata.name} to $to: no matching containers to update") Future(Left(ConflictError.fromDeployment(deployment, to))) } //if there were no updates to be made to init containers, then `updatedInitContainers`==`initContainers` so there is no change here. val updatedTemplateSpec = deployment.spec.flatMap(_.template.spec).map(_.copy(containers=updatedContainers, initContainers=updatedInitContainers)) val updatedTemplate = deployment.spec.map(_.template).map(_.copy(spec=updatedTemplateSpec)) val updatedDeloymentSpec = deployment.spec.map(_.copy(template = updatedTemplate.get)) val updatedDeployment = deployment.copy(spec=updatedDeloymentSpec) logger.debug(s"Updated deployment is $updatedDeployment") (k8s update[Deployment] updatedDeployment).map(Right.apply) case None=> Future(Left(GenericError("Deployment is misconfigured, there was nothing to update"))) } def updateDeployedSoftware(to:DockerImage, deploymentName:String) = { for { deployment <- k8s getInNamespace[Deployment](deploymentName, namespace) result <- performUpdate(deployment, to) } yield result } }