4

I would like to access the docker API using the /var/lib/docker.sock unix domain socket. I've seen examples where you can use (modern versions of) curl to call the API as follows:

curl --unix-socket /var/run/docker.sock http:/containers/json

where the REST command is expressed in the /containers/json path. I was excited to see the Alpakka Unix Domain Socket adapter, but you only seem to be able to send and receive raw bytes. Is there any elegant way to do this? Or do I have to manually construct an HTTP header and manage all the difficult stuff manually?

3 Answers3

4

Here's a working snippet (see also the rest of the discussion at akka/akka-http#2139):

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
  • 8,307
  • 4
  • 32
  • 50
  • Wow! I really appreciate the time you’ve put into this. I’d been racking my brain, and ended up trying to hand-code the http protocol manually and then realizing how many edge cases I was going to have to handle... it’s far better to keep down this path and get the’ libraries to work together. I’ll try to put this snippet in place of what I was trying and will let y’all know how it works. (It’ll be two weeks when I get back from a trip.) – Murray Todd Williams Aug 17 '18 at 16:57
1

Interesting use-case. You should be able to use Alpakka Unix Domain socket flow and put Akka Http ClientLayer on top of it.

dvim
  • 2,223
  • 1
  • 17
  • 17
  • Thanks for the initial direction. If there are any additional breadcrumbs you (or anyone) could drop, it would be helpful. I've been staring through the source code for a few hours now and pondering this. As far as I understand your suggestion, it's to use the Bidi ClientLayer and somehow connect the SslTlsOutbound and SslTlsInbound flows to the UnixDomainSocket (since there's no SSL layer, there must be a way to use dummy that just passes the data through via a Flow[ByteString, ByteString, Future[OutgoingConnection]] abstraction as is used by UnixDomainSocket.outgoingConnection. – Murray Todd Williams Aug 06 '18 at 18:42
  • 1
    Take a look at [TLS Placebo](https://github.com/akka/akka/blob/d01487778652d92bfe1edbf1eb7c53b4de04a2b0/akka-stream/src/main/scala/akka/stream/scaladsl/TLS.scala#L203-L210) BidiFlow which unwraps SslTls* messages to ByteStrings without doing any crypto. Note that it is part of akka-stream project which is in [akka/akka](https://github.com/akka/akka) repo. Also you could implement akka-http [ClientTransport](https://github.com/akka/akka-http/blob/ed091bb29c3b4d76efc7055b6c935671d7e4e15e/akka-http-core/src/main/scala/akka/http/scaladsl/ClientTransport.scala#L26-L29) SPI for unix domain socket – dvim Aug 07 '18 at 06:25
  • Thanks for those extra breadcrumbs. It's been an interesting journey (I'm learning a lot about Akka Streams staring at the source code!) I believe I now fully understand the problem. (See my answer below.) – Murray Todd Williams Aug 07 '18 at 17:12
1

The short answer to the question is "It Can't be Done"—at least not with the existing building blocks of Akka HTTP and the Alkappa Unix Domain Sockets. You would have to handle writing the HTTP GET request by manually sending the headers, i.e. (using the Docker API as an example)

GET /v1.24/containers/json HTTP/1.1\n
Host: localhost\n
\n\n

...and then reading the TCP response manually. Additionally, the Unix Domain Socket logic can't use the Alpakka code because it only currently provides a ServerBinding, and thus is designed to create a server that handles requests to a Unix socket, not to send data to a Unix socket and handle the response.

So everything has to be done manually. There's another StackOverflow question here that points out how the AFUNIXSocket github source code could be used to help with some of the low-level Unix Domain Socket logic that might be of help to others wanting to tackle this same problem.

The most elegant solution would also involve (as suggested by dvim's comment) writing an HTTP.ClientTransport to plug in the Unix Domain Socket communication layer and allow the HTTP library to expose the low-level functionality of writing request/response headers, etc. (One interesting note on that is that the API assumes a host/port parameter pair, which is tightly bound to the TCP paradigm.)