Repository: 7mind/izumi
Pull request #738 (2019-11-19 22:51:51)

Update sbt-scoverage to 1.6.1
Updates [org.scoverage:sbt-scoverage](https://github.com/scoverage/sbt-scoverage) [from 1.6.0 to 1.6.1](https://github.com/scoverage/sbt-scoverage/compare/v1.6.0...v1.6.1).
[Release Notes/Changelog](https://github.com/scoverage/sbt-scoverage/releases/tag/v1.6.1)

I'll automatically update this PR to resolve conflicts as long as you don't change it yourself.

If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.

Have a fantastic day writing Scala!

<details>
<summary>Ignore future updates</summary>

Add this to your `.scala-steward.conf` file to ignore future updates of this dependency:
```
updates.ignore = [ { groupId = "org.scoverage", artifactId = "sbt-scoverage" } ]
```
</details>

labels: semver-patch


Patch:
diff --git a/build.sbt b/build.sbt
index f85b254f4..31fa89c81 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1209,6 +1209,7 @@ lazy val `distage-testkit` = project.in(file("distage/distage-testkit"))
       "org.scala-lang.modules" %% "scala-collection-compat" % V.collection_compat,
       "org.scalatest" %% "scalatest" % V.scalatest % Test,
       "org.scalatest" %% "scalatest" % V.scalatest,
+      "com.github.docker-java" % "docker-java" % V.docker_java,
       "org.typelevel" %% "cats-core" % V.cats % Optional,
       "org.typelevel" %% "cats-effect" % V.cats_effect % Optional,
       "dev.zio" %% "zio" % V.zio % Optional
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
index e952aa178..662a29f39 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/monadic/DIEffectAsync.scala
@@ -6,8 +6,8 @@ import izumi.distage.model.monadic.LowPriorityDIEffectAsyncInstances.{_Parallel,
 import izumi.functional.bio.{BIOAsync, F}
 import izumi.fundamentals.platform.functional.Identity
 
-import scala.concurrent.duration.FiniteDuration
-import scala.concurrent.{ExecutionContext, Future}
+import scala.concurrent.duration.{Duration, FiniteDuration}
+import scala.concurrent.{Await, ExecutionContext, Future}
 
 trait DIEffectAsync[F[_]] {
   def parTraverse_[A](l: Iterable[A])(f: A => F[Unit]): F[Unit]
@@ -20,7 +20,8 @@ object DIEffectAsync extends LowPriorityDIEffectAsyncInstances {
   implicit val diEffectParIdentity: DIEffectAsync[Identity] = {
     new DIEffectAsync[Identity] {
       override def parTraverse_[A](l: Iterable[A])(f: A => Unit): Unit = {
-        l.foreach(a => Future(f(a))(ExecutionContext.global))
+        val future = Future.sequence(l.map(a => Future(f(a))(ExecutionContext.global)))(implicitly, ExecutionContext.global)
+        Await.result(future, Duration.Inf)
       }
       override def sleep(duration: FiniteDuration): Identity[Unit] = {
         Thread.sleep(duration.toMillis)
diff --git a/distage/distage-model/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala b/distage/distage-model/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
index b4a8a76cc..108c435d4 100644
--- a/distage/distage-model/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
+++ b/distage/distage-model/src/main/scala/izumi/distage/model/providers/ProviderMagnet.scala
@@ -78,16 +78,26 @@ import scala.language.implicitConversions
   *
   * @see [[izumi.distage.model.reflection.macros.ProviderMagnetMacro]]
   */
-final case class ProviderMagnet[+R](get: Provider) {
-  def map[B: Tag](f: R => B): ProviderMagnet[B] = {
-    copy[B](get = get.unsafeMap(SafeType.get[B], (any: Any) => f(any.asInstanceOf[R])))
+final case class ProviderMagnet[+A](get: Provider) {
+  def map[B: Tag](f: A => B): ProviderMagnet[B] = {
+    copy[B](get = get.unsafeMap(SafeType.get[B], (any: Any) => f(any.asInstanceOf[A])))
   }
 
-  def zip[B: Tag](that: ProviderMagnet[B]): ProviderMagnet[(R, B)] = {
-    implicit val rTag: Tag[R] = Tag.unsafeFromSafeType(Tag[B].tpe.mirror)(get.ret)
+  def zip[B: Tag](that: ProviderMagnet[B]): ProviderMagnet[(A, B)] = {
+    implicit val rTag: Tag[A] = Tag.unsafeFromSafeType(Tag[B].tpe.mirror)(get.ret)
     rTag.discard() // scalac can't detect usage in TagMacro assembling Tag[(R, B)] below
 
-    copy[(R, B)](get = get.unsafeZip(SafeType.get[(R, B)], that.get))
+    copy[(A, B)](get = get.unsafeZip(SafeType.get[(A, B)], that.get))
+  }
+
+  def map2[B: Tag, C: Tag](that: ProviderMagnet[B])(f: (A, B) => C): ProviderMagnet[C] = {
+    zip(that).map[C](f.tupled)
+  }
+
+  /** Add `B` as an unused dependency for this constructor */
+  def addDependency[B: Tag]: ProviderMagnet[A] = {
+    implicit val rTag: Tag[A] = Tag.unsafeFromSafeType(Tag[B].tpe.mirror)(get.ret)
+    map2[B, A](ProviderMagnet.identity[B])((a, _) => a)
   }
 }
 
diff --git a/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala b/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
index 37e1a32c9..283206cb2 100644
--- a/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
+++ b/fundamentals/fundamentals-functional/src/main/scala/izumi/functional/Value.scala
@@ -5,6 +5,23 @@ final class Value[A] private (private val value: A) extends AnyVal {
     new Value(f(this.value))
   }
 
+  @inline def mut[C, B](context: Option[C])(f: (C, A) => A): Value[A] = {
+    context match {
+      case Some(ctx) =>
+        new Value(f(ctx, this.value))
+      case None =>
+        this
+    }
+  }
+
+  @inline def mut[B](cond: Boolean)(f: A => A): Value[A] = {
+    if (cond) {
+      new Value(f(this.value))
+    } else {
+      this
+    }
+  }
+
   @inline def eff(f: A => Unit): Value[A] = {
     f(value)
     this
diff --git a/project/Deps.sc b/project/Deps.sc
index 9cc28047a..4524c015a 100644
--- a/project/Deps.sc
+++ b/project/Deps.sc
@@ -27,6 +27,7 @@ object Izumi {
     val typesafe_config = Version.VExpr("V.typesafe_config")
     val cglib_nodep = Version.VExpr("V.cglib_nodep")
     val scala_java_time = Version.VExpr("V.scala_java_time")
+    val docker_java = Version.VExpr("V.docker_java")
   }
 
   object PV {
@@ -119,6 +120,7 @@ object Izumi {
     val http4s_all = (http4s_server ++ http4s_client)
 
     val asynchttpclient = Library("org.asynchttpclient", "async-http-client", V.asynchttpclient, LibraryType.Invariant)
+    val docker_java = Library("com.github.docker-java", "docker-java", V.docker_java, LibraryType.Invariant)
   }
 
   import Deps._
@@ -439,7 +441,7 @@ object Izumi {
       ),
       Artifact(
         name = Projects.distage.testkit,
-        libs = Seq(scalatest.dependency in Scope.Compile.all) ++ allMonadsOptional,
+        libs = Seq(scalatest.dependency, docker_java).map(_ in Scope.Compile.all) ++ allMonadsOptional,
         depends =
           Seq(Projects.distage.config, Projects.distage.roles, Projects.logstage.di).map(_ in Scope.Compile.all) ++
             Seq(Projects.distage.core, Projects.distage.plugins).map(_ tin Scope.Compile.all),
diff --git a/project/Versions.scala b/project/Versions.scala
index 3f1081b58..d37cae3e8 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -36,6 +36,6 @@ object V {
 
   // good to drop - java
   val cglib_nodep = "3.3.0"
-
   val scala_java_time = "2.0.0-RC3"
+  val docker_java = "3.2.0-rc1"
 }
diff --git a/project/project/PluginVersions.scala b/project/project/PluginVersions.scala
index 8a4bc0c15..ff7656132 100644
--- a/project/project/PluginVersions.scala
+++ b/project/project/PluginVersions.scala
@@ -4,7 +4,7 @@ object PV {
   val sbt_ghpages = "0.6.3"
   val sbt_site = "1.3.3"
   val sbt_unidoc = "0.4.2"
-  val sbt_scoverage = "1.6.0"
+  val sbt_scoverage = "1.6.1"
   val sbt_pgp = "2.0.1-M3"
   val sbt_assembly = "0.14.9"
 


Test patch:
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/Docker.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/Docker.scala
new file mode 100644
index 000000000..84df7b6fd
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/Docker.scala
@@ -0,0 +1,78 @@
+package izumi.distage.testkit.integration.docker
+
+import java.util.concurrent.TimeUnit
+
+import izumi.fundamentals.platform.integration.{PortCheck, ResourceCheck}
+
+import scala.concurrent.duration.FiniteDuration
+
+object Docker {
+
+  final case class ContainerId(name: String) extends AnyVal
+
+  trait DockerPort {
+    def number: Int
+  }
+  object DockerPort {
+    final case class TCP(number: Int) extends DockerPort
+  }
+
+  final case class ClientConfig(readTimeoutMs: Int, connectTimeoutMs: Int)
+
+  sealed trait HealthCheckResult
+  object HealthCheckResult {
+    case object Running extends HealthCheckResult
+    case object Uknnown extends HealthCheckResult
+    final case class Failed(t: Throwable) extends HealthCheckResult
+  }
+
+  trait ContainerHealthCheck[Tag] {
+    def check(container: DockerContainer[Tag]): HealthCheckResult
+  }
+  object ContainerHealthCheck {
+    def dontCheckPorts[T]: ContainerHealthCheck[T] = _ => HealthCheckResult.Running
+
+    def checkFirstPort[T](timeout: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS)): ContainerHealthCheck[T] = {
+      container: DockerContainer[T] =>
+        container.containerConfig.ports.headOption match {
+          case Some(value) =>
+            checkPort[T](value, timeout).check(container)
+          case None =>
+            HealthCheckResult.Running
+        }
+    }
+
+    def checkPort[T](exposedPort: DockerPort, timeout: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS)): ContainerHealthCheck[T] = {
+      container: DockerContainer[T] =>
+        container.mapping.get(exposedPort) match {
+          case Some(value) =>
+            new PortCheck(timeout.toMillis.intValue()).checkPort("localhost", value, s"open port ${exposedPort} on ${container.id}") match {
+              case _: ResourceCheck.Success =>
+                HealthCheckResult.Running
+              case f: ResourceCheck.Failure =>
+                println(f)
+                HealthCheckResult.Uknnown
+            }
+          case None =>
+            HealthCheckResult.Failed(new RuntimeException(s"Port ${exposedPort} is not mapped!"))
+        }
+    }
+  }
+
+  final case class Mount(host: String, container: String)
+
+  final case class ContainerConfig[T](
+                                       image: String,
+                                       ports: Seq[DockerPort],
+                                       env: Map[String, String] = Map.empty,
+                                       cmd: Seq[String] = Seq.empty,
+                                       entrypoint: Seq[String] = Seq.empty,
+                                       cwd: Option[String] = None,
+                                       user: Option[String] = None,
+                                       mounts: Seq[Mount] = Seq.empty,
+                                       healthCheckInterval: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS),
+                                       pullTimeout: FiniteDuration = FiniteDuration(120, TimeUnit.SECONDS),
+                                       healthCheck: ContainerHealthCheck[T] = ContainerHealthCheck.checkFirstPort[T](),
+                                     )
+
+}
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerClientWrapper.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerClientWrapper.scala
new file mode 100644
index 000000000..eedd4a3f7
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerClientWrapper.scala
@@ -0,0 +1,92 @@
+package izumi.distage.testkit.integration.docker
+
+import java.util.UUID
+
+import com.github.dockerjava.api.DockerClient
+import com.github.dockerjava.api.command.DockerCmdExecFactory
+import com.github.dockerjava.core.{DefaultDockerClientConfig, DockerClientBuilder}
+import izumi.distage.model.definition.DIResource
+import izumi.distage.model.monadic.DIEffect.syntax._
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync}
+import izumi.distage.roles.model.IntegrationCheck
+import izumi.distage.testkit.integration.docker.Docker.ContainerId
+import izumi.fundamentals.platform.integration.ResourceCheck
+import izumi.fundamentals.platform.language.Quirks._
+import izumi.logstage.api.IzLogger
+
+import scala.jdk.CollectionConverters._
+
+class DockerClientWrapper[F[_]]
+(
+  val client: DockerClient,
+  val labels: Map[String, String],
+  logger: IzLogger,
+)(
+  implicit
+  F: DIEffect[F],
+) {
+  def destroyContainer(container: ContainerId): F[Unit] = {
+    F.maybeSuspend {
+      logger.info(s"Going to destroy $container...")
+
+      try {
+        client
+          .stopContainerCmd(container.name)
+          .exec()
+          .discard()
+      } finally {
+        client
+          .removeContainerCmd(container.name)
+          .withForce(true)
+          .exec()
+          .discard()
+      }
+    }
+  }
+}
+
+object DockerClientWrapper {
+
+  class Resource[F[_]: DIEffect: DIEffectAsync]
+  (
+    factory: DockerCmdExecFactory,
+    logger: IzLogger,
+  ) extends DIResource[F, DockerClientWrapper[F]]
+    with IntegrationCheck {
+
+    private[this] lazy val dcc = DefaultDockerClientConfig.createDefaultConfigBuilder().build()
+    private[this] lazy val client = DockerClientBuilder
+      .getInstance(dcc)
+      .withDockerCmdExecFactory(factory)
+      .build
+
+    override def resourcesAvailable(): ResourceCheck = {
+      try {
+        client.infoCmd().exec()
+        ResourceCheck.Success()
+      } catch {
+        case t: Throwable =>
+          ResourceCheck.ResourceUnavailable("Docker daemon is unavailable", Some(t))
+      }
+    }
+
+    override def acquire: F[DockerClientWrapper[F]] = {
+      DIEffect[F].maybeSuspend {
+        new DockerClientWrapper[F](
+          client = client,
+          labels = Map("type" -> "distage-testkit", "distage-run" -> UUID.randomUUID().toString),
+          logger = logger,
+        )
+      }
+    }
+
+    override def release(resource: DockerClientWrapper[F]): F[Unit] = {
+      for {
+        containers <- DIEffect[F].maybeSuspend(resource.client.listContainersCmd().withLabelFilter(resource.labels.asJava).exec())
+        _ <- DIEffect[F].traverse_(containers.asScala)(c => resource.destroyContainer(ContainerId(c.getId)))
+        _ <- DIEffect[F].maybeSuspend(resource.client.close())
+      } yield ()
+    }
+  }
+
+}
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerCmdExecFactoryResource.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerCmdExecFactoryResource.scala
new file mode 100644
index 000000000..7ced182b4
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerCmdExecFactoryResource.scala
@@ -0,0 +1,17 @@
+package izumi.distage.testkit.integration.docker
+
+import com.github.dockerjava.api.command.DockerCmdExecFactory
+import com.github.dockerjava.netty.NettyDockerCmdExecFactory
+import izumi.distage.model.definition.DIResource
+import izumi.distage.model.monadic.DIEffect
+
+class DockerCmdExecFactoryResource[F[_]: DIEffect]
+(
+  config: Docker.ClientConfig,
+) extends DIResource.FromAutoCloseable[F, DockerCmdExecFactory](
+  DIEffect[F].maybeSuspend {
+    new NettyDockerCmdExecFactory()
+      .withReadTimeout(config.readTimeoutMs)
+      .withConnectTimeout(config.connectTimeoutMs)
+  }
+)
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerContainer.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerContainer.scala
new file mode 100644
index 000000000..022d088d2
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/DockerContainer.scala
@@ -0,0 +1,162 @@
+package izumi.distage.testkit.integration.docker
+
+import java.util.concurrent.TimeUnit
+
+import com.github.dockerjava.api.model._
+import com.github.dockerjava.core.command.PullImageResultCallback
+import distage.TagK
+import izumi.distage.model.definition.DIResource
+import izumi.distage.model.monadic.DIEffect.syntax._
+import izumi.distage.model.monadic.{DIEffect, DIEffectAsync}
+import izumi.distage.model.providers.ProviderMagnet
+import izumi.distage.testkit.integration.docker.Docker.{ContainerConfig, ContainerId, DockerPort, HealthCheckResult}
+import izumi.functional.Value
+import izumi.fundamentals.platform.language.Quirks._
+import izumi.fundamentals.platform.network.IzSockets
+import izumi.logstage.api.IzLogger
+
+trait ContainerDef {
+  type Tag
+  type Container = DockerContainer[Tag]
+  type Config = ContainerConfig[Tag]
+
+  def config: Config
+
+  /** For binding in `ModuleDef`:
+    *
+    * {{{
+    * object KafkaDocker extends ContainerDef
+    * object ZookeeperDocker extends ContainerDef
+    *
+    * make[KafkaDocker.Container].fromResource {
+    *   KafkaDocker
+    *     .make[F]
+    *     .dependOnDocker(ZookeeperDocker)
+    * }
+    * }}}
+    */
+  final def make[F[_]: TagK](implicit tag: distage.Tag[Tag]): ProviderMagnet[DIResource[F, DockerContainer[Tag]]] = {
+    tag.discard()
+    DockerContainer.resource[F](this)
+  }
+}
+object ContainerDef {
+  type Aux[T] = ContainerDef {type Tag = T}
+}
+
+case class DockerContainer[Tag](
+                                 id: Docker.ContainerId,
+                                 mapping: Map[Docker.DockerPort, Int],
+                                 containerConfig: ContainerConfig[Tag],
+                               )
+object DockerContainer {
+  def resource[F[_]](conf: ContainerDef): (DockerClientWrapper[F], IzLogger, DIEffect[F], DIEffectAsync[F]) => DIResource[F, DockerContainer[conf.Tag]] = {
+    new Resource[F, conf.Tag](conf)(_, _)(_, _)
+  }
+
+  implicit final class DockerProviderExtensions[F[_], T](private val self: ProviderMagnet[DIResource[F, DockerContainer[T]]]) extends AnyVal {
+    def dependOnDocker(containerDecl: ContainerDef)(implicit tag: distage.Tag[DockerContainer[containerDecl.Tag]]): ProviderMagnet[DIResource[F, DockerContainer[T]]] = {
+      self.addDependency[DockerContainer[containerDecl.Tag]]
+    }
+    def dependOnDocker[T2](implicit tag: distage.Tag[DockerContainer[T2]]): ProviderMagnet[DIResource[F, DockerContainer[T]]] = {
+      self.addDependency[DockerContainer[T2]]
+    }
+  }
+
+  class Resource[F[_]: DIEffect: DIEffectAsync, T]
+  (
+    containerDecl: ContainerDef.Aux[T],
+  )(
+    clientw: DockerClientWrapper[F],
+    logger: IzLogger,
+  ) extends DIResource[F, DockerContainer[T]] {
+    private val client = clientw.client
+    private val config = containerDecl.config
+
+    override def acquire: F[DockerContainer[T]] = {
+      val ports = config.ports.map(_ -> IzSockets.temporaryLocalPort())
+
+      val specs = ports.map {
+        case (container, local) =>
+
+          val p = container match {
+            case DockerPort.TCP(number) =>
+              ExposedPort.tcp(number)
+          }
+          new PortBinding(Ports.Binding.bindPort(local), p)
+      }
+
+      val exposed = config.ports.map {
+        case DockerPort.TCP(number) =>
+          ExposedPort.tcp(number)
+      }
+      import scala.jdk.CollectionConverters._
+
+      val baseCmd = client
+        .createContainerCmd(config.image)
+        .withLabels(clientw.labels.asJava)
+
+      val volumes = config.mounts.map(m => new Bind(m.host, new Volume(m.container), true))
+
+      def await(c: DockerContainer[T]): F[Unit] = {
+        DIEffect[F].maybeSuspend {
+          try {
+            val status = client.inspectContainerCmd(c.id.name).exec()
+            if (status.getState.getRunning) {
+              config.healthCheck.check(c)
+            } else {
+              HealthCheckResult.Failed(new RuntimeException(s"Container exited: ${c.id}, full status: $status"))
+            }
+          } catch {
+            case t: Throwable =>
+              HealthCheckResult.Failed(t)
+          }
+        }.flatMap {
+          case HealthCheckResult.Running =>
+            DIEffect[F].unit
+          case HealthCheckResult.Failed(t) =>
+            DIEffect[F].fail(new RuntimeException(s"Container failed: ${c.id}", t))
+          case HealthCheckResult.Uknnown =>
+            DIEffectAsync[F].sleep(config.healthCheckInterval).flatMap(_ => await(c))
+        }
+      }
+
+      for {
+        out <- DIEffect[F].maybeSuspend {
+          val cmd = Value(baseCmd)
+            .mut(exposed.nonEmpty)(_.withExposedPorts(exposed: _*))
+            .mut(specs.nonEmpty)(_.withPortBindings(specs: _*))
+            .mut(config.env.nonEmpty)(_.withEnv(config.env.map {
+              case (k, v) => s"$k=$v"
+            }.toList.asJava))
+            .mut(config.cmd.nonEmpty)(_.withCmd(config.cmd.toList.asJava))
+            .mut(config.entrypoint.nonEmpty)(_.withEntrypoint(config.cmd.toList.asJava))
+            .mut(config.cwd)((cwd, cmd) => cmd.withWorkingDir(cwd))
+            .mut(config.user)((user, cmd) => cmd.withUser(user))
+            .mut(volumes.nonEmpty)(_.withVolumes(volumes.map(_.getVolume).asJava))
+            .mut(volumes.nonEmpty)(_.withBinds(volumes.toList.asJava))
+            .get
+
+          logger.info(s"Going to pull `${config.image}`...")
+          client
+            .pullImageCmd(config.image)
+            .exec(new PullImageResultCallback())
+            .awaitCompletion(config.pullTimeout.toMillis, TimeUnit.MILLISECONDS);
+
+          logger.debug(s"Going to create container from image `${config.image}`...")
+          val res = cmd.exec()
+          logger.info(s"Created container from `${config.image}` with ${res.getId -> "id"} ...")
+
+          client.startContainerCmd(res.getId).exec()
+
+          DockerContainer[T](ContainerId(res.getId), ports.toMap, config)
+        }
+        _ <- await(out)
+      } yield out
+    }
+
+    override def release(resource: DockerContainer[T]): F[Unit] = {
+      clientw.destroyContainer(resource.id)
+    }
+  }
+}
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/DynamoDocker.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/DynamoDocker.scala
new file mode 100644
index 000000000..55ba74bd8
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/DynamoDocker.scala
@@ -0,0 +1,22 @@
+package izumi.distage.testkit.integration.docker.examples
+
+import distage.{ModuleDef, TagK}
+import izumi.distage.testkit.integration.docker.ContainerDef
+import izumi.distage.testkit.integration.docker.Docker.{ContainerConfig, DockerPort}
+
+object DynamoDocker extends ContainerDef {
+  val primaryPort: DockerPort = DockerPort.TCP(8042)
+
+  override def config: Config = {
+    ContainerConfig(
+      image = "amazon/dynamodb-local:latest",
+      ports = Seq(primaryPort),
+    )
+  }
+}
+
+class DynamoDockerModule[F[_]: TagK] extends ModuleDef {
+  make[DynamoDocker.Container].fromResource {
+    DynamoDocker.make[F]
+  }
+}
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/PostgresDocker.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/PostgresDocker.scala
new file mode 100644
index 000000000..4da8a0507
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/examples/PostgresDocker.scala
@@ -0,0 +1,23 @@
+package izumi.distage.testkit.integration.docker.examples
+
+import izumi.distage.model.definition.ModuleDef
+import izumi.distage.testkit.integration.docker.ContainerDef
+import izumi.distage.testkit.integration.docker.Docker.{ContainerConfig, DockerPort}
+import zio.Task
+
+object PostgresDocker extends ContainerDef {
+  val primaryPort: DockerPort = DockerPort.TCP(5432)
+
+  override def config: Config = {
+    ContainerConfig(
+      image = "library/postgres:latest",
+      ports = Seq(primaryPort),
+    )
+  }
+}
+
+class PostgresDockerModule extends ModuleDef {
+  make[PostgresDocker.Container].fromResource {
+    PostgresDocker.make[Task]
+  }
+}
diff --git a/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/modules/DockerContainerModule.scala b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/modules/DockerContainerModule.scala
new file mode 100644
index 000000000..ee7c83145
--- /dev/null
+++ b/distage/distage-testkit/src/main/scala/izumi/distage/testkit/integration/docker/modules/DockerContainerModule.scala
@@ -0,0 +1,26 @@
+package izumi.distage.testkit.integration.docker.modules
+
+import com.github.dockerjava.api.command.DockerCmdExecFactory
+import distage.TagK
+import izumi.distage.config.annotations.ConfPath
+import izumi.distage.model.definition.ModuleDef
+import izumi.distage.model.monadic.DIEffect
+import izumi.distage.testkit.integration.docker.{Docker, DockerClientWrapper, DockerCmdExecFactoryResource}
+
+class DockerContainerModule[F[_]: DIEffect: TagK] extends ModuleDef {
+  // expose DockerClientWrapper.Resource to execute its IntegrationCheck https://github.com/7mind/izumi/issues/563
+  make[DockerClientWrapper.Resource[F]]
+  make[DockerClientWrapper[F]].refResource[DockerClientWrapper.Resource[F]]
+  make[DockerCmdExecFactory].fromResource[DockerCmdExecFactoryResource[F]]
+
+  include(DockerContainerModule.config)
+}
+
+object DockerContainerModule {
+  final val config = new ModuleDef {
+    make[Docker.ClientConfig].from {
+      conf: Docker.ClientConfig @ConfPath("docker") =>
+        conf
+    }
+  }
+}
diff --git a/distage/distage-testkit/src/test/resources/common-reference-dev.conf b/distage/distage-testkit/src/test/resources/common-reference-dev.conf
new file mode 100644
index 000000000..3b9f93dd4
--- /dev/null
+++ b/distage/distage-testkit/src/test/resources/common-reference-dev.conf
@@ -0,0 +1,4 @@
+docker {
+  readTimeoutMs = 5000
+  connectTimeoutMs = 1000
+}
\ No newline at end of file
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
index d9cfa0401..a3948e430 100644
--- a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/DistageTestExample.scala
@@ -1,10 +1,13 @@
 package izumi.distage.testkit.distagesuite
 
 import cats.effect.{IO => CIO}
-import distage.DIKey
+import distage._
+import izumi.distage.model.monadic.DIEffect
+import izumi.distage.model.monadic.DIEffect.syntax._
 import izumi.distage.testkit.distagesuite.fixtures.{ApplePaymentProvider, MockCache, MockCachedUserService, MockUserRepository}
 import izumi.distage.testkit.services.st.dtest.{DistageAbstractScalatestSpec, TestConfig}
 import izumi.distage.testkit.st.specs.{DistageBIOSpecScalatest, DistageSpecScalatest}
+import izumi.fundamentals.platform.functional.Identity
 import zio.Task
 
 trait DistageMemoizeExample[F[_]] { this: DistageAbstractScalatestSpec[F] =>
@@ -30,66 +33,39 @@ class DistageTestExampleBIO extends DistageBIOSpecScalatest[zio.IO] with Distage
 
 }
 
-class DistageTestExample extends DistageSpecScalatest[CIO] with DistageMemoizeExample[CIO] {
-  "distage test runner" should {
-    "test 1" in {
-      service: MockUserRepository[CIO] =>
-        for {
-          _ <- CIO.delay(assert(service != null))
-          _ <- CIO.delay(println("test2"))
-        } yield ()
-    }
-
-    "test 2" in {
-      service: MockCachedUserService[CIO] =>
-        for {
-          _ <- CIO.delay(assert(service != null))
-          _ <- CIO.delay(println("test1"))
-        } yield ()
-    }
-
-    "test 3" in {
-      service: MockCachedUserService[CIO] =>
-        CIO.delay(assert(service != null))
-    }
+final class DistageTestExampleCIO extends DistageTestExampleBase[CIO]
+final class DistageTestExampleZIO extends DistageTestExampleBase[Task]
+final class DistageTestExampleId extends DistageTestExampleBase[Identity]
 
-    "test 4 (should be ignored)" in {
-      _: ApplePaymentProvider[CIO] =>
-//        ???
-    }
-  }
-
-}
-
-class DistageTestExample1 extends DistageSpecScalatest[CIO] with DistageMemoizeExample[CIO] {
+abstract class DistageTestExampleBase[F[_]: TagK](implicit F: DIEffect[F]) extends DistageSpecScalatest[F] with DistageMemoizeExample[F] {
 
   "distage test custom runner" should {
     "test 1" in {
-      service: MockUserRepository[CIO] =>
+      service: MockUserRepository[F] =>
         for {
-          _ <- CIO.delay(assert(service != null))
-          _ <- CIO.delay(println("test2"))
+          _ <- F.maybeSuspend(assert(service != null))
+          _ <- F.maybeSuspend(println("test2"))
         } yield ()
     }
 
     "test 2" in {
-      service: MockCachedUserService[CIO] =>
+      service: MockCachedUserService[F] =>
         for {
-          _ <- CIO.delay(assert(service != null))
-          _ <- CIO.delay(println("test1"))
+          _ <- F.maybeSuspend(assert(service != null))
+          _ <- F.maybeSuspend(println("test1"))
         } yield ()
     }
 
     "test 3" in {
-      service: MockCachedUserService[CIO] =>
-        CIO.delay(assert(service != null))
+      service: MockCachedUserService[F] =>
+        F.maybeSuspend(assert(service != null))
     }
 
-    "test 4" in {
-      _: ApplePaymentProvider[CIO] =>
-        //???
+    "test 4 (should be ignored)" in {
+      _: ApplePaymentProvider[F] =>
+        ???
     }
   }
 
-
 }
+
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
index 342d22ccd..8dd949d9f 100644
--- a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/distagesuite/fixtures/Fixtures.scala
@@ -2,19 +2,22 @@ package izumi.distage.testkit.distagesuite.fixtures
 
 import java.util.concurrent.atomic.AtomicInteger
 
-import cats.effect.IO
+import cats.effect.{IO => CIO}
 import distage.TagK
 import izumi.distage.monadic.modules.{CatsDIEffectModule, ZIODIEffectModule}
 import izumi.distage.plugins.PluginDef
 import izumi.distage.roles.model.IntegrationCheck
+import izumi.fundamentals.platform.functional.Identity
 import izumi.fundamentals.platform.integration.ResourceCheck
+import zio.Task
 
 object MonadPlugin extends PluginDef
   with CatsDIEffectModule
   with ZIODIEffectModule
 
-object MockAppCatsIOPlugin extends MockAppPlugin[IO]
-object MockAppZioPlugin extends MockAppPlugin[zio.IO[Throwable, ?]]
+object MockAppCatsIOPlugin extends MockAppPlugin[CIO]
+object MockAppZioPlugin extends MockAppPlugin[Task]
+object MockAppIdPlugin extends MockAppPlugin[Identity]
 
 abstract class MockAppPlugin[F[_] : TagK] extends PluginDef {
   make[MockPostgresDriver[F]]
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
new file mode 100644
index 000000000..9f8248d8f
--- /dev/null
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/DistageTestDockerBIO.scala
@@ -0,0 +1,35 @@
+package izumi.distage.testkit.docker
+
+import distage.DIKey
+import izumi.distage.testkit.docker.fixtures.PgSvcExample
+import izumi.distage.testkit.integration.docker.examples.{DynamoDocker, PostgresDocker}
+import izumi.distage.testkit.services.st.dtest.TestConfig
+import izumi.distage.testkit.st.specs.DistageBIOSpecScalatest
+import zio.IO
+
+final class DistageTestDockerBIO extends DistageBIOSpecScalatest[IO] {
+
+  "distage test runner" should {
+    "support docker resources" in {
+      service: PgSvcExample =>
+        for {
+          _ <- IO(println(s"ports: pg=${service.pg} ddb=${service.ddb} "))
+        } yield ()
+    }
+
+    "support memoization" in {
+      service: PgSvcExample =>
+        for {
+          _ <- IO(println(s"ports: pg=${service.pg} ddb=${service.ddb} "))
+        } yield ()
+    }
+  }
+
+  override protected def config: TestConfig = {
+    TestConfig(
+      memoizedKeys = Set(
+        DIKey.get[DynamoDocker.Container],
+        DIKey.get[PostgresDocker.Container],
+      ))
+  }
+}
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
new file mode 100644
index 000000000..4257ae7a0
--- /dev/null
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/docker/fixtures/DockerPlugin.scala
@@ -0,0 +1,48 @@
+package izumi.distage.testkit.docker.fixtures
+
+import distage.config.ConfPath
+import izumi.distage.model.definition.Id
+import izumi.distage.model.definition.StandardAxis.Env
+import izumi.distage.monadic.modules.{CatsDIEffectModule, ZIODIEffectModule}
+import izumi.distage.plugins.PluginDef
+import izumi.distage.testkit.integration.docker.examples.{DynamoDocker, PostgresDocker}
+import izumi.distage.testkit.integration.docker.modules.DockerContainerModule
+import zio.Task
+
+case class ServicePort(number: Int)
+
+class PgSvcExample(val pg: ServicePort@Id("pg"), val ddb: ServicePort@Id("ddb"))
+
+object MonadPlugin extends PluginDef
+  with CatsDIEffectModule
+  with ZIODIEffectModule
+
+object DockerPlugin extends DockerContainerModule[Task] with PluginDef {
+  make[DynamoDocker.Container].fromResource {
+    DynamoDocker.make[Task]
+  }
+
+  // this container will start once `DynamoContainer` is up and running
+  make[PostgresDocker.Container].fromResource {
+    PostgresDocker.make[Task]
+      .dependOnDocker(DynamoDocker)
+  }
+
+  // these lines are for test scope
+  make[ServicePort].named("pg").tagged(Env.Test).from {
+    pg: PostgresDocker.Container =>
+      ServicePort(pg.mapping(PostgresDocker.primaryPort))
+  }
+  make[ServicePort].named("ddb").tagged(Env.Test).from {
+    dn: DynamoDocker.Container =>
+      ServicePort(dn.mapping(DynamoDocker.primaryPort))
+  }
+
+  // and this one is for production
+  make[ServicePort].named("pg").tagged(Env.Prod).from {
+    pgPort: Int @ConfPath("postgres.port") =>
+      ServicePort(pgPort)
+  }
+
+  make[PgSvcExample]
+}
diff --git a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/st/fixtures/Fixtures.scala b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/st/fixtures/Fixtures.scala
index 446dd1d3c..1678975ab 100644
--- a/distage/distage-testkit/src/test/scala/izumi/distage/testkit/st/fixtures/Fixtures.scala
+++ b/distage/distage-testkit/src/test/scala/izumi/distage/testkit/st/fixtures/Fixtures.scala
@@ -9,6 +9,7 @@ import izumi.distage.model.definition.StandardAxis._
 import izumi.distage.monadic.modules.{CatsDIEffectModule, ZIODIEffectModule}
 import izumi.distage.plugins.PluginDef
 import izumi.distage.roles.model.IntegrationCheck
+import izumi.distage.testkit.integration.docker.modules.DockerContainerModule
 import izumi.fundamentals.platform.integration.ResourceCheck
 import izumi.fundamentals.platform.language.Quirks
 import izumi.logstage.api.IzLogger