Repository: 7mind/izumi
Pull request #1469 (2021-04-30 10:48:18)

Feature: ipv6 in docker

codecov-commenter: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/1469?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=7mind) Report
> Merging [#1469](https://codecov.io/gh/7mind/izumi/pull/1469?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=7mind) (0645854) into [develop](https://codecov.io/gh/7mind/izumi/commit/c9a9293620c05bed642e55998c07b7ac40f52b7b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=7mind) (c9a9293) will **decrease** coverage by `0.09%`.
> The diff coverage is `0.00%`.


```diff
@@             Coverage Diff             @@
##           develop    #1469      +/-   ##
===========================================
- Coverage    55.97%   55.87%   -0.10%     
===========================================
  Files          456      456              
  Lines         9203     9219      +16     
  Branches       451      445       -6     
===========================================
  Hits          5151     5151              
- Misses        4052     4068      +16     
```


Patch:
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
index 7e53f1a84..94cc657ae 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/ContainerResource.scala
@@ -377,9 +377,9 @@ case class ContainerResource[F[_], T](
           case static: DockerPort.Static =>
             Some(toExposedPort(static, static.number))
         }
-        val mappings = exposedPort.toSeq.flatMap(p => Option(network.getPorts.getBindings.get(p))).flatten.map {
+        val mappings = exposedPort.toSeq.flatMap(p => Option(network.getPorts.getBindings.get(p))).flatten.flatMap {
           port =>
-            ServicePort(port.getHostIp, Integer.parseInt(port.getHostPortSpec))
+            ServiceHost(port.getHostIp).map(ServicePort(_, Integer.parseInt(port.getHostPortSpec)))
         }
         (containerPort, NonEmptyList.from(mappings))
     }
@@ -389,7 +389,7 @@ case class ContainerResource[F[_], T](
       Left(UnmappedPorts(unmapped))
     } else {
       val dockerHost = client.rawClientConfig.getDockerHost.getHost
-      val networkAddresses = network.getNetworks.asScala.values.toList.map(_.getIpAddress)
+      val networkAddresses = network.getNetworks.asScala.values.toList.map(_.getIpAddress).flatMap(ServiceHost(_))
       val mapped = ports.collect { case (cp, Some(lst)) => (cp, lst) }
 
       Right(
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/Docker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/Docker.scala
index 312797538..c5124ab5f 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/Docker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/Docker.scala
@@ -7,17 +7,45 @@ import izumi.distage.docker.healthcheck.ContainerHealthCheck
 import izumi.fundamentals.collections.nonempty.NonEmptyList
 import pureconfig.ConfigReader
 
+import java.net.{Inet4Address, Inet6Address, InetAddress}
 import java.util.concurrent.TimeUnit
 import scala.concurrent.duration.FiniteDuration
+import scala.util.{Success, Try}
 
 object Docker {
-  final case class AvailablePort(hostV4: String, port: Int)
+  final case class AvailablePort(host: ServiceHost, port: Int)
   object AvailablePort {
-    def local(port: Int): AvailablePort = hostPort("127.0.0.1", port)
-    def hostPort(host: String, port: Int): AvailablePort = AvailablePort(host, port)
+    def local(port: Int): AvailablePort = hostPort(ServiceHost.local, port)
+    def hostPort(host: ServiceHost, port: Int): AvailablePort = AvailablePort(host, port)
   }
 
-  final case class ServicePort(listenOnV4: String, port: Int)
+  final case class ServicePort(host: ServiceHost, port: Int)
+
+  sealed trait ServiceHost {
+    def address: InetAddress
+    def host: String
+    override def toString: String = address.getHostAddress
+  }
+  object ServiceHost {
+    def zeroAddresses: Set[ServiceHost] = {
+      Set(ServiceHost("0.0.0.0"), ServiceHost("::")).flatten
+    }
+    def local: ServiceHost = (InetAddress.getLocalHost: @unchecked) match {
+      case address: Inet4Address => IPv4(address)
+      case address: Inet6Address => IPv6(address)
+    }
+    def apply(hostStr: String): Option[ServiceHost] = Try(InetAddress.getByName(hostStr)) match {
+      case Success(address: Inet4Address) => Some(IPv4(address))
+      case Success(address: Inet6Address) => Some(IPv6(address))
+      case _ => None
+    }
+    final case class IPv4(address: Inet4Address) extends ServiceHost {
+      override def host: String = address.getHostAddress
+    }
+    final case class IPv6(address: Inet6Address) extends ServiceHost {
+      override def host: String = s"[${address.getHostAddress}]"
+    }
+  }
 
   final case class ContainerId(name: String) extends AnyVal {
     override def toString: String = name
@@ -218,10 +246,10 @@ object Docker {
 
   final case class ReportedContainerConnectivity(
     dockerHost: Option[String],
-    containerAddressesV4: Seq[String],
+    containerAddresses: Seq[ServiceHost],
     dockerPorts: Map[DockerPort, NonEmptyList[ServicePort]],
   ) {
-    override def toString: String = s"{host: $dockerHost; addresses=$containerAddressesV4; ports=$dockerPorts}"
+    override def toString: String = s"{host: $dockerHost; addresses=$containerAddresses; ports=$dockerPorts}"
   }
 
   sealed trait ContainerState
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/ContainerHealthCheckBase.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/ContainerHealthCheckBase.scala
index f5605272e..2ae8ae39c 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/ContainerHealthCheckBase.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/ContainerHealthCheckBase.scala
@@ -38,7 +38,7 @@ abstract class ContainerHealthCheckBase[Tag] extends ContainerHealthCheck[Tag] {
 
   protected def findContainerInternalCandidates[T <: DockerPort](container: DockerContainer[Tag], ports: Map[T, NonEmptyList[ServicePort]]): Seq[PortCandidate[T]] = {
     val labels = container.labels
-    val addresses = container.connectivity.containerAddressesV4
+    val addresses = container.connectivity.containerAddresses
     ports.toSeq.flatMap {
       case (mappedPort, _) =>
         // if we have dynamic port then we will try find mapped port number in container labels
@@ -63,8 +63,8 @@ abstract class ContainerHealthCheckBase[Tag] extends ContainerHealthCheck[Tag] {
           .flatMap {
             ib =>
               List(
-                PortCandidate(mappedPort, AvailablePort(container.connectivity.dockerHost.getOrElse("127.0.0.1"), ib.port)),
-                PortCandidate(mappedPort, AvailablePort(ib.listenOnV4, ib.port)),
+                PortCandidate(mappedPort, AvailablePort(container.connectivity.dockerHost.flatMap(ServiceHost(_)).getOrElse(ServiceHost.local), ib.port)),
+                PortCandidate(mappedPort, AvailablePort(ib.host, ib.port)),
               )
           }
     }
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/HttpGetCheck.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/HttpGetCheck.scala
index 65a6afabd..89990318f 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/HttpGetCheck.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/HttpGetCheck.scala
@@ -1,11 +1,12 @@
 package izumi.distage.docker.healthcheck
 
-import java.net.{HttpURLConnection, URL}
 import izumi.distage.docker.Docker.{ContainerState, DockerPort}
 import izumi.distage.docker.DockerContainer
 import izumi.distage.docker.healthcheck.ContainerHealthCheck.HealthCheckResult
 import izumi.logstage.api.IzLogger
 
+import java.net.{HttpURLConnection, URL}
+
 final class HttpGetCheck[Tag](
   portStatus: HealthCheckResult.GoodOnPorts,
   port: DockerPort,
@@ -16,7 +17,7 @@ final class HttpGetCheck[Tag](
       portStatus.availablePorts.firstOption(port) match {
         case Some(availablePort) if portStatus.allTCPPortsAccessible =>
           val protocol = if (useHttps) "https" else "http"
-          val url = new URL(s"$protocol://${availablePort.hostV4}:${availablePort.port}")
+          val url = new URL(s"$protocol://${availablePort.host.host}:${availablePort.port}")
           logger.info(s"Checking docker port $port via $url for $container. Will try to establish HTTP connection.")
           try {
             val connection = url.openConnection().asInstanceOf[HttpURLConnection]
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/PostgreSqlProtocolCheck.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/PostgreSqlProtocolCheck.scala
index 25c41ea62..e293fadfa 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/PostgreSqlProtocolCheck.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/PostgreSqlProtocolCheck.scala
@@ -22,7 +22,7 @@ final class PostgreSqlProtocolCheck[Tag](
           val startupMessage = genStartupMessage()
           val socket = new Socket()
           try {
-            socket.connect(new InetSocketAddress(availablePort.hostV4, availablePort.port), container.containerConfig.portProbeTimeout.toMillis.toInt)
+            socket.connect(new InetSocketAddress(availablePort.host.address, availablePort.port), container.containerConfig.portProbeTimeout.toMillis.toInt)
             logger.info(s"Checking PostgreSQL protocol on $port for $container. ${startupMessage.toIterable.toHex -> "Startup message"}.")
             val out = socket.getOutputStream
             val in = socket.getInputStream
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/TCPContainerHealthCheck.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/TCPContainerHealthCheck.scala
index 8f1271ba5..1c6f18f14 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/TCPContainerHealthCheck.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/healthcheck/TCPContainerHealthCheck.scala
@@ -25,13 +25,13 @@ class TCPContainerHealthCheck[Tag] extends ContainerHealthCheckBase[Tag] {
     val containerCandidates = findContainerInternalCandidates(container, tcpPorts)
 
     val allCandidates = (dockerHostCandidates ++ containerCandidates).distinct
-      .filterNot(_.maybeAvailable.hostV4 == "0.0.0.0")
+      .filterNot(c => ServiceHost.zeroAddresses.contains(c.maybeAvailable.host))
 
     logger.debug(s"going to check ports on $container: ${allCandidates.map { case PortCandidate(k, v) => s"if $k is available at $v" }.niceList() -> "port mappings"}")
 
     val checks = allCandidates.map {
       case PortCandidate(dp, ap) =>
-        check.checkPort(ap.hostV4, ap.port, s"open port ${ap.hostV4}:${ap.port} for ${container.id}") match {
+        check.checkAddressPort(ap.host.address, ap.port, s"open port ${ap.host}:${ap.port} for ${container.id}") match {
           case ResourceCheck.Success() =>
             Right(GoodPort(dp, ap))
           case ResourceCheck.ResourceUnavailable(_, cause) =>
diff --git a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/integration/PortCheck.scala b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/integration/PortCheck.scala
index ac746110a..bde1f89c9 100644
--- a/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/integration/PortCheck.scala
+++ b/fundamentals/fundamentals-platform/src/main/scala/izumi/fundamentals/platform/integration/PortCheck.scala
@@ -1,7 +1,6 @@
 package izumi.fundamentals.platform.integration
 
-import java.net.{InetSocketAddress, Socket, URI, URL}
-
+import java.net._
 import scala.concurrent.duration._
 
 /**
@@ -14,18 +13,12 @@ import scala.concurrent.duration._
   */
 class PortCheck(timeout: FiniteDuration) {
 
-  @deprecated(
-    "PortCheck constructor now accepts FiniteDuration instead of Int of milliseconds. Rewrite from `new PortCheck(100)` to `new PortCheck(100.millis)`",
-    "Int constructor will be removed in 0.11.1",
-  )
-  def this(timeoutMillis: Int) = this(timeoutMillis.millis)
-
-  def checkUrl(uri: URL, clue: String, defaultPort: Int): ResourceCheck = {
-    checkUrl(uri, Some(clue), Some(defaultPort))
+  def checkUrl(url: URL, clue: String, defaultPort: Int): ResourceCheck = {
+    checkUrl(url, Some(clue), Some(defaultPort))
   }
 
-  def checkUrl(uri: URL, clue: String): ResourceCheck = {
-    checkUrl(uri, Some(clue), None)
+  def checkUrl(url: URL, clue: String): ResourceCheck = {
+    checkUrl(url, Some(clue), None)
   }
 
   def checkUri(uri: URI, defaultPort: Int, clue: String): ResourceCheck = {
@@ -36,9 +29,13 @@ class PortCheck(timeout: FiniteDuration) {
     checkPort(host, port, Some(clue))
   }
 
-  def checkUrl(uri: URL, clue: Option[String] = None, defaultPort: Option[Int] = None): ResourceCheck = {
-    val portOrDefault: Int = portFor(uri.getDefaultPort, defaultPort, uri.getPort)
-    checkPort(uri.getHost, portOrDefault, clue)
+  def checkAddressPort(address: InetAddress, port: Int, clue: String): ResourceCheck = {
+    checkAddress(new InetSocketAddress(address, port), Some(clue))
+  }
+
+  def checkUrl(url: URL, clue: Option[String] = None, defaultPort: Option[Int] = None): ResourceCheck = {
+    val portOrDefault: Int = portFor(url.getDefaultPort, defaultPort, url.getPort)
+    checkPort(url.getHost, portOrDefault, clue)
   }
 
   def checkUri(uri: URI, defaultPort: Int, clue: Option[String] = None): ResourceCheck = {
@@ -47,27 +44,45 @@ class PortCheck(timeout: FiniteDuration) {
   }
 
   def checkPort(host: String, port: Int, clue: Option[String] = None): ResourceCheck = {
+    checkAddress(new InetSocketAddress(host, port), clue)
+  }
+
+  def checkAddressPort(address: InetAddress, port: Int, clue: Option[String] = None): ResourceCheck = {
+    checkAddress(new InetSocketAddress(address, port), clue)
+  }
+
+  def checkAddress(address: => InetSocketAddress, clue: Option[String] = None): ResourceCheck = {
     try {
-      val socket = new Socket()
+      val evaluatedAddress = address
       try {
-        socket.connect(new InetSocketAddress(host, port), timeout.toMillis.toInt)
-        ResourceCheck.Success()
-      } finally {
-        socket.close()
+        val socket = new Socket()
+        try {
+          socket.connect(evaluatedAddress, timeout.toMillis.toInt)
+          ResourceCheck.Success()
+        } finally {
+          socket.close()
+        }
+      } catch {
+        case t: Throwable =>
+          val message = errorMessage(s"${evaluatedAddress.getHostName}:${evaluatedAddress.getPort}, timeout: $timeout", clue)
+          ResourceCheck.ResourceUnavailable(message, Some(t))
       }
     } catch {
       case t: Throwable =>
-        val message = clue match {
-          case Some(_) =>
-            s"$clue: port check failed on $host:$port, timeout: $timeout"
-          case None =>
-            s"Port check failed on $host:$port, timeout: $timeout"
-        }
-
+        val message = errorMessage(s"unknown address, timeout: $timeout", clue)
         ResourceCheck.ResourceUnavailable(message, Some(t))
     }
   }
 
+  private def errorMessage(ctx: String, mbClue: Option[String]): String = {
+    mbClue match {
+      case Some(clue) =>
+        s"Port check of $clue failed on $ctx"
+      case None =>
+        s"Port check failed on $ctx"
+    }
+  }
+
   private def portFor(uriPort: => Int, defaultPort: Option[Int], port: Int) = {
     val portOrDefault = port match {
       case -1 =>


Test patch:
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
index dbfbd7394..8c693a1b7 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -24,8 +24,8 @@ class PgSvcExample(
 ) extends IntegrationCheck[Task] {
   override def resourcesAvailable(): Task[ResourceCheck] = Task.effect {
     val portCheck = new PortCheck(10.milliseconds)
-    portCheck.checkPort(pg.hostV4, pg.port)
-    portCheck.checkPort(pgfw.hostV4, pgfw.port)
+    portCheck.checkAddressPort(pg.host.address, pg.port)
+    portCheck.checkAddressPort(pg.host.address, pgfw.port)
   }
 }