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

routing.RoutingTable.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
package routing

import storage._
import quiver.{LNode,LEdge}
import scalaz._
import Scalaz._
import journal._

object RoutingTable {
  import Datacenter._

  private val log = Logger[RoutingTable.type]

  implicit val NamespaceNameOrder: Order[ServiceTarget] =
    Order[String].contramap[ServiceTarget](_._1)

  /*
   * add a single route to the routing table
   */
  private def addTarget(from: RoutingNode)(to: Target): GraphBuild[Unit] = {
    to match {
      case SingletonTarget(d) =>
        graphBuild.modify { rg =>
          val node = RoutingNode(d)
          d.unit.ports.foldLeft(rg)((rg,p) =>
            rg.addEdge(LEdge(from, node, RoutePath(d, p.name, p.protocol,p.port, 100))))
        }

      case x: TrafficShift =>
        val weightFrom = Math.ceil(x.fromValue * 100).toInt
        val weightTo = 100 - weightFrom

        graphBuild.modify { rg =>
          val toNode = RoutingNode(x.to)
          val fromNode = RoutingNode(x.from)

          val rg2 = x.from.unit.ports.foldLeft(rg)((rg, p) =>
            rg.addEdge(LEdge(from, fromNode, RoutePath(x.from, p.name, p.protocol, p.port, weightFrom))))

          x.to.unit.ports.foldLeft(rg2)((rg, p) =>
            rg.addEdge(LEdge(from, toNode, RoutePath(x.to, p.name, p.protocol, p.port, weightTo))))
        }
    }
  }

  private def addLoadbalancerDependency(from: LoadbalancerDeployment)(r: Manifest.Route): GraphBuild[Unit] =
    for {
      rts <- graphBuild.ask
      ns  <- StoreOp.getNamespaceByID(from.nsid).liftM[GraphBuildT]
      d   <- rts.lookup(ns.name).flatMap(_.lookup((r.destination.name, from.loadbalancer.version))).point[StoreOpF].liftM[GraphBuildT]
      _   <- d.fold(graphBuild.tell(List(s"missing ${r.destination.name} dependency for ${from.stackName}")))(t => addTarget(RoutingNode(from))(t))
    } yield ()

  private def addDependencies(from: Deployment, sn: ServiceName): GraphBuild[Unit] = {

    val dc = from.namespace.datacenter

    def lookup(ns: NamespaceName, rts: RoutingTables): Option[Target] =
      rts.lookup(ns).flatMap(_.lookup((sn.serviceType, sn.version.toMajorVersion)))

    // Look for dependency in current namespace. If target doesn't exist
    // move up the namespace ancestery until target is found.
    def resolveDefault(ns: NamespaceName, rts: RoutingTables): Option[Target] =
      lookup(ns, rts) orElse ns.parent.flatMap(p => resolveDefault(p, rts))

    def addDefault(ns: Namespace): GraphBuild[Unit] =
      for {
        rt <- graphBuild.ask
        d  <- resolveDefault(ns.name, rt).point[StoreOpF].liftM[GraphBuildT]
        _  <- d.fold(graphBuild.tell(List(s"missing $sn dependency for ${from.stackName}")))(t => addTarget(RoutingNode(from))(t))
      } yield ()

    // Look for all downsteram dependecies.
    def resolveDownstream(ns: Namespace, rts: RoutingTables): List[Target] =
      rts.keys
        .filter(n => ns.name.isSubordinate(n))
        .flatMap(n => lookup(n, rts))

    def addDownstream(ns: Namespace): GraphBuild[Unit] =
      for {
        rt <- graphBuild.ask
        ds <- resolveDownstream(ns, rt).point[StoreOpF].liftM[GraphBuildT]
        _  <- ds.traverse(t => addTarget(RoutingNode(from))(t))
      } yield ()

    addDefault(from.namespace) >> addDownstream(from.namespace)
  }

  /*
   * for each node in the graph, add edges representing the possible routes
   */
  private def addEdges: GraphBuild[Unit] = {

    def add(rn: RoutingNode): GraphBuild[Unit] =
      rn.node.fold(
        lb => lb.loadbalancer.routes.traverse_(r => addLoadbalancerDependency(lb)(r)),
        ds => ds.unit.dependencies.toVector.traverse_(sn => addDependencies(ds, sn))
      )

    graphBuild.get.flatMap(_.nodes.traverse_(rn => add(rn)))
  }

  /*
   * prunes the routing graph by a given namespace.
   * keep nodes that are in the given namepsace or
   * has an incoming edge from a node in the given namespace or
   * has and outgoing edge to a node in the give namepsace
   */
  private def prune(gr: RoutingGraph, n: Namespace): RoutingGraph =
    gr.nfilter { d =>
      d.deployment.exists(_.nsid == n.id) ||
      gr.ins(d).exists(_._2.nsid == n.id) ||     // incoming edge, source
      gr.outs(d).exists(_._1.stack.nsid == n.id) // outgoing edge, destination
    }

  // given a namespace, return all upstream and downstream namespaces
  private def upDownNamespaces(ns: Namespace): StoreOpF[Set[Namespace]] =
    StoreOp.listNamespacesForDatacenter(ns.datacenter).map(_.filter(n =>
      ns.name.isSubordinate(n.name) || // downstream
      n.name.isSubordinate(ns.name)    // upstream
    ))

  // Input deployments are assumed to be in the same namespace
  private def generateRoutingTable(ns: Namespace, ds: List[Deployment]): StoreOpF[RoutingTable] = {

    // only deployments that expose ports can be targets
    val st: Set[ServiceTarget] =
      ds.filter(!_.unit.ports.isEmpty)
        .map(x => (x.unit.name, x.unit.version.toMajorVersion)).toSet

    st.foldLeft[StoreOpF[RoutingTable]](==>>.empty.point[StoreOpF]) { (s, x) =>
      for {
        m <- s
        t <- StoreOp.getCurrentTargetForServiceName(ns.id, ServiceName(x._1, x._2.minFeatureVersion))
      } yield t.cata(t => m.insert(x, t), m)
    }
  }

  private def generateRoutingTables(ds: List[Deployment]): StoreOpF[RoutingTables] = {
    ds.groupBy(_.namespace).foldLeft[StoreOpF[RoutingTables]](==>>.empty.point[StoreOpF]) { (s, x) =>
      val (ns, d) = x
      for {
        m  <- s
        rt <- generateRoutingTable(ns, d)
      } yield m.insert(ns.name, rt)
    }
  }

  private def getDeployments(ns: Set[Namespace]): StoreOpF[List[Deployment]] =
    ns.toList.traverseM { n =>
      StoreOp.listDeploymentsForNamespaceByStatus(n.id, DeploymentStatus.routable)
        .map(_.map(_._1))
        .map(_.toList)
    }

  private def getLoadbalancers(ns: Set[Namespace]): StoreOpF[List[LoadbalancerDeployment]] =
    ns.toList.traverseM { n =>
      StoreOp.listLoadbalancerDeploymentsForNamespace(n.id).map(_.toList)
    }

  private def generateGraph(ds: List[Deployment], lb: List[LoadbalancerDeployment]): StoreOpF[RoutingGraph] =
    for {
      rts  <- generateRoutingTables(ds)
      nodes = ds.map(RoutingNode(_)) ::: lb.map(RoutingNode(_))
      seed  = nodes.foldLeft[RoutingGraph](quiver.empty)((g,d) => g.addNode(LNode(d, ())))
      gr   <- addEdges.run(rts, seed)
    } yield {
      gr._1.foreach(e => log.error("ERROR calculating dependency graph: "+ e))
      gr._3
    }

  /*
   * produces a routing graph for a single deployment, outgoing edges only
   */
  def outgoingRoutingGraph(d: Deployment): StoreOpF[RoutingGraph] = {

    def dependencies(n: Namespace): StoreOpF[Vector[Deployment]] =
      d.unit.dependencies.toVector.traverseM { sn =>
        StoreOp.getCurrentTargetForServiceName(n.id, sn)
          .map(_.cata(_.deployments, Vector()))
      }

    for {
      ud <- upDownNamespaces(d.namespace)
      ns  = ud + d.namespace
      ds <- ns.toVector.traverseM(n => dependencies(n))
      gr <- generateGraph(d :: ds.toList, Nil)
    } yield gr
  }

  /*
   * produces a graph containing all routable services in the given namespace.
   *
   * note: Some nodes might be in other namespaces because of how dependency
   * resolution works.
   */
  def routingGraph(n: Namespace): StoreOpF[RoutingGraph] =
    for {
      ud <- upDownNamespaces(n)
      ns  = ud + n
      ds <- getDeployments(ns)
      ls <- getLoadbalancers(ns)
      gr <- generateGraph(ds, ls)
    } yield prune(gr, n)

  def generateRoutingTables(dc: String): StoreOpF[List[(Namespace, RoutingGraph)]] =
    for {
      ns  <- StoreOp.listNamespacesForDatacenter(dc)
      ds  <- getDeployments(ns)
      ls  <- getLoadbalancers(ns)
      gr  <- generateGraph(ds, ls)
    } yield ns.toList.map(n => (n, prune(gr, n)))
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy