All Downloads are FREE. Search and download functionalities are using the official Maven repository.

blended.itestsupport.docker.ContainerManager.scala Maven / Gradle / Ivy

/*
 * Copyright 2014ff,  https://github.com/woq-blended
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package blended.itestsupport.docker

import akka.actor._
import akka.event.{LoggingAdapter, LoggingReceive}
import akka.pattern._
import akka.util.Timeout
import com.github.dockerjava.api.DockerClient
import com.github.dockerjava.api.model.Container
import com.typesafe.config.Config
import blended.itestsupport.{ContainerUnderTest, NamedContainerPort}
import blended.itestsupport.docker.protocol._

import scala.collection.convert.Wrappers.JListWrapper
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._

private[docker] case class InternalMapDockerContainers(requestor: ActorRef, cuts: Map[String, ContainerUnderTest], client: DockerClient)
private[docker] case class InternalDockerContainersMapped(requestor: ActorRef, result : DockerResult[Map[String, ContainerUnderTest]])
private[docker] case class InternalStartContainers(cuts : Map[String, ContainerUnderTest])
private[docker] case class InternalContainersStarted(result : DockerResult[Map[String, ContainerUnderTest]])

trait DockerClientProvider {
  def getClient : DockerClient
}

class ContainerManager extends Actor with ActorLogging with Docker with VolumeBaseDir { this:  DockerClientProvider =>

  implicit val timeout = Timeout(30.seconds)
  implicit val eCtxt   = context.dispatcher
  val client : DockerClient = getClient  

  override val config: Config = context.system.settings.config
  override val logger: LoggingAdapter = context.system.log
  
  def mapper = context.actorOf(Props(new DockerContainerMapper))
  
  def receive = LoggingReceive {
    
    case StartContainerManager(containers) => 
      
      val requestor = sender 
      
      val externalCt = config.getBoolean("docker.external")
      log.info(s"Containers have been started externally: [$externalCt]")

      val dockerHandler = context.actorOf(Props(new DockerContainerHandler()(client)), "DockerHandler")
      
      externalCt match {
        case true => self ! InternalContainersStarted(Right(configureDockerContainer(containers)))
        case _ => (dockerHandler ? InternalStartContainers(configureDockerContainer(containers))) pipeTo self
      }
      
      context.become(starting(dockerHandler, sender))
  }
  
  def starting(dockerHandler: ActorRef, requestor: ActorRef) : Receive = LoggingReceive {
    case r : InternalContainersStarted => r.result match {
      case Right(cuts) => mapper ! InternalMapDockerContainers(self, cuts, client)
      case _ => requestor ! r.result
    }  
    case r : InternalDockerContainersMapped => 
      log.info(s"Container Manager started with docker attached docker containers: [${r.result}]")
      requestor ! ContainerManagerStarted(r.result)
      r.result match {
        case Right(cuts) => context.become(running(dockerHandler, cuts))
        case _ => context.stop(self)
      }
  }
  
  def running(dockerHandler: ActorRef, cuts: Map[String, ContainerUnderTest]) : Receive = LoggingReceive {
    case scm : StopContainerManager =>
      log.info("Stopping Test Container Manager")
      dockerHandler.forward(scm)
  }

  private[this] def configureDockerContainer(cut : Map[String, ContainerUnderTest]) : Map[String, ContainerUnderTest] = {
    val cuts = cut.values.map { ct =>
      search(searchByTag(ct.imgPattern)).zipWithIndex.map { case (img, idx) =>
        val ctName = s"${ct.ctName}_$idx"
        ct.copy(ctName = ctName, dockerName = s"${ctName}_${System.currentTimeMillis}", imgId = img.getId)
      }
    }.flatten
    
    cuts.map { ct => (ct.ctName, ct) }.toMap
  }
}

class DockerContainerMapper extends Actor with ActorLogging {
   
  def receive = LoggingReceive {
    case InternalMapDockerContainers(requestor, cuts, client) => 
      log.debug(s"Mapping docker containers $cuts")
      
      val mapped : Map[String, Either[Throwable, ContainerUnderTest]] = cuts.map { case (name, cut) =>
        dockerContainer(cut, client) match {
          case e if e.isEmpty => (name, Left(new Exception(s"No suitable docker container found for [${cut.ctName}]")))
          case head :: rest if rest.isEmpty => (name, Right(mapDockerContainer(head, cut)))
          case _ => (name, Left(new Exception(s"No unique docker container found for [${cut.ctName}]")))
        }
      }
      
      val errors = mapped.values.filter(_.isLeft).map(_.left.get.getMessage)
      val mappedCuts = mapped.values.filter(_.isRight).map(_.right.get)
      
      val result = errors match {
        case e if e.isEmpty => InternalDockerContainersMapped(requestor, Right(mappedCuts.map { c => (c.ctName, c) }.toMap ))
        case l => InternalDockerContainersMapped(requestor, Left(new Exception(errors.mkString(","))))
      }
      
      log.debug(s"$result")
      sender ! result
  }
  
  private[docker] def mapDockerContainer(dc: Container, cut: ContainerUnderTest) : ContainerUnderTest = {
    val mapped = cut.ports.map { case (name, port) => 
      (name, mapPort(dc.getPorts, port)) 
    }
    
    cut.copy(dockerName = rootName(dc), ports = mapped)    
  }
  
  private[docker] def rootName(dc : Container) : String = 
    dc.getNames.filter { _.indexOf("/", 1) == -1 }.head.substring(1)
  
    
  private[docker] def mapPort(dockerPorts: Array[Container.Port], port: NamedContainerPort) : NamedContainerPort = {
    dockerPorts.filter { _.getPrivatePort() == port.privatePort }.toList match {
      case e if e.isEmpty => port
      case l => port.copy(publicPort = l.head.getPublicPort)
    }
  }
  
  private[docker] def dockerContainer(cut: ContainerUnderTest, client: DockerClient) : List[Container] = {
    
    val dc = JListWrapper(client.listContainersCmd().exec()).toList
    
    dockerContainerByName(cut, dc) match {
      case e if e.isEmpty => dockerContainerByImage(cut, dc)
      case l => l
    }
  } 
  
  private[docker] def dockerContainerByName(cut: ContainerUnderTest, dc: List[Container]) : List[Container] = {
    log.debug(s"Matching Docker Container by name: [${cut.dockerName}]")
    dc.filter(_.getNames.contains(s"/${cut.dockerName}"))
  }

  private[docker] def dockerContainerByImage(cut: ContainerUnderTest, dc: List[Container]) : List[Container] = {
    log.debug(s"Matching Docker Container by Image: [${cut.imgPattern}]")
    dc.filter(_.getImage.matches(cut.imgPattern))
  }
}

class DockerContainerHandler(implicit client: DockerClient) extends Actor with ActorLogging {
  
  def receive = LoggingReceive {
    case InternalStartContainers(cuts) =>
      log.info(s"Starting docker containers [$cuts]")
      
      val noDeps   = cuts.values.filter( _.links.isEmpty ).toList
      val withDeps = cuts.values.filter( _.links.nonEmpty).toList
      
      val pending  = withDeps.map { cut =>
        ( context.actorOf(Props(DependentContainerActor(cut))), cut )
      }

      noDeps.foreach{ startContainer }

      context.become(starting(sender, pending, noDeps, List.empty))
    case scm : StopContainerManager =>
      sender ! ContainerManagerStopped
      context.stop(self)
  }
  
  def starting(
    requestor           : ActorRef,
    pendingContainers   : List[(ActorRef, ContainerUnderTest)],
    startingContainers  : List[ContainerUnderTest],
    runningContainers   : List[ContainerUnderTest]
  ) : Receive = LoggingReceive {
    case ContainerStarted(result) => result match {
      case Right(cut) =>
        pendingContainers.foreach { _._1 ! ContainerStarted(Right(cut)) }
        val remaining = startingContainers.filter(_ != cut)
        val started = cut :: runningContainers

        if (pendingContainers.isEmpty && remaining.isEmpty) {
          log.info(s"Container Manager started [$started]")
          context.become(running(started))
          requestor ! InternalContainersStarted(Right(started.map { ct => (ct.ctName, ct) }.toMap ))
        } else {
          context.become(starting(requestor, pendingContainers, remaining, started))
        }
      case Left(e) => 
        log error s"Error in starting docker containers [${e.getMessage}]"
        requestor ! Left(e)
        context.stop(self)
    }
    case DependenciesStarted(result) => result match {
      case Right(cut) =>
        val pending  = pendingContainers.filter(_._1 != sender())
        startContainer(cut)
        context.become(starting(requestor, pending, cut :: startingContainers, runningContainers))
      case Left(e) => 
        context.stop(self)
    }
  }

  def running(managedContainers: List[ContainerUnderTest]) : Receive = LoggingReceive { 
    case scm : StopContainerManager =>
      
      log.info(s"Stopping Docker Container handler [$managedContainers]")
      
      implicit val timeout = new Timeout(scm.timeout)
      implicit val eCtxt = context.system.dispatcher
      val requestor = sender

      val stopFutures : Seq[Future[ContainerStopped]] = managedContainers.map { cut => 
        for{
          actor <- context.actorSelection(cut.ctName).resolveOne()
          stopped <- (actor ? StopContainer).mapTo[ContainerStopped]
        } yield stopped
      }
      
      val r = Await.result(Future.sequence(stopFutures), scm.timeout) 
      log.debug(s"Stopped Containers [$r]")
      requestor ! ContainerManagerStopped
      
      context.stop(self)
  }

  private[this] def startContainer(cut : ContainerUnderTest) : ActorRef = {

    val actor = context.actorOf(Props(ContainerActor(cut)), cut.ctName)
    actor ! StartContainer(cut.ctName)
    
    log.debug(s"Container Actor is [$actor]")
    
    actor
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy