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

lson.core_2.11.0.9.71.source-code.Templates.scala Maven / Gradle / Ivy

The newest version!
//: ----------------------------------------------------------------------------
//: Copyright (C) 2017 Verizon.  All Rights Reserved.
//:
//:   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 nelson

import java.nio.file.{Files, Path}
import java.util.concurrent.TimeoutException
import journal._
import scala.sys.process.{Process => _, _}
import scala.concurrent.duration._
import scalaz.{\/-, -\/}
import scalaz.Kleisli
import scalaz.concurrent.Task
import scalaz.stream.Process
import scalaz.stream.time
import scalaz.syntax.monad._

import Datacenter.StackName
import Nelson.NelsonK
import Metrics.default.{consulTemplateContainerCleanupFailuresTotal, consulTemplateContainersRunning, consulTemplateRunsDurationSeconds, consulTemplateRunsFailuresTotal}

object Templates {
  import java.util.concurrent.ScheduledExecutorService

  private[this] val logger = Logger[this.type]

  /** The result of a consul-template validation */
  sealed abstract class ConsulTemplateResult(val isValid: Boolean)
      extends Product with Serializable

  /** The consul-template rendered correctly. */
  case object Rendered extends ConsulTemplateResult(true)

  /** The consul-template had an error */
  final case class InvalidTemplate(msg: String) extends ConsulTemplateResult(false)

  /** The consul-template job timed out. It might be okay, but we gave up trying. */
  final case class TemplateTimeout(msg: String) extends ConsulTemplateResult(false)

  /** We couldn't call consul-template */
  final case class ConsulTemplateError(exitCode: Int, msg: String)
      extends RuntimeException(s"consul-template exited with code $exitCode: $msg")

  /**
   * A template that we want to render in Nelson
   *
   * @param unitRef the name of the unit that owns the template
   * @param resources a set of resources the unit depends on
   * @param template the content of the template
   */
  final case class TemplateValidation(
    unitRef: UnitRef,
    resources: Set[String],
    template: String
  )

  /** Validate a template according to a template validation request */
  def validateTemplate(tv: TemplateValidation): NelsonK[ConsulTemplateResult] =
    Kleisli.kleisli { cfg =>
      val dc = cfg.datacenters.headOption.getOrElse {
        // We should never see this. Nelson doesn't start without a datacenter.
        sys.error("Can't validate a template without a datacenter")
      }

      val vault = dc.interpreters.vault

      val dcName = dc.name
      val dnsRoot = dc.domain.name

      val ns = cfg.defaultNamespace
      val sn = StackName(tv.unitRef, Version(0, 0, 0), s"test${randomAlphaNumeric(8)}")

      val env = Map(
        "NELSON_DATACENTER" -> dcName,
        "NELSON_DNS_ROOT" -> dnsRoot,
        "NELSON_ENV" -> ns.root.asString,
        "NELSON_PLAN" -> "default",
        "NELSON_STACKNAME" -> sn.toString
      )

      val templateConfig = cfg.template

      policies.withPolicy(dc.policy, sn, ns, tv.resources, vault) { token =>
        Process.bracket(Task.delay {
          // Mounting a single file in Docker is supported, but troublesome.
          // Instead, we're going to create a temp directory for our temp file.
          //
          // Also troublesome is that we can only mount from our home directory
          // on docker-machine, so we can't just use java.io.tmpdir.
          Files.createDirectories(templateConfig.tempDir)
          val dir = Files.createTempDirectory(templateConfig.tempDir, "nelson")
          val file = dir.toFile
          dir.toFile.deleteOnExit()
          dir
        })(dir => Process.eval_(Task.delay(dir.toFile.delete()))) { dir =>
          withTempFile(tv.template, "nelson", ".template", dir) { file =>
            Process.eval(renderTemplate(cfg.pools.schedulingPool, templateConfig, cfg.dockercfg, file.toPath, token.value, env))
          }
        }
      }.runLastOr(sys.error("Expected a ConsulTemplateResult"))
    }

  @SuppressWarnings(Array("org.brianmckenna.wartremover.warts.IsInstanceOf")) // false wart
  def renderTemplate(
    scheduler: ScheduledExecutorService,
    templateConfig: TemplateConfig,
    dockerConfig: DockerConfig,
    path: Path,
    vaultToken: String,
    env: Map[String, String] = Map.empty
  ): Task[ConsulTemplateResult] = {
    val containerName = s"consul-template-${randomAlphaNumeric(12)}"
    val dockerCommand = for {
      vaultAddr <- templateConfig.vaultAddress
    } yield {
      "docker" :: "-H" :: dockerConfig.connection ::
      "run" :: "--name" :: containerName ::
      "--rm" :: "-v" :: s"${path.getParent}:/consul-template/templates" ::
      s"--memory=${templateConfig.memoryMegabytes}m" ::
      // Our kernel doesn't support swap limit capabilites. This supresses a warning we can't do anything about.
      s"--memory-swap=-1" ::
      s"--cpu-period=${templateConfig.cpuPeriod}" :: s"--cpu-quota=${templateConfig.cpuQuota}" ::
      "--net=host" ::
      env.map { case (k, v) => s"--env=${k}=${v}" }.toList :::
      templateConfig.consulTemplateImage ::
      "-once" :: "-dry" ::
      s"-vault-addr=${vaultAddr}" ::
      s"-vault-token=${vaultToken}" ::
      s"-template=/consul-template/templates/${path.getFileName}" ::
      Nil
    }

    def run(cmd: List[String]) = timedRun(Task.delay {
      val err = new StringBuilder
      def writeLine(s: String): Unit = { err.append(s).append("\n"); () }
      val pLogger = ProcessLogger(_ => (), writeLine)

      Task.delay {
        consulTemplateContainersRunning.inc()
        val exitCode = cmd.!(pLogger)
        exitCode
      }.timed(templateConfig.timeout)(scheduler).attempt.flatMap {
        case \/-(0) =>
          consulTemplateContainersRunning.dec()
          Task.now(Rendered)
        case \/-(14) =>
          consulTemplateContainersRunning.dec()
          Task.now(InvalidTemplate(err.toString))
        case \/-(n: Int) =>
          consulTemplateContainersRunning.dec()
          Task.fail(ConsulTemplateError(n, err.toString))
        case -\/(e: TimeoutException) =>
          // We gave up.  We need to terminate the container and show the user as far as we got.
          cleanup(scheduler, dockerConfig, containerName).attempt >> Task.now(TemplateTimeout(err.toString))
        case -\/(e) =>
          // Something went wrong internally.  We need to clean up the template container.
          cleanup(scheduler, dockerConfig, containerName).attempt >> Task.fail(e)
      }
    }.join)

    dockerCommand match {
      case Some(cmd) => run(cmd)
      case None => Task.fail(ConsulTemplateError(2, "No vault address detected. Templates are linted against the vault in the first data center."))
    }
  }

  private def cleanup(scheduler: ScheduledExecutorService, dockerConfig: DockerConfig, containerName: String) = {
    val stop = {
      val pLogger = ProcessLogger(_ => (), s => logger.info(s"[stopping docker $containerName]: ${s}"))
      Task.delay {
        logger.info(s"Stopping failed container: container=${containerName}")
        List("docker", "-H", dockerConfig.connection, "rm", "-f", containerName).!(pLogger)
      }.attempt
    }

    (time.awakeEvery(1.second)(implicitly, scheduler) >> Process.eval(stop))
      .take(5)
      .collect { case \/-(0) => () } // look for a success
      .take(1) // Only need to succeed once
      .runLast
      .map {
        case Some(_) =>
          consulTemplateContainersRunning.dec()
        case None =>
          consulTemplateContainerCleanupFailuresTotal.inc()
          logger.error(s"Failed to stop container. This probably leaked a thread: container=$containerName")
      }
  }

  @SuppressWarnings(Array("org.brianmckenna.wartremover.warts.IsInstanceOf")) // false wart
  private def timedRun(task: Task[ConsulTemplateResult]): Task[ConsulTemplateResult] =
    Task.delay(System.nanoTime).flatMap { startNanos =>
      task.attempt.flatMap { att =>
        val elapsed = System.nanoTime - startNanos
        consulTemplateRunsDurationSeconds.observe(elapsed / 1.0e9)
        att match {
          case \/-(Rendered) =>
            Task.now(Rendered)
          case \/-(it: InvalidTemplate) =>
            consulTemplateRunsFailuresTotal.labels("invalid_template").inc()
            Task.now(it)
          case \/-(it: TemplateTimeout) =>
            consulTemplateRunsFailuresTotal.labels("timeout").inc()
            Task.now(it)
          case -\/(e) =>
            consulTemplateRunsFailuresTotal.labels("error").inc()
            Task.fail(e)
        }
      }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy