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

com.lightbend.rp.servicediscovery.scaladsl.ServiceLocator.scala Maven / Gradle / Ivy

/*
 * Copyright 2017 Lightbend, Inc.
 *
 * 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 com.lightbend.rp.servicediscovery.scaladsl

import akka.actor._
import akka.io.{ Dns, IO }
import akka.io.dns.{ DnsProtocol, SRVRecord, ARecord, AAAARecord }
import akka.pattern.{ after, ask }
import com.lightbend.rp.common._
import com.lightbend.rp.servicediscovery.scaladsl.ServiceLocatorLike.{ AddressSelection, AddressSelectionRandom }
import java.net.URI
import java.util.concurrent.ThreadLocalRandom
import scala.collection.immutable.Seq
import scala.concurrent.{ ExecutionContext, Future }
import scala.concurrent.duration.FiniteDuration

case class ServiceLocator(as: ActorSystem) {
  def lookupOne(namespace: String, name: String, endpoint: String): Future[Option[Service]] =
    ServiceLocator.lookupOne(namespace, name, endpoint)(as)

  def lookupOne(name: String, endpoint: String): Future[Option[Service]] =
    ServiceLocator.lookupOne(name, endpoint)(as)

  def lookupOne(name: String): Future[Option[Service]] =
    ServiceLocator.lookupOne(name)(as)

  def lookupOne(namespace: String, name: String, endpoint: String, addressSelection: AddressSelection): Future[Option[Service]] =
    ServiceLocator.lookupOne(namespace, name, endpoint, addressSelection)(as)

  def lookupOne(namespace: String, name: String, addressSelection: AddressSelection): Future[Option[Service]] =
    ServiceLocator.lookupOne(namespace, name, addressSelection)(as)

  def lookupOne(name: String, addressSelection: AddressSelection): Future[Option[Service]] =
    ServiceLocator.lookupOne(name, addressSelection)(as)

  def lookup(namespace: String, name: String, endpoint: String): Future[Seq[Service]] =
    ServiceLocator.lookup(namespace, name, endpoint)(as)

  def lookup(name: String, endpoint: String): Future[Seq[Service]] =
    ServiceLocator.lookup(name, endpoint)(as)

  def lookup(name: String): Future[Seq[Service]] =
    ServiceLocator.lookup(name)(as)
}

object ServiceLocator extends ServiceLocatorLike {
  def dnsResolver(implicit as: ActorSystem): ActorRef = IO(Dns)

  def env: Map[String, String] = sys.env

  def targetRuntime: Option[Platform] = Platform.active

}

object ServiceLocatorLike {
  type AddressSelection = Seq[Service] => Option[Service]

  val AddressSelectionRandom: AddressSelection =
    services => if (services.isEmpty) None else Some(services(ThreadLocalRandom.current.nextInt(services.length)))

  val AddressSelectionFirst: AddressSelection = services =>
    services.headOption
}

trait ServiceLocatorLike {
  val DnsCharacters: Set[Char] = Set('_', '.')

  val ValidEndpointServiceChars: Set[Char] =
    (('0' to '9') ++ ('A' to 'Z') ++ ('a' to 'z') ++ Seq('-')).toSet

  def targetRuntime: Option[Platform]
  def dnsResolver(implicit as: ActorSystem): ActorRef
  def env: Map[String, String]

  def translateName(namespace: Option[String], name: String, endpoint: String): String =
    targetRuntime match {
      case _ if endpoint.isEmpty =>
        name

      case Some(Kubernetes) =>
        if (name.exists(DnsCharacters.contains)) {
          name
        } else {
          val serviceNamespace = namespace.orElse(namespaceFromEnv()).getOrElse(kubernetes.DefaultNamespace)
          val clusterSuffix = suffixFromEnv().getOrElse(kubernetes.DefaultSuffix)
          val serviceName = endpointServiceName(name)
          val endpointName = endpointServiceName(endpoint)

          // @TODO hardcoded _tcp
          s"_$endpointName._tcp.$serviceName.$serviceNamespace.svc.$clusterSuffix"
        }

      case Some(Mesos) =>
        if (name.exists(DnsCharacters.contains)) {
          name
        } else {
          val serviceNamespace = namespace.orElse(namespaceFromEnv())
          val serviceName = endpointServiceName(name)
          val endpointName = endpointServiceName(endpoint)

          // @TODO hardcoded _tcp
          s"_$endpointName._$serviceName${serviceNamespace.fold("")(ns => s"-$ns")}._tcp.marathon.mesos"
        }

      case None =>
        name
    }

  def translateProtocol(endpoint0: String, srvName: String): Option[String] = {
    val dotParts = srvName.split('.').toList
    // pick up http endpoint from SRV name in case endpoint0 is empty
    val endpoint =
      if (endpoint0 == "" && dotParts.contains[String]("_http")) "http"
      else endpoint0

    targetRuntime match {
      case _ if endpoint == "" => Option.empty

      case Some(Kubernetes) =>
        Option(dotParts)
          .filter(_.length >= 2)
          .map(parts => normalizeProtocol(parts(1).dropWhile(_ == '_'), endpoint))

      case Some(Mesos) =>
        // Mesos has an undocumented port name query syntax (https://github.com/mesosphere/mesos-dns/issues/478)
        // so the protocol specification may not always be the second entry, sometimes it is the third.

        val knownSrvProtocols = Set("_tcp", "_udp")
        val candidates = Vector(dotParts.lift(1), dotParts.lift(2)).flatten

        candidates
          .find(knownSrvProtocols.contains)
          .orElse(candidates.headOption)
          .map(part => normalizeProtocol(part.dropWhile(_ == '_'), endpoint))

      case _ => Option.empty
    }
  }

  def translateResolvedSrv(protocol: Option[String], srvRecord: SRVRecord, addressARecord: DnsProtocol.Resolved): Seq[Service] =
    targetRuntime match {
      case Some(Kubernetes | Mesos) =>
        val uris = addressARecord.records collect {
          case r: ARecord =>
            new URI(protocol.orNull, null, r.ip.getHostAddress, srvRecord.port, null, null, null)
          case r: AAAARecord =>
            new URI(protocol.orNull, null, r.ip.getHostAddress, srvRecord.port, null, null, null)
        }
        uris.map(u => Service(srvRecord.target, u))
      case None =>
        Seq.empty
    }

  def translateResolved(protocol: Option[String], hostname: String, addressARecord: DnsProtocol.Resolved): Seq[Service] =
    targetRuntime match {
      case Some(Kubernetes | Mesos) =>
        val uris = addressARecord.records collect {
          case r: ARecord =>
            new URI(protocol.orNull, r.ip.getHostAddress, null, null)
          case r: AAAARecord =>
            new URI(protocol.orNull, r.ip.getHostAddress, null, null)
        }
        uris.map(u => Service(hostname, u))
      case None =>
        Seq.empty
    }

  def lookupOne(namespace: String, name: String, endpoint: String, addressSelection: AddressSelection)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(namespace, name, endpoint).map(addressSelection)
  }

  def lookupOne(name: String, endpoint: String, addressSelection: AddressSelection)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(name, endpoint).map(addressSelection)
  }

  def lookupOne(name: String, addressSelection: AddressSelection)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(name).map(addressSelection)
  }

  def lookupOne(namespace: String, name: String, endpoint: String)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(namespace, name, endpoint).map(AddressSelectionRandom)
  }

  def lookupOne(name: String, endpoint: String)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(name, endpoint).map(AddressSelectionRandom)
  }

  def lookupOne(name: String)(implicit as: ActorSystem): Future[Option[Service]] = {
    import as.dispatcher
    lookup(name).map(AddressSelectionRandom)
  }

  def lookup(namespace: String, name: String, endpoint: String)(implicit as: ActorSystem): Future[Seq[Service]] =
    doLookup(namespace = Option(namespace).filter(_.nonEmpty), name, endpoint, externalChecks = 0)

  def lookup(name: String, endpoint: String)(implicit as: ActorSystem): Future[Seq[Service]] =
    doLookup(namespace = None, name, endpoint, externalChecks = 0)

  def lookup(name: String)(implicit as: ActorSystem): Future[Seq[Service]] =
    doLookup(namespace = None, name, endpoint = "", externalChecks = 0)

  private def normalizeProtocol(protocol: String, endpoint: String) =
    if (protocol == "tcp" && (endpoint == "http" || endpoint.contains("http-") || endpoint.contains("-http")))
      "http"
    else
      protocol

  private def doLookup(namespace: Option[String], name: String, endpoint: String, externalChecks: Int)(implicit as: ActorSystem): Future[Seq[Service]] = {
    as.log.debug(s"looking up namespace = $namespace, name = $name, endpoint = $endpoint, externalChecks = $externalChecks")

    val settings = Settings(as)

    import as.dispatcher

    val externalEntry =
      if (endpoint.isEmpty)
        name
      else
        s"$name/$endpoint"

    def retry[T](delays: Seq[FiniteDuration])(value: => Future[T]): Future[T] =
      value
        .recoverWith {
          case _ if delays.nonEmpty => after(delays.head, as.scheduler)(retry(delays.tail)(value))
        }

    settings.externalServiceAddresses.get(externalEntry) match {
      case Some(services) =>
        val resolved =
          services.map { uri =>
            if (uri.getScheme == null && externalChecks < settings.externalServiceAddressLimit) {
              val parts = uri.toString.split("/", 2)

              if (parts.length == 2)
                doLookup(namespace, parts(0), parts(1), externalChecks + 1)
              else
                doLookup(namespace, parts(0), "", externalChecks + 1)
            } else {
              Future.successful(Seq(Service(name, uri)))
            }
          }

        for {
          results <- Future.sequence(resolved)
        } yield results.flatten.toVector

      case None =>
        targetRuntime match {
          case None =>
            Future.successful(Seq.empty)

          case Some(Kubernetes | Mesos) =>
            val nameToLookup = translateName(namespace, name, endpoint)
            val queryType =
              if (nameToLookup.startsWith("_")) DnsProtocol.Srv
              else DnsProtocol.Ip(ipv6 = false)

            for {
              result <- dnsResolver
                .ask(DnsProtocol.resolve(nameToLookup, queryType))(settings.askTimeout)
                .flatMap {
                  case resolved: DnsProtocol.Resolved =>
                    val srvName = resolved.name
                    lazy val protocol = translateProtocol(endpoint, srvName)
                    val lookups: Seq[Future[Seq[Service]]] = resolved.records collect {
                      case srvRecord: SRVRecord =>
                        retry(settings.retryDelays)(dnsResolver.ask(DnsProtocol.resolve(srvRecord.target, DnsProtocol.Ip(ipv6 = false)))(settings.askTimeout))
                          .collect {
                            case aRecord: DnsProtocol.Resolved =>
                              translateResolvedSrv(protocol, srvRecord, aRecord)
                          }
                      case aRecord: ARecord =>
                        Future.successful(translateResolved(protocol, name, resolved))
                    }
                    Future
                      .sequence(lookups)
                      .map(_.flatten.toVector)
                }
            } yield result
        }
    }
  }

  protected def namespaceFromEnv(): Option[String] =
    env.get("RP_NAMESPACE")

  protected def suffixFromEnv(): Option[String] = {
    env.get("RP_KUBERNETES_CLUSTER_SUFFIX")
  }

  private def endpointServiceName(name: String): String =
    name
      .map(c => if (ValidEndpointServiceChars.contains(c)) c else '-')
      .toLowerCase
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy