akka / akka-http

The Streaming-first HTTP server/module of Akka
https://doc.akka.io/libraries/akka-http/current/
Other
1.34k stars 594 forks source link

ClientTransport.connectTo API tightly coupled to TCP's host/port paradigm #2139

Closed murraytodd closed 4 years ago

murraytodd commented 6 years ago

I am considering trying to write a ClientTransport implementation to enable Akka HTTP to access a service leveraging Unix Domain Sockets. (Specifically, the Docker API is exposed on /var/run/docker.sock and uses HTTP REST)

Looking at the ClientTransport trait that defines the general API, connectTo assumes a hostname/port pairing which is tightly coupled to the TCP implementation. If one were to create a Unix Domain Sockets implementation, it would require some inelegant approaches like forcing the port parameter to be supplied but ignored, and for the filename to be passed in the named "host" parameter, etc.

I realize that an API change in something as central as ClientTransport.connectTo isn't something done lightly—but I figured it was worth consideration. If there's a better way for an external (Alpakka) module to deal with this (like implementing connectTo but either making the hack or returning an exception directing the developer to use an alternative method call), please let me know.

ktoso commented 6 years ago

I realize that an API change in something as central as ClientTransport.connectTo isn't something done lightly—but I figured it was worth consideration.

Changing the existing types is not an option because of compatibility guarantees Akka upholds. In reality what I'd advice in this specific case is to yes pass the file as the hostname and ignore the port.

What is the intended use and will end users even be exposed to this in what you are building?

jrudolph commented 6 years ago

ClientTransport is marked @ApiMayChange, so we could still change it.

If the socket file path is hardcoded, couldn't you otherwise just ignore the given host/port and create the connection directly?

But that still wouldn't help, because you still need an akka-streams API for unix domain sockets and ClientTransport is only the first API that doesn't support iut. The next is the akka-streams TCP, then the Akka IO API and last not even the Java JDK supports unix domain sockets out of the box.

(I guess it could be supported with "moderate" complexity if we would open up Akka IO, and akka-streams API to use any kind of already connected java.nio.channels.SocketChannel. https://github.com/jnr/jnr-unixsocket could then provide such a channel.)

johanandren commented 6 years ago

@jrudolph There is support for unix domain sockets in Alpakka since a while though https://developer.lightbend.com/docs/alpakka/current/unix-domain-socket.html

2m commented 6 years ago

For additional context, see this SO question: https://stackoverflow.com/questions/51565935/how-to-access-rest-api-on-a-unix-domain-socket-with-akka-http-or-alpakka

jrudolph commented 6 years ago

Thanks, @johanandren. I missed that somehow. So, maybe even without changing anything in akka-http for now we could provide an custom example ClientContext that achieves that result?

jrudolph commented 6 years ago

Here's a working snippet:

build.sbt:

val scalaV = "2.12.6"
val akkaV = "2.5.14"
val akkaHttpV = "10.1.3"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-http" % akkaHttpV,
  "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpV,
  "com.typesafe.akka" %% "akka-stream" % akkaV,
  "com.lightbend.akka" %% "akka-stream-alpakka-unix-domain-socket" % "0.20",
)

DockerSockMain.scala:

import java.io.File
import java.net.InetSocketAddress

import akka.actor.ActorSystem
import akka.http.scaladsl.ClientTransport
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.HttpRequest
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.settings.ClientConnectionSettings
import akka.http.scaladsl.settings.ConnectionPoolSettings
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import akka.stream.alpakka.unixdomainsocket.scaladsl.UnixDomainSocket
import akka.stream.scaladsl.Flow
import akka.util.ByteString
import spray.json.JsValue

import scala.concurrent.Future

object DockerSockMain extends App {
  object DockerSockTransport extends ClientTransport {
    override def connectTo(host: String, port: Int, settings: ClientConnectionSettings)(implicit system: ActorSystem): Flow[ByteString, ByteString, Future[Http.OutgoingConnection]] = {
      // ignore everything for now

      UnixDomainSocket().outgoingConnection(new File("/var/run/docker.sock"))
        .mapMaterializedValue { _ =>
          // Seems that the UnixDomainSocket.OutgoingConnection is never completed? It works anyway if we just assume it is completed
          // instantly
          Future.successful(Http.OutgoingConnection(InetSocketAddress.createUnresolved(host, port), InetSocketAddress.createUnresolved(host, port)))
        }
    }
  }

  implicit val system = ActorSystem()
  implicit val mat = ActorMaterializer()
  import system.dispatcher

  val settings = ConnectionPoolSettings(system).withTransport(DockerSockTransport)

  import SprayJsonSupport._
  def handleResponse(response: HttpResponse): Future[String] =
    // TODO: create docker json model classes and directly marshal to them
    Unmarshal(response).to[JsValue].map(_.prettyPrint)

  Http().singleRequest(HttpRequest(uri = "http://localhost/images/json"), settings = settings)
    .flatMap(handleResponse)
    .onComplete { res =>
      println(s"Got result: [$res]")
      system.terminate()
    }
}
jrudolph commented 4 years ago

The problem is that what we get from URIs is host/port so the interface matches what is needed in general. Since the workaround works nicely for docker, let's close this for now.