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

pl.touk.nussknacker.k8s.manager.deployment.DeploymentPreparer.scala Maven / Gradle / Ivy

package pl.touk.nussknacker.k8s.manager.deployment

import com.typesafe.scalalogging.LazyLogging
import io.circe.syntax.EncoderOps
import monocle.Iso
import monocle.macros.GenLens
import monocle.std.option._
import pl.touk.nussknacker.engine.api.{LiteStreamMetaData, ProcessVersion, RequestResponseMetaData, TypeSpecificData}
import pl.touk.nussknacker.k8s.manager.K8sDeploymentManager.{
  labelsForScenario,
  objectNameForScenario,
  scenarioIdLabel,
  scenarioVersionAnnotation,
  versionAnnotationForScenario
}
import pl.touk.nussknacker.k8s.manager.K8sDeploymentManagerConfig
import skuber.EnvVar.FieldRef
import skuber.LabelSelector.IsEqualRequirement
import skuber.apps.v1.Deployment
import skuber.apps.v1.Deployment.Strategy
import skuber.{Container, EnvVar, HTTPGetAction, LabelSelector, Pod, Probe, Volume}

case class MountableResources(
    commonConfigConfigMap: String,
    loggingConfigConfigMap: String,
    runtimeConfigSecret: String
)

class DeploymentPreparer(config: K8sDeploymentManagerConfig) extends LazyLogging {

  private val CommonConfigMountPath  = "/config"
  private val LoggingConfigMountPath = "/logging-config"
  private val RuntimeConfigMountPath = "/runtime-config"

  def prepare(
      processVersion: ProcessVersion,
      typeSpecificData: TypeSpecificData,
      resourcesToMount: MountableResources,
      determinedReplicasCount: Int
  ): Deployment = {
    val userConfigurationBasedDeployment = DeploymentUtils.parseDeploymentWithFallback(
      config.k8sDeploymentConfig,
      getClass.getResource(s"/defaultMinimalDeployment.conf")
    )
    applyDeploymentDefaults(
      userConfigurationBasedDeployment,
      processVersion,
      typeSpecificData,
      resourcesToMount,
      determinedReplicasCount,
      config.nussknackerInstanceName
    )
  }

  private def applyDeploymentDefaults(
      userConfigurationBasedDeployment: Deployment,
      processVersion: ProcessVersion,
      typeSpecificData: TypeSpecificData,
      resourcesToMount: MountableResources,
      determinedReplicasCount: Int,
      nussknackerInstanceName: Option[String]
  ) = {
    val objectName  = objectNameForScenario(processVersion, config.nussknackerInstanceName, None)
    val annotations = versionAnnotationForScenario(processVersion)
    val labels      = labelsForScenario(processVersion, nussknackerInstanceName)

    // we use 'OptionOptics some' here and do not worry about withDefault because _.spec is provided in defaultMinimalDeployment
    val deploymentSpecLens = GenLens[Deployment](_.spec) composePrism some
    val templateSpecLens =
      deploymentSpecLens composeLens GenLens[Deployment.Spec](_.template.spec) composeIso withDefault(Pod.Spec())
    val deploymentLens =
      GenLens[Deployment](_.metadata.name).set(objectName) andThen
        GenLens[Deployment](_.metadata.labels).modify(_ ++ labels) andThen
        GenLens[Deployment](_.metadata.annotations).modify(_ ++ annotations) andThen
        // here we use id to avoid sanitization problems
        (deploymentSpecLens composeLens GenLens[Deployment.Spec](_.selector))
          .set(LabelSelector(IsEqualRequirement(scenarioIdLabel, processVersion.processId.value.toString))) andThen
        (deploymentSpecLens composeLens GenLens[Deployment.Spec](_.strategy)).modify(maybeStrategy =>
          maybeStrategy.orElse(Some(deploymentStrategy(typeSpecificData)))
        ) andThen
        (deploymentSpecLens composeLens GenLens[Deployment.Spec](_.replicas))
          .modify(modifyReplicasCount(determinedReplicasCount)) andThen
        (deploymentSpecLens composeLens GenLens[Deployment.Spec](_.template.metadata.name)).set(objectName) andThen
        (deploymentSpecLens composeLens GenLens[Deployment.Spec](_.template.metadata.labels))
          .modify(_ ++ labels) andThen
        (templateSpecLens composeLens GenLens[Pod.Spec](_.volumes)).modify(
          _ ++ List(
            Volume("common-conf", Volume.ConfigMapVolumeSource(resourcesToMount.commonConfigConfigMap)),
            Volume("logging-conf", Volume.ConfigMapVolumeSource(resourcesToMount.loggingConfigConfigMap)),
            Volume("runtime-conf", Volume.Secret(resourcesToMount.runtimeConfigSecret)),
          )
        ) andThen
        (templateSpecLens composeLens GenLens[Pod.Spec](_.containers)).modify(containers =>
          modifyContainers(containers)
        )
    deploymentLens(userConfigurationBasedDeployment)
  }

  private def deploymentStrategy(typeSpecificData: TypeSpecificData): Strategy = {
    typeSpecificData match {
      case _: LiteStreamMetaData => Deployment.Strategy.Recreate
      case _: RequestResponseMetaData =>
        Deployment.Strategy(rollingUpdate =
          Deployment.RollingUpdate(maxUnavailable = Right("25%"), maxSurge = Right("25%"))
        )
      case other: TypeSpecificData => throw new IllegalStateException(s"Deploying $other unsupported on k8s")
    }
  }

  private def modifyReplicasCount(determinedReplicasCount: Int)(maybeReplicasFromRawConfig: Option[Int]) =
    maybeReplicasFromRawConfig match {
      case Some(replicasFromRawConfig) =>
        logger.warn(
          s"Found replicas specified in config: $replicasFromRawConfig that will be used instead of replicas determined based on scaling config: $determinedReplicasCount. It can cause not equal tasks assigment to replicas"
        )
        Some(replicasFromRawConfig)
      case None =>
        Some(determinedReplicasCount)
    }

  private def withDefault[A](defaultValue: A): Iso[Option[A], A] =
    Iso[Option[A], A](_.getOrElse(defaultValue))(value => if (value == defaultValue) None else Some(value))

  private def modifyContainers(containers: List[Container]): List[Container] = {
    val image                 = s"${config.dockerImageName}:${config.dockerImageTag}"
    val defaultTimeoutSeconds = 5
    val runtimeContainer = Container(
      name = "runtime",
      image = image,
      env = List(
        EnvVar("SCENARIO_FILE", s"$CommonConfigMountPath/scenario.json"),
        EnvVar("CONFIG_FILE", s"/opt/nussknacker/conf/application.conf,$RuntimeConfigMountPath/runtimeConfig.conf"),
        EnvVar("DEPLOYMENT_CONFIG_FILE", s"$CommonConfigMountPath/deploymentConfig.conf"),
        EnvVar("LOGBACK_FILE", s"$LoggingConfigMountPath/logback.xml"),
        // We pass POD_NAME, because there is no option to pass only replica hash which is appended to pod name.
        // Hash will be extracted on entrypoint side.
        EnvVar("POD_NAME", FieldRef("metadata.name"))
      ),
      volumeMounts = List(
        Volume.Mount(name = "common-conf", mountPath = CommonConfigMountPath),
        Volume.Mount(name = "logging-conf", mountPath = LoggingConfigMountPath),
        Volume.Mount(name = "runtime-conf", mountPath = RuntimeConfigMountPath),
      ),
      // used standard AkkaManagement see HealthCheckServerRunner for details
      startupProbe = Some(
        Probe(
          new HTTPGetAction(Left(8080), path = "/alive"),
          periodSeconds = Some(1),
          failureThreshold = Some(60),
          timeoutSeconds = defaultTimeoutSeconds
        )
      ),
      readinessProbe = Some(
        Probe(
          new HTTPGetAction(Left(8080), path = "/ready"),
          periodSeconds = Some(5),
          failureThreshold = Some(3),
          timeoutSeconds = defaultTimeoutSeconds
        )
      ),
      livenessProbe = Some(
        Probe(
          new HTTPGetAction(Left(8080), path = "/alive"),
          periodSeconds = Some(5),
          failureThreshold = Some(3),
          timeoutSeconds = defaultTimeoutSeconds
        )
      )
    )

    def modifyRuntimeContainer(value: Container): Container = {
      val containerLens = GenLens[Container](_.env).modify(_ ++ runtimeContainer.env) andThen
        GenLens[Container](_.volumeMounts).modify(_ ++ runtimeContainer.volumeMounts) andThen
        GenLens[Container](_.startupProbe).modify(_.orElse(runtimeContainer.startupProbe)) andThen
        GenLens[Container](_.readinessProbe).modify(_.orElse(runtimeContainer.readinessProbe)) andThen
        GenLens[Container](_.livenessProbe).modify(_.orElse(runtimeContainer.livenessProbe)) andThen
        GenLens[Container](_.image).modify { configuredImage =>
          if (configuredImage != runtimeContainer.image)
            logger.warn(s"Overriding $configuredImage image with ${runtimeContainer.image} image")
          runtimeContainer.image
        }
      containerLens(value)
    }

    val (runtimeContainers, nonRuntimeContainers) = containers.partition(_.name == "runtime")

    (runtimeContainers match {
      case Nil           => List(runtimeContainer)
      case single :: Nil => List(modifyRuntimeContainer(single))
      case _             => throw new IllegalStateException("Deployment should have only one 'runtime' container")
    }) ++ nonRuntimeContainers
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy