├── .github ├── CODEOWNERS ├── labeler.yml ├── dependabot.yml ├── clean-up.sh └── workflows │ ├── ci.yml │ ├── publish.yml │ └── bump-version.yml ├── mise.toml ├── project ├── build.properties ├── plugins.sbt └── Dependencies.scala ├── docker-controller-scala-core └── src │ ├── test │ ├── resources │ │ ├── settings.env.ftl │ │ ├── docker-compose-1.yml │ │ ├── docker-compose-2.yml.ftl │ │ ├── docker-compose-3.yml.ftl │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ ├── DockerComposeFileGenSpec.scala │ │ ├── DockerComposeControllerSpec.scala │ │ ├── DockerComposeController2Spec.scala │ │ └── DockerControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ ├── Network.scala │ ├── Base58.scala │ ├── RandomPortUtil.scala │ ├── DockerComposeFileGen.scala │ ├── DockerClientConfigUtil.scala │ ├── NetworkSettingsImplicits.scala │ ├── DockerMachineEnv.scala │ ├── DockerComposeController.scala │ ├── DockerControllerHelper.scala │ ├── WaitPredicates.scala │ └── DockerController.scala ├── update-changelog.sh ├── docker-clean.sh ├── docker-controller-scala-postgresql └── src │ ├── test │ ├── resources │ │ └── flyway │ │ │ └── V1__Create_Tables.sql │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── postgresql │ │ └── PostgreSQLControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── postgresql │ └── PostgreSQLController.scala ├── .claude ├── settings.local.json └── commands │ └── git-commit.md ├── docker-controller-scala-mysql └── src │ ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── mysql │ │ ├── MySQLUserNameAndPassword.scala │ │ └── MySQLController.scala │ └── test │ ├── resources │ ├── flyway │ │ └── V1__Create_Tables.sql │ └── logback-test.xml │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── MySQLControllerSpec.scala ├── docker-controller-scala-scalatest └── src │ ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ ├── DockerContainerStartStopLifecycle.scala │ │ ├── DockerContainerCreateRemoveLifecycle.scala │ │ └── DockerControllerSpecSupport.scala │ └── test │ ├── scala │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ ├── DockerController_CreateRemoveForAll_StartStopForAll_Spec.scala │ │ ├── DockerController_CreateRemoveForAll_StartStopForEach_Spec.scala │ │ ├── DockerController_CreateRemoveForEach_StartStopForEach_Spec.scala │ │ ├── HttpRequestUtil.scala │ │ └── DockerControllerSpecBase.scala │ └── resources │ └── logback-test.xml ├── .scalafix.conf ├── .gitignore ├── docker-controller-scala-kafka └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── kafka │ │ └── KafkaControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── kafka │ └── KafkaController.scala ├── docker-controller-scala-minio └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── minio │ │ └── MinioControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── minio │ └── MinioController.scala ├── docker-controller-scala-redis └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── redis │ │ └── RedisControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── redis │ └── RedisController.scala ├── docker-controller-scala-elasticmq └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── elasticmq │ │ └── ElasticMQControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── elasticmq │ └── ElasticMQController.scala ├── docker-controller-scala-localstack └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── localstack │ │ └── LocalStackControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── localstack │ └── LocalStackController.scala ├── docker-controller-scala-zookeeper └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── ZooKeeperControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── zooKeeper │ └── ZooKeeperController.scala ├── docker-controller-scala-dynamodb-local └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── dynamodbLocal │ │ └── DynamoDBLocalControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── dynamodbLocal │ └── DynamoDBLocalController.scala ├── docker-controller-scala-elasticsearch └── src │ ├── test │ ├── resources │ │ └── logback-test.xml │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── elasticsearch │ │ └── ElasticsearchControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── elasticsearch │ └── ElasticsearchController.scala ├── renovate.json ├── LICENSE ├── .scalafmt.conf ├── docker-controller-scala-memcached └── src │ ├── test │ └── scala │ │ └── com │ │ └── github │ │ └── j5ik2o │ │ └── dockerController │ │ └── memcached │ │ └── MemcachedControllerSpec.scala │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── memcached │ └── MemcachedController.scala ├── docker-controller-scala-flyway └── src │ └── main │ └── scala │ └── com │ └── github │ └── j5ik2o │ └── dockerController │ └── flyway │ └── FlywaySpecSupport.scala └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @j5ik2o -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | java = "temurin-17.0.17+10" 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.11.7 2 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/resources/settings.env.ftl: -------------------------------------------------------------------------------- 1 | MESSAGE=${message} -------------------------------------------------------------------------------- /update-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | conventional-changelog -p angular -i CHANGELOG.md -s 4 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | automerge: 2 | - project/Dependencies.scala 3 | - project/plugins.sbt 4 | - .scalafmt.conf -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/resources/docker-compose-1.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: nginx 5 | ports: 6 | - 8080:80 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /docker-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker ps -aq | xargs docker rm -f 4 | docker network ls -f name=kafka -q | xargs docker network rm 5 | docker images -aq | xargs docker rmi -f 6 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/resources/docker-compose-2.yml.ftl: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: nginx 5 | ports: 6 | - ${nginxHostPort}:80 -------------------------------------------------------------------------------- /docker-controller-scala-postgresql/src/test/resources/flyway/V1__Create_Tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id integer PRIMARY KEY, 3 | name varchar(255) 4 | ); 5 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(sbt:*)", 5 | "Bash(java:*)", 6 | "Bash(gh run view:*)" 7 | ], 8 | "deny": [], 9 | "ask": [] 10 | } 11 | } -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/Network.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | case class Network(id: String) 4 | case class NetworkAlias(network: Network, name: String) 5 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/resources/docker-compose-3.yml.ftl: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | image: bithavoc/hello-world-env 5 | ports: 6 | - ${hostPort}:3000 7 | env_file: 8 | - ./settings-${id}.env -------------------------------------------------------------------------------- /docker-controller-scala-mysql/src/main/scala/com/github/j5ik2o/dockerController/mysql/MySQLUserNameAndPassword.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.mysql 2 | 3 | case class MySQLUserNameAndPassword(name: String, password: String) 4 | -------------------------------------------------------------------------------- /docker-controller-scala-mysql/src/test/resources/flyway/V1__Create_Tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` bigint NOT NULL , 3 | `name` varchar(255), 4 | PRIMARY KEY (`id`) 5 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 6 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/main/scala/com/github/j5ik2o/dockerController/DockerContainerStartStopLifecycle.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | object DockerContainerStartStopLifecycle extends Enumeration { 4 | val ForAllTest, ForEachTest = Value 5 | } 6 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/main/scala/com/github/j5ik2o/dockerController/DockerContainerCreateRemoveLifecycle.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | object DockerContainerCreateRemoveLifecycle extends Enumeration { 4 | val ForAllTest, ForEachTest = Value 5 | } 6 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | //Semantic Rules 3 | //ExplicitResultTypes, // Deactivated as scalafix on Scala 3 fails otherwise 4 | NoAutoTupling, 5 | //Syntactic Rules 6 | DisableSyntax, 7 | //ProcedureSyntax, 8 | LeakingImplicitClassVal, 9 | NoValInForComprehension 10 | ] 11 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 4 | 5 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") 6 | 7 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8") 8 | 9 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.4") 10 | 11 | addDependencyTreePlugin 12 | -------------------------------------------------------------------------------- /.github/clean-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf "$HOME/.ivy2/local" || true 4 | find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true 5 | find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true 6 | find $HOME/.sbt -name "*.lock" -delete || true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .cache-main 4 | .cache-tests 5 | .settings/ 6 | target/ 7 | project/target 8 | .cache 9 | .idea/ 10 | .cache-main 11 | .envrc 12 | bin/ 13 | native 14 | *.pyc 15 | *.pem 16 | *.stackdump 17 | *.tfvars 18 | *.tfstate 19 | *.tfstate.backup 20 | *.deb 21 | *.tgz 22 | *.log 23 | .terraform/ 24 | node_modules/ 25 | dump.rdb 26 | 27 | .DS_Store 28 | .credentials 29 | .gpgCredentials 30 | dumps/ 31 | .bsp/ 32 | .bloop/ 33 | 34 | .metals/ 35 | .vscode/ -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/scala/com/github/j5ik2o/dockerController/DockerController_CreateRemoveForAll_StartStopForAll_Spec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | class DockerController_CreateRemoveForAll_StartStopForAll_Spec extends DockerControllerSpecBase { 4 | 5 | override def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value = 6 | DockerContainerCreateRemoveLifecycle.ForAllTest 7 | 8 | override def startStopLifecycle: DockerContainerStartStopLifecycle.Value = 9 | DockerContainerStartStopLifecycle.ForAllTest 10 | 11 | } 12 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/scala/com/github/j5ik2o/dockerController/DockerController_CreateRemoveForAll_StartStopForEach_Spec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | class DockerController_CreateRemoveForAll_StartStopForEach_Spec extends DockerControllerSpecBase { 4 | 5 | override def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value = 6 | DockerContainerCreateRemoveLifecycle.ForAllTest 7 | 8 | override def startStopLifecycle: DockerContainerStartStopLifecycle.Value = 9 | DockerContainerStartStopLifecycle.ForEachTest 10 | 11 | } 12 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/scala/com/github/j5ik2o/dockerController/DockerController_CreateRemoveForEach_StartStopForEach_Spec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | class DockerController_CreateRemoveForEach_StartStopForEach_Spec extends DockerControllerSpecBase { 4 | 5 | override def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value = 6 | DockerContainerCreateRemoveLifecycle.ForEachTest 7 | 8 | override def startStopLifecycle: DockerContainerStartStopLifecycle.Value = 9 | DockerContainerStartStopLifecycle.ForEachTest 10 | 11 | } 12 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/Base58.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import java.security.SecureRandom 4 | 5 | object Base58 { 6 | private val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray 7 | private val RANDOM = new SecureRandom 8 | 9 | def randomString(length: Int): String = { 10 | val result = new Array[Char](length) 11 | for (i <- 0 until length) { 12 | val pick = ALPHABET(RANDOM.nextInt(ALPHABET.length)) 13 | result(i) = pick 14 | } 15 | new String(result) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/scala/com/github/j5ik2o/dockerController/DockerComposeFileGenSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import org.scalatest.freespec.AnyFreeSpec 4 | 5 | import java.nio.file.{ Files, Paths } 6 | 7 | class DockerComposeFileGenSpec extends AnyFreeSpec { 8 | "DockerComposeYmlGenSpec" - { 9 | "generate" in { 10 | val tmpFile = Files.createTempFile(Paths.get("/tmp"), "docker-compose-", ".yml") 11 | 12 | DockerComposeFileGen.generate( 13 | "docker-compose-2.yml.ftl", 14 | Map("nginxHostPort" -> Integer.valueOf(8080)), 15 | tmpFile.toFile 16 | ) 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-kafka/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-minio/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-mysql/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-redis/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-elasticmq/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-localstack/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-zookeeper/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-dynamodb-local/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docker-controller-scala-elasticsearch/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"], 4 | "commitMessagePrefix": "chore(deps):", 5 | "platformAutomerge": true, 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": true 10 | }, 11 | { 12 | "matchDepTypes": ["devDependencies"], 13 | "automerge": true 14 | }, 15 | { 16 | "managers": ["sbt"], 17 | "packageNames": ["sbt-ci-release"], 18 | "enabled": false 19 | }, 20 | { 21 | "matchManagers": ["mise"], 22 | "matchPackageNames": ["java"], 23 | "allowedVersions": "17" 24 | } 25 | ], 26 | "prHourlyLimit": 0, 27 | "prConcurrentLimit": 5 28 | } 29 | -------------------------------------------------------------------------------- /.claude/commands/git-commit.md: -------------------------------------------------------------------------------- 1 | --- 2 | allowed-tools: Bash(git:*), Bash(npm:*), Read(*.md), Fetch(*) 3 | description: "ワーキングディレクトリでの変更をコミットします" 4 | --- 5 | 6 | 意味のある変更単位ごとにコミットを行うことは、コードの履歴を明確にし、将来の変更を追跡しやすくするために重要です。 7 | 8 | 以下の手順に従って、ワーキングディレクトリでの変更をコミットします。 9 | 10 | ### 1. 変更を確認する 11 | 12 | まず、現在のワーキングディレクトリでの変更を確認します。以下のコマンドを実行してください: 13 | 14 | ```bash 15 | git status 16 | ``` 17 | 18 | ### 2. 変更をステージングする 19 | 20 | 変更をコミットする前に、ステージングエリアに追加する必要があります。以下のコマンドを使用して、すべての変更をステージングします: 21 | 22 | ```bash 23 | git add 対象ファイルやディレクトリ 24 | ``` 25 | 26 | 意味のある変更単位ごとにファイルを指定すること。無条件にすべての変更を追加しないでください。 27 | 28 | ### 3. コミットメッセージを作成する 29 | 30 | コミットメッセージは、変更内容を簡潔に説明する重要な部分です。以下のコマンドを使用して、コミットを作成します: 31 | 32 | ```bash 33 | git commit -m "コミットメッセージをここに入力" 34 | ``` 35 | 36 | コミットメッセージは日本語で記述してください。co-authorやコミットメッセージに"Claude Code"のキーワードは含めないこと。 37 | 38 | ### 4. コミットを確認する 39 | コミットが正しく行われたかを確認するために、以下のコマンドを実行します: 40 | 41 | ```bash 42 | git log --oneline 43 | ``` 44 | 45 | これにより、最近のコミットの一覧が表示され、コミットメッセージとハッシュが確認できます。 46 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/RandomPortUtil.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import java.net.InetSocketAddress 4 | import java.nio.channels.ServerSocketChannel 5 | 6 | trait RandomPortUtil { 7 | 8 | def temporaryServerAddress(interface: String = "127.0.0.1"): InetSocketAddress = RandomPortUtil.synchronized { 9 | val serverSocket = ServerSocketChannel.open() 10 | try { 11 | serverSocket.socket.bind(new InetSocketAddress(interface, 0)) 12 | val port = serverSocket.socket.getLocalPort 13 | new InetSocketAddress(interface, port) 14 | } finally serverSocket.close() 15 | } 16 | 17 | def temporaryServerHostnameAndPort(interface: String = "127.0.0.1"): (String, Int) = RandomPortUtil.synchronized { 18 | val socketAddress = temporaryServerAddress(interface) 19 | socketAddress.getHostName -> socketAddress.getPort 20 | } 21 | 22 | def temporaryServerPort(interface: String = "127.0.0.1"): Int = 23 | temporaryServerHostnameAndPort(interface)._2 24 | } 25 | 26 | object RandomPortUtil extends RandomPortUtil 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Junichi Kato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/scala/com/github/j5ik2o/dockerController/HttpRequestUtil.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import org.apache.commons.io.IOUtils 4 | import org.slf4j.LoggerFactory 5 | 6 | import java.io.InputStream 7 | import java.net.{ HttpURLConnection, URL } 8 | import scala.jdk.CollectionConverters._ 9 | 10 | object HttpRequestUtil { 11 | 12 | private val logger = LoggerFactory.getLogger(getClass) 13 | 14 | def wget(url: URL): Unit = { 15 | var connection: HttpURLConnection = null 16 | var in: InputStream = null 17 | try { 18 | connection = url.openConnection().asInstanceOf[HttpURLConnection] 19 | connection.setRequestMethod("GET") 20 | connection.connect() 21 | val responseCode = connection.getResponseCode 22 | assert(responseCode == HttpURLConnection.HTTP_OK) 23 | in = connection.getInputStream 24 | val lines = IOUtils.readLines(in, "UTF-8").asScala.mkString("\n") 25 | logger.debug(lines) 26 | } finally { 27 | if (in != null) 28 | in.close() 29 | if (connection != null) 30 | connection.disconnect() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.8.2 2 | runner.dialect = scala213 3 | style = defaultWithAlign 4 | danglingParentheses.preset = true 5 | indentOperator.preset = spray 6 | indentOperator.exemptScope = all 7 | align.preset = more 8 | align.tokens = [ 9 | { 10 | code = "=>" 11 | owners = [{ 12 | regex = "Case" 13 | }] 14 | }, 15 | { 16 | code = "=" 17 | owners = [{ 18 | regex = "Defn\\." 19 | }] 20 | }, 21 | { 22 | code = "->" 23 | }, 24 | { 25 | code = "//" 26 | }, 27 | { 28 | code = "%" 29 | owners = [{ 30 | regex = "Term.ApplyInfix" 31 | }] 32 | }, 33 | { 34 | code = "%%" 35 | owners = [{ 36 | regex = "Term.ApplyInfix" 37 | }] 38 | } 39 | ] 40 | includeCurlyBraceInSelectChains = true 41 | maxColumn = 120 42 | rewrite.rules = [RedundantParens, SortImports, PreferCurlyFors] 43 | spaces.inImportCurlyBraces = true 44 | binPack.literalArgumentLists = false 45 | optIn.breaksInsideChains = true -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerComposeFileGen.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import freemarker.template.{ Configuration, TemplateExceptionHandler } 4 | import org.seasar.util.io.ResourceUtil 5 | 6 | import java.io.{ File, FileWriter } 7 | import java.util.Locale 8 | import scala.jdk.CollectionConverters._ 9 | 10 | object DockerComposeFileGen { 11 | 12 | final val FreemarkerVersion = Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS 13 | 14 | private val cfg = new Configuration(FreemarkerVersion) 15 | 16 | cfg.setDefaultEncoding("UTF-8") 17 | cfg.setLocale(Locale.ENGLISH) 18 | cfg.setNumberFormat("computer") 19 | cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER) 20 | cfg.setLogTemplateExceptions(false) 21 | cfg.setWrapUncheckedExceptions(true) 22 | cfg.setFallbackOnNullLoopVariable(false) 23 | 24 | def generate(ymlFtl: String, context: Map[String, AnyRef], outputFile: File): Unit = { 25 | val ymlFtlFile: File = ResourceUtil.getResourceAsFile(ymlFtl) 26 | cfg.setDirectoryForTemplateLoading(ymlFtlFile.getParentFile) 27 | val template = cfg.getTemplate(ymlFtlFile.getName) 28 | val writer = new FileWriter(outputFile) 29 | try template.process(context.asJava, writer) 30 | finally writer.close() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /docker-controller-scala-redis/src/test/scala/com/github/j5ik2o/dockerController/redis/RedisControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.redis 2 | 3 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 4 | import com.redis.RedisClient 5 | import org.scalatest.freespec.AnyFreeSpec 6 | 7 | import scala.concurrent.duration._ 8 | import scala.util.control.NonFatal 9 | 10 | class RedisControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 11 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 12 | logger.debug(s"testTimeFactor = $testTimeFactor") 13 | 14 | val hostPort: Int = temporaryServerPort() 15 | val controller: RedisController = RedisController(dockerClient)(hostPort) 16 | 17 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 18 | 19 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 20 | Map( 21 | controller -> WaitPredicateSetting( 22 | Duration.Inf, 23 | WaitPredicates.forListeningHostTcpPort( 24 | dockerHost, 25 | hostPort, 26 | (1 * testTimeFactor).seconds, 27 | Some((5 * testTimeFactor).seconds) 28 | ) 29 | ) 30 | ) 31 | 32 | "RedisController" - { 33 | "run" in { 34 | var redisClient: RedisClient = null 35 | try { 36 | redisClient = new RedisClient(dockerHost, hostPort) 37 | redisClient.set("1", "2") 38 | assert(redisClient.get("1").get == "2") 39 | } catch { 40 | case NonFatal(ex) => 41 | fail(ex) 42 | } finally { 43 | if (redisClient != null) 44 | redisClient.close() 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerClientConfigUtil.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.core.{ DefaultDockerClientConfig, DockerClientConfig } 4 | import org.slf4j.LoggerFactory 5 | 6 | import scala.util.{ Failure, Success } 7 | 8 | object DockerClientConfigUtil { 9 | private val logger = LoggerFactory.getLogger(getClass) 10 | 11 | def dockerHost(dockerClientConfig: DockerClientConfig): String = 12 | if (dockerClientConfig.getDockerHost.getHost == null) 13 | "127.0.0.1" 14 | else 15 | dockerClientConfig.getDockerHost.getHost 16 | 17 | def buildConfigAwareOfDockerMachine( 18 | configBuilder: DefaultDockerClientConfig.Builder = DefaultDockerClientConfig.createDefaultConfigBuilder, 19 | profileName: String = "default" 20 | ): DockerClientConfig = { 21 | if (DockerMachineEnv.isSupportDockerMachine) { 22 | DockerMachineEnv 23 | .load(profileName) match { 24 | case Success(env) => 25 | logger.debug(s"env = $env") 26 | configBuilder 27 | .withDockerTlsVerify(env.tlsVerify) 28 | .withDockerHost(env.dockerHost) 29 | .withDockerCertPath(env.dockerCertPath) 30 | .build 31 | case Failure(ex) => 32 | logger.warn( 33 | s"Failed to load `docker-machine env $profileName`, so it was fallback to the default configuration.", 34 | ex 35 | ) 36 | // Let docker-java handle the default configuration 37 | configBuilder.build() 38 | } 39 | } else { 40 | // Let docker-java handle the default configuration 41 | logger.debug("Using docker-java default configuration") 42 | configBuilder.build() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docker-controller-scala-memcached/src/test/scala/com/github/j5ik2o/dockerController/memcached/MemcachedControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.memcached 2 | 3 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 4 | import com.twitter.finagle.Memcached 5 | import com.twitter.io.Buf 6 | import org.scalatest.concurrent.ScalaFutures 7 | import org.scalatest.freespec.AnyFreeSpec 8 | 9 | import scala.concurrent.duration._ 10 | 11 | class MemcachedControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport with ScalaFutures { 12 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 13 | logger.debug(s"testTimeFactor = $testTimeFactor") 14 | 15 | val hostPort: Int = temporaryServerPort() 16 | val controller: MemcachedController = MemcachedController(dockerClient)(hostPort) 17 | 18 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 19 | 20 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = Map( 21 | controller -> WaitPredicateSetting( 22 | Duration.Inf, 23 | WaitPredicates.forListeningHostTcpPort( 24 | dockerHost, 25 | hostPort, 26 | (1 * testTimeFactor).seconds, 27 | Some((5 * testTimeFactor).seconds) 28 | ) 29 | ) 30 | ) 31 | 32 | "MemcachedController" - { 33 | "run" in { 34 | val client = Memcached.client.newRichClient(s"$dockerHost:$hostPort") 35 | val str = "a" 36 | val buf = Buf.Utf8(str) 37 | val resultFuture = for { 38 | _ <- client.set("1", buf) 39 | r <- client.get("1") 40 | } yield r 41 | val result = resultFuture.toCompletableFuture.get().get 42 | assert(result == buf) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.scala' 9 | - '**.java' 10 | - '**.sbt' 11 | - '.scalafmt.conf' 12 | - '.github/workflows/**' 13 | - 'project/**' 14 | pull_request: 15 | branches: 16 | - main 17 | schedule: 18 | - cron: '0 * * * *' 19 | jobs: 20 | lint: 21 | runs-on: ubuntu-latest 22 | env: 23 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 24 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 25 | steps: 26 | - uses: actions/checkout@v5.0.1 27 | with: 28 | fetch-depth: 0 29 | - uses: actions/setup-java@v5 30 | with: 31 | distribution: 'temurin' 32 | java-version: '17' 33 | cache: 'sbt' 34 | - uses: sbt/setup-sbt@v1 35 | - run: sbt -v lint 36 | test: 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | jdk: [ 17, 19 ] 41 | scala: [ 2.12.19, 2.13.14, 3.3.3 ] 42 | runs-on: ubuntu-latest 43 | needs: lint 44 | env: 45 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 46 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 47 | AWS_REGION: ap-northeast-1 48 | TEST_TIME_FACTOR: 5 49 | steps: 50 | - uses: actions/checkout@v5.0.1 51 | with: 52 | fetch-depth: 0 53 | - uses: actions/setup-java@v5 54 | with: 55 | distribution: 'temurin' 56 | java-version: ${{ matrix.jdk }} 57 | cache: 'sbt' 58 | - uses: sbt/setup-sbt@v1 59 | - name: sbt test 60 | run: sbt -v ++${{ matrix.scala }} test 61 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/NetworkSettingsImplicits.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.model.{ ExposedPort, NetworkSettings, Ports } 4 | 5 | import scala.jdk.CollectionConverters._ 6 | import scala.language.implicitConversions 7 | 8 | final class NetworkSettingsOps(val networkSettings: NetworkSettings) extends AnyVal { 9 | 10 | def ports: Ports = { 11 | networkSettings.getPorts 12 | } 13 | 14 | def portBindings: Map[ExposedPort, Vector[Ports.Binding]] = { 15 | ports.getBindings.asScala.map { case (k, v) => k -> v.toVector }.toMap 16 | } 17 | 18 | def portBinding(exposedPort: ExposedPort): Option[Vector[Ports.Binding]] = { 19 | portBindings.get(exposedPort) 20 | } 21 | 22 | def bindingHostPorts(exposedPort: ExposedPort): Option[Vector[Int]] = { 23 | portBinding(exposedPort).map(_.map(_.getHostPortSpec.toInt)) 24 | } 25 | 26 | def bindingHostTcpPorts(exposedPort: Int): Option[Vector[Int]] = { 27 | bindingHostPorts(ExposedPort.tcp(exposedPort)) 28 | } 29 | 30 | def bindingHostUdpPorts(exposedPort: Int): Option[Vector[Int]] = { 31 | bindingHostPorts(ExposedPort.udp(exposedPort)) 32 | } 33 | 34 | def bindingHostPort(exposedPort: ExposedPort): Option[Int] = { 35 | portBinding(exposedPort).flatMap(_.headOption.map(_.getHostPortSpec.toInt)) 36 | } 37 | 38 | def bindingHostTcpPort(exposedPort: Int): Option[Int] = { 39 | bindingHostPort(ExposedPort.tcp(exposedPort)) 40 | } 41 | 42 | def bindingHostUdpPort(exposedPort: Int): Option[Int] = { 43 | bindingHostPort(ExposedPort.udp(exposedPort)) 44 | } 45 | 46 | } 47 | 48 | trait NetworkSettingsImplicits { 49 | 50 | implicit def toNetworkSettingsOps(networkSettings: NetworkSettings): NetworkSettingsOps = 51 | new NetworkSettingsOps(networkSettings) 52 | 53 | } 54 | 55 | object NetworkSettingsImplicits extends NetworkSettingsImplicits 56 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | push: 4 | tags: [ 'v*' ] 5 | branches: [ main ] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | env: 10 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 11 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 12 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 13 | steps: 14 | - uses: actions/checkout@v5 15 | with: { fetch-depth: 0 } 16 | - uses: actions/setup-java@v5 17 | with: 18 | distribution: temurin 19 | java-version: 17 20 | cache: sbt 21 | - uses: sbt/setup-sbt@v1 22 | - name: Configure Sonatype credentials 23 | run: | 24 | mkdir -p ~/.sbt/1.0 25 | cat > ~/.sbt/1.0/sonatype_credentials < waitPredicateSetting) 28 | 29 | "ZooKeeperControllerSpec" - { 30 | "run" in { 31 | var zk: ZooKeeper = null 32 | try { 33 | val connectionLatch = new CountDownLatch(1) 34 | zk = new ZooKeeper( 35 | s"$dockerHost:$hostPort", 36 | 3000, 37 | new Watcher { 38 | override def process(event: WatchedEvent): Unit = { 39 | logger.debug(s"event = $event") 40 | if (event.getState == KeeperState.SyncConnected) 41 | connectionLatch.countDown() 42 | } 43 | } 44 | ) 45 | connectionLatch.await(10, TimeUnit.SECONDS) 46 | } finally 47 | if (zk != null) 48 | zk.close() 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/test/scala/com/github/j5ik2o/dockerController/DockerControllerSpecBase.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 4 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 5 | import com.github.j5ik2o.dockerController.NetworkSettingsImplicits._ 6 | import org.scalatest.freespec.AnyFreeSpec 7 | 8 | import java.net.URL 9 | import scala.concurrent.duration._ 10 | 11 | abstract class DockerControllerSpecBase extends AnyFreeSpec with DockerControllerSpecSupport { 12 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 13 | logger.debug(s"testTimeFactor = $testTimeFactor") 14 | 15 | val nginx: DockerController = DockerController(dockerClient)( 16 | imageName = "nginx", 17 | tag = Some("latest") 18 | ).configureCreateContainerCmd { cmd => 19 | val hostPort: Int = temporaryServerPort() 20 | val containerPort: ExposedPort = ExposedPort.tcp(80) 21 | val portBinding: Ports = new Ports() 22 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 23 | logger.debug(s"hostPort = $hostPort, containerPort = $containerPort") 24 | cmd 25 | .withExposedPorts(containerPort) 26 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 27 | } 28 | 29 | override val dockerControllers: Vector[DockerController] = { 30 | Vector(nginx) 31 | } 32 | 33 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 34 | Map( 35 | nginx -> WaitPredicateSetting( 36 | Duration.Inf, 37 | WaitPredicates 38 | .forLogMessageContained("Configuration complete; ready for start up", Some((1 * testTimeFactor).seconds)) 39 | ) 40 | ) 41 | 42 | getClass.getSimpleName.stripPrefix("DockerController_").stripSuffix("_Spec") - { 43 | "run-1" in { 44 | val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get 45 | val url = new URL(s"http://$dockerHost:$hostPort") 46 | HttpRequestUtil.wget(url) 47 | } 48 | "run-2" in { 49 | val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get 50 | val url = new URL(s"http://$dockerHost:$hostPort") 51 | HttpRequestUtil.wget(url) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerMachineEnv.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | import scala.sys.process._ 6 | import scala.util.Try 7 | import scala.util.matching.Regex 8 | 9 | case class DockerMachineEnv(tlsVerify: Boolean, dockerHost: String, dockerCertPath: String) { 10 | require(dockerHost != null) 11 | require(dockerCertPath != null) 12 | } 13 | 14 | object DockerMachineEnv { 15 | LoggerFactory.getLogger(getClass) 16 | 17 | private val tlsVerifyRegex: Regex = """export DOCKER_TLS_VERIFY="(.*)"""".r 18 | 19 | private val hostRegex: Regex = """export DOCKER_HOST="(.*)"""".r 20 | 21 | private val certPathRegex: Regex = """export DOCKER_CERT_PATH="(.*)"""".r 22 | 23 | private def getDockerMachineCmd = Try { 24 | Seq("which", "docker-machine").!!.stripSuffix("\n") 25 | } 26 | 27 | def isSupportDockerMachine: Boolean = getDockerMachineCmd.isSuccess 28 | 29 | private def getDockerMachineEnv(name: String): Try[Vector[String]] = { 30 | for { 31 | cmd <- getDockerMachineCmd 32 | result <- Try { 33 | Seq(cmd, "env", name).!!.split("\n").toVector 34 | } 35 | } yield result 36 | } 37 | 38 | private def getDockerTlsVerify(env: Vector[String]): Boolean = { 39 | env 40 | .map { 41 | case tlsVerifyRegex(bs) if bs == "1" => Some(true) 42 | case tlsVerifyRegex(bs) if bs == "0" => Some(false) 43 | case _ => None 44 | }.find(_.nonEmpty).flatten.get 45 | } 46 | 47 | private def getDockerHost(env: Vector[String]): String = { 48 | env 49 | .map { 50 | case hostRegex(v) => Some(v) 51 | case _ => None 52 | }.find(_.nonEmpty).flatten.get 53 | } 54 | 55 | private def getDockerCertPath(env: Vector[String]): String = { 56 | env 57 | .map { 58 | case certPathRegex(v) => Some(v) 59 | case _ => None 60 | }.find(_.nonEmpty).flatten.get 61 | } 62 | 63 | def load(name: String): Try[DockerMachineEnv] = { 64 | val envTry = getDockerMachineEnv(name) 65 | val result = envTry.map { env => 66 | val tlsVerify = getDockerTlsVerify(env) 67 | val host = getDockerHost(env) 68 | val certPath = getDockerCertPath(env) 69 | DockerMachineEnv(tlsVerify, host, certPath) 70 | } 71 | result 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /docker-controller-scala-elasticsearch/src/test/scala/com/github/j5ik2o/dockerController/elasticsearch/ElasticsearchControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.elasticsearch 2 | 3 | import co.elastic.clients.elasticsearch.ElasticsearchClient 4 | import co.elastic.clients.json.jackson.JacksonJsonpMapper 5 | import co.elastic.clients.transport.rest_client.RestClientTransport 6 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 7 | import org.elasticsearch.client.RestClient 8 | import org.scalatest.freespec.AnyFreeSpec 9 | import org.apache.http.HttpHost 10 | 11 | import scala.concurrent.duration.{ Duration, DurationInt } 12 | import scala.util.control.NonFatal 13 | 14 | class ElasticsearchControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 15 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 16 | logger.debug(s"testTimeFactor = $testTimeFactor") 17 | 18 | val hostPort1: Int = temporaryServerPort() 19 | val hostPort2: Int = temporaryServerPort() 20 | val controller: ElasticsearchController = ElasticsearchController(dockerClient)(hostPort1, hostPort2) 21 | 22 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 23 | 24 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 25 | Map( 26 | controller -> WaitPredicateSetting( 27 | Duration.Inf, 28 | WaitPredicates.forListeningHostTcpPort( 29 | dockerHost, 30 | hostPort1, 31 | (60 * testTimeFactor).seconds, 32 | Some((30 * testTimeFactor).seconds) 33 | ) 34 | ) 35 | ) 36 | 37 | "ElasticsearchController" - { 38 | "run" in { 39 | var httpClient: RestClient = null 40 | var transport: RestClientTransport = null 41 | try { 42 | httpClient = RestClient 43 | .builder( 44 | new HttpHost("localhost", hostPort1) 45 | ).build() 46 | transport = new RestClientTransport( 47 | httpClient, 48 | new JacksonJsonpMapper() 49 | ) 50 | val esClient = new ElasticsearchClient(transport) 51 | // Test connection with ping 52 | val pingResult = esClient.ping() 53 | assert(pingResult.value()) 54 | } finally { 55 | if (transport != null) 56 | transport.close() 57 | if (httpClient != null) 58 | httpClient.close() 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docker-controller-scala-memcached/src/main/scala/com/github/j5ik2o/dockerController/memcached/MemcachedController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.memcached 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 6 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.memcached.MemcachedController._ 9 | 10 | import scala.concurrent.duration._ 11 | 12 | object MemcachedController { 13 | final val DefaultImageName: String = "memcached" 14 | final val DefaultImageTag: Option[String] = Some("trixie") 15 | final val DefaultContainerPort: Int = 11211 16 | 17 | def apply( 18 | dockerClient: DockerClient, 19 | isDockerClientAutoClose: Boolean = false, 20 | outputFrameInterval: FiniteDuration = 500.millis, 21 | imageName: String = DefaultImageName, 22 | imageTag: Option[String] = DefaultImageTag, 23 | envVars: Map[String, String] = Map.empty 24 | )( 25 | hostPort: Int, 26 | prometheusEnabled: Boolean = false 27 | ): MemcachedController = 28 | new MemcachedController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 29 | hostPort, 30 | prometheusEnabled 31 | ) 32 | } 33 | 34 | class MemcachedController( 35 | dockerClient: DockerClient, 36 | isDockerClientAutoClose: Boolean = false, 37 | outputFrameInterval: FiniteDuration = 500.millis, 38 | imageName: String = DefaultImageName, 39 | imageTag: Option[String] = DefaultImageTag, 40 | envVars: Map[String, String] = Map.empty 41 | )( 42 | hostPort: Int, 43 | prometheusEnabled: Boolean = false 44 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 45 | 46 | private val environmentVariables = Map( 47 | "MEMCACHED_PROMETHEUS_ENABLED" -> prometheusEnabled.toString 48 | ) ++ 49 | envVars 50 | 51 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 52 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 53 | val portBinding = new Ports 54 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 55 | super 56 | .newCreateContainerCmd() 57 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 58 | .withExposedPorts(containerPort) 59 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docker-controller-scala-dynamodb-local/src/main/scala/com/github/j5ik2o/dockerController/dynamodbLocal/DynamoDBLocalController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.dynamodbLocal 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.dynamodbLocal.DynamoDBLocalController._ 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | import scala.util.matching.Regex 12 | 13 | object DynamoDBLocalController { 14 | final val DefaultImageName: String = "amazon/dynamodb-local" 15 | final val DefaultImageTag: Option[String] = Some("1.18.0") 16 | final val DefaultContainerPort: Int = 8000 17 | final val RegexOfWaitPredicate: Regex = s"""Port.*$DefaultContainerPort.*""".r 18 | 19 | def apply( 20 | dockerClient: DockerClient, 21 | isDockerClientAutoClose: Boolean = false, 22 | outputFrameInterval: FiniteDuration = 500.millis, 23 | imageName: String = DefaultImageName, 24 | imageTag: Option[String] = DefaultImageTag, 25 | envVars: Map[String, String] = Map.empty 26 | )( 27 | hostPort: Int 28 | ): DynamoDBLocalController = 29 | new DynamoDBLocalController( 30 | dockerClient, 31 | isDockerClientAutoClose, 32 | outputFrameInterval, 33 | imageName, 34 | imageTag, 35 | envVars 36 | )(hostPort) 37 | } 38 | 39 | class DynamoDBLocalController( 40 | dockerClient: DockerClient, 41 | isDockerClientAutoClose: Boolean = false, 42 | outputFrameInterval: FiniteDuration = 500.millis, 43 | imageName: String = DefaultImageName, 44 | imageTag: Option[String] = DefaultImageTag, 45 | envVars: Map[String, String] = Map.empty 46 | )( 47 | hostPort: Int 48 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 49 | 50 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 51 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 52 | val portBinding = new Ports() 53 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 54 | super 55 | .newCreateContainerCmd() 56 | .withCmd("-jar", "DynamoDBLocal.jar", "-dbPath", ".", "-sharedDb") 57 | .withEnv(envVars.map { case (k, v) => s"$k=$v" }.toArray: _*) 58 | .withExposedPorts(containerPort) 59 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /docker-controller-scala-flyway/src/main/scala/com/github/j5ik2o/dockerController/flyway/FlywaySpecSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.flyway 2 | 3 | import org.flywaydb.core.Flyway 4 | import org.flywaydb.core.api.callback.Callback 5 | import org.flywaydb.core.internal.jdbc.DriverDataSource 6 | 7 | import javax.sql.DataSource 8 | import scala.jdk.CollectionConverters._ 9 | 10 | final case class PlaceholderConfig( 11 | placeholderReplacement: Boolean = false, 12 | placeholders: Map[String, String] = Map.empty, 13 | placeholderPrefix: Option[String] = None, 14 | placeholderSuffix: Option[String] = None 15 | ) 16 | 17 | final case class FlywayConfig( 18 | locations: Seq[String], 19 | callbacks: Seq[Callback] = Seq.empty, 20 | placeholderConfig: Option[PlaceholderConfig] = None 21 | ) 22 | 23 | final case class FlywayConfigWithDataSource(driverDataSource: DataSource, config: FlywayConfig) 24 | 25 | final case class FlywayContext(flyway: Flyway, config: FlywayConfigWithDataSource) 26 | 27 | trait FlywaySpecSupport { 28 | protected def flywayDriverClassName: String 29 | protected def flywayDbHost: String 30 | protected def flywayDbHostPort: Int 31 | protected def flywayDbName: String 32 | protected def flywayDbUserName: String 33 | protected def flywayDbPassword: String 34 | protected def flywayJDBCUrl: String 35 | 36 | private def createFlywayContext(flywayConfigWithDataSource: FlywayConfigWithDataSource): FlywayContext = { 37 | val configure = Flyway.configure() 38 | configure.dataSource(flywayConfigWithDataSource.driverDataSource) 39 | configure.locations(flywayConfigWithDataSource.config.locations: _*) 40 | configure.callbacks(flywayConfigWithDataSource.config.callbacks: _*) 41 | flywayConfigWithDataSource.config.placeholderConfig.foreach { pc => 42 | configure.placeholderReplacement(pc.placeholderReplacement) 43 | configure.placeholders(pc.placeholders.asJava) 44 | pc.placeholderPrefix.foreach { pp => 45 | configure.placeholderPrefix(pp) 46 | } 47 | pc.placeholderSuffix.foreach { ps => 48 | configure.placeholderSuffix(ps) 49 | } 50 | } 51 | FlywayContext(configure.load(), flywayConfigWithDataSource) 52 | } 53 | 54 | private def createFlywayDataSource: DataSource = new DriverDataSource( 55 | getClass.getClassLoader, 56 | flywayDriverClassName, 57 | flywayJDBCUrl, 58 | flywayDbUserName, 59 | flywayDbPassword 60 | ) 61 | 62 | // s"jdbc:mysql://$flywayDbHost:$flywayDbHostPort/$flywayDbName?useSSL=false&user=$flywayDbUserName&password=$flywayDbPassword", 63 | 64 | protected def createFlywayContext(flywayConfig: FlywayConfig): FlywayContext = createFlywayContext( 65 | FlywayConfigWithDataSource(createFlywayDataSource, flywayConfig) 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /docker-controller-scala-redis/src/main/scala/com/github/j5ik2o/dockerController/redis/RedisController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.redis 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.redis.RedisController._ 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | 12 | object RedisController { 13 | final val DefaultImageName: String = "redis" 14 | final val DefaultImageTag: Option[String] = Some("bookworm") 15 | final val DefaultContainerPort: Int = 6379 16 | 17 | def apply( 18 | dockerClient: DockerClient, 19 | isDockerClientAutoClose: Boolean = false, 20 | outputFrameInterval: FiniteDuration = 500.millis, 21 | imageName: String = DefaultImageName, 22 | imageTag: Option[String] = DefaultImageTag, 23 | envVars: Map[String, String] = Map.empty 24 | )( 25 | hostPort: Int, 26 | allowEmptyPassword: Boolean = true, 27 | redisPassword: Option[String] = None, 28 | redisAofEnabled: Boolean = false 29 | ): RedisController = 30 | new RedisController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 31 | hostPort, 32 | allowEmptyPassword, 33 | redisPassword, 34 | redisAofEnabled 35 | ) 36 | } 37 | 38 | class RedisController( 39 | dockerClient: DockerClient, 40 | isDockerClientAutoClose: Boolean = false, 41 | outputFrameInterval: FiniteDuration = 500.millis, 42 | imageName: String = DefaultImageName, 43 | imageTag: Option[String] = DefaultImageTag, 44 | envVars: Map[String, String] = Map.empty 45 | )( 46 | hostPort: Int, 47 | allowEmptyPassword: Boolean = true, 48 | redisPassword: Option[String] = None, 49 | redisAofEnabled: Boolean = false 50 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 51 | 52 | private val environmentVariables = Map( 53 | "ALLOW_EMPTY_PASSWORD" -> { if (allowEmptyPassword) "yes" else "no" }, 54 | "REDIS_AOF_ENABLED" -> { if (redisAofEnabled) "yes" else "no" } 55 | ) ++ 56 | redisPassword.map(password => Map("REDIS_PASSWORD" -> password)).getOrElse(Map.empty) ++ 57 | envVars 58 | 59 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 60 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 61 | val portBinding = new Ports 62 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 63 | super 64 | .newCreateContainerCmd() 65 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 66 | .withExposedPorts(containerPort) 67 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docker-controller-scala-elasticmq/src/main/scala/com/github/j5ik2o/dockerController/elasticmq/ElasticMQController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.elasticmq 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.elasticmq.ElasticMQController.{ 9 | DefaultContainerPorts, 10 | DefaultImageName, 11 | DefaultImageTag 12 | } 13 | 14 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 15 | import scala.jdk.CollectionConverters._ 16 | 17 | object ElasticMQController { 18 | final val DefaultImageName: String = "softwaremill/elasticmq" 19 | final val DefaultImageTag: Option[String] = Some("1.6.14") 20 | final val DefaultContainerPorts: Seq[Int] = Seq(9324, 9325) 21 | 22 | def apply( 23 | dockerClient: DockerClient, 24 | isDockerClientAutoClose: Boolean = false, 25 | outputFrameInterval: FiniteDuration = 500.millis, 26 | imageName: String = DefaultImageName, 27 | imageTag: Option[String] = DefaultImageTag, 28 | envVars: Map[String, String] = Map.empty 29 | )(dockerHost: String, hostPorts: Seq[Int]): ElasticMQController = 30 | new ElasticMQController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 31 | dockerHost, 32 | hostPorts 33 | ) 34 | } 35 | 36 | class ElasticMQController( 37 | dockerClient: DockerClient, 38 | isDockerClientAutoClose: Boolean = false, 39 | outputFrameInterval: FiniteDuration = 500.millis, 40 | imageName: String = DefaultImageName, 41 | imageTag: Option[String] = DefaultImageTag, 42 | envVars: Map[String, String] = Map.empty 43 | )(dockerHost: String, hostPorts: Seq[Int]) 44 | extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 45 | 46 | private val environmentVariables = Map( 47 | "JAVA_OPTS" -> "-Dconfig.override_with_env_vars=true", 48 | "CONFIG_FORCE_node__address_host" -> "*", 49 | "CONFIG_FORCE_rest__sqs_bind__hostname" -> "0.0.0.0", 50 | "CONFIG_FORCE_generate__node__address" -> "false" 51 | ) ++ 52 | envVars 53 | 54 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 55 | val containerPorts = DefaultContainerPorts.map(ExposedPort.tcp) 56 | val ports = new Ports() 57 | containerPorts.zip(hostPorts).foreach { case (containerPort, hostPort) => 58 | ports.bind(containerPort, Ports.Binding.bindPort(hostPort)) 59 | } 60 | super 61 | .newCreateContainerCmd() 62 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 63 | .withExposedPorts(containerPorts.toList.asJava) 64 | .withHostConfig(newHostConfig().withPortBindings(ports)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | force_bump: 6 | description: 'Force version bump' 7 | required: true 8 | type: boolean 9 | default: false 10 | schedule: 11 | - cron: '0 0 * * *' 12 | jobs: 13 | bump-version: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | with: 18 | fetch-depth: 0 19 | persist-credentials: false 20 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 21 | - name: Calculate changes from the latest tag to HEAD 22 | id: changes 23 | run: | 24 | # タグが存在するか確認 25 | if git tag -l | grep -q .; then 26 | LATEST_TAG=$(git describe --abbrev=0 --tags) 27 | echo "latest-tag = $LATEST_TAG" 28 | # 最新タグから現在までの変更をカウント 29 | COUNT=$(git log $LATEST_TAG..HEAD --pretty=format:"%s" --no-merges \ 30 | --grep='^build:' \ 31 | --grep='^ci:' \ 32 | --grep='^feat:' \ 33 | --grep='^fix:' \ 34 | --grep='^docs:' \ 35 | --grep='^style:' \ 36 | --grep='^refactor:' \ 37 | --grep='^perf:' \ 38 | --grep='^test:' \ 39 | --grep='^revert:' \ 40 | --grep='^chore:' | awk 'END{print NR}') 41 | else 42 | echo "No tags found - using initial commit as base" 43 | # 初期コミットから現在までの変更をカウント(初回実行時) 44 | FIRST_COMMIT=$(git rev-list --max-parents=0 HEAD) 45 | COUNT=$(git log $FIRST_COMMIT..HEAD --pretty=format:"%s" --no-merges \ 46 | --grep='^build:' \ 47 | --grep='^ci:' \ 48 | --grep='^feat:' \ 49 | --grep='^fix:' \ 50 | --grep='^docs:' \ 51 | --grep='^style:' \ 52 | --grep='^refactor:' \ 53 | --grep='^perf:' \ 54 | --grep='^test:' \ 55 | --grep='^revert:' \ 56 | --grep='^chore:' | awk 'END{print NR}') 57 | # 初回は必ず1以上にしてタグ付けができるようにする 58 | if [ "$COUNT" -eq "0" ]; then 59 | COUNT=1 60 | fi 61 | fi 62 | echo "steps.changes.outputs.count = $COUNT" 63 | if [[ "${{ inputs.force_bump }}" == "true" ]]; then 64 | echo "count=1" >> $GITHUB_OUTPUT 65 | else 66 | echo "count=$COUNT" >> $GITHUB_OUTPUT 67 | fi 68 | - name: Bump version and push tag 69 | id: tag_version 70 | uses: mathieudutour/github-tag-action@v6.2 71 | with: 72 | github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 73 | default_bump: patch 74 | if: steps.changes.outputs.count > 0 75 | - name: Create a GitHub release 76 | uses: actions/create-release@v1 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 79 | with: 80 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 81 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 82 | body: ${{ steps.tag_version.outputs.changelog }} 83 | if: steps.changes.outputs.count > 0 -------------------------------------------------------------------------------- /docker-controller-scala-elasticsearch/src/main/scala/com/github/j5ik2o/dockerController/elasticsearch/ElasticsearchController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.elasticsearch 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.elasticsearch.ElasticsearchController._ 9 | 10 | import scala.concurrent.duration._ 11 | 12 | object ElasticsearchController { 13 | final val DefaultImageName: String = "docker.elastic.co/elasticsearch/elasticsearch" 14 | final val DefaultImageTag: Option[String] = Some("9.1.3") 15 | final val DefaultContainerPorts: Seq[Int] = Seq(9200, 9300) 16 | 17 | final val DefaultEnvVars: Map[String, String] = Map( 18 | "discovery.type" -> "single-node", 19 | "xpack.security.enabled" -> "false", 20 | "xpack.security.http.ssl.enabled" -> "false", 21 | "ES_JAVA_OPTS" -> "-Xms512m -Xmx512m" 22 | ) 23 | 24 | def apply( 25 | dockerClient: DockerClient, 26 | isDockerClientAutoClose: Boolean = false, 27 | outputFrameInterval: FiniteDuration = 500.millis, 28 | imageName: String = DefaultImageName, 29 | imageTag: Option[String] = DefaultImageTag, 30 | envVars: Map[String, String] = DefaultEnvVars 31 | )( 32 | hostPort1: Int, 33 | hostPort2: Int 34 | ): ElasticsearchController = 35 | new ElasticsearchController( 36 | dockerClient, 37 | isDockerClientAutoClose, 38 | outputFrameInterval, 39 | imageName, 40 | imageTag, 41 | envVars 42 | )(hostPort1, hostPort2) 43 | } 44 | 45 | class ElasticsearchController( 46 | dockerClient: DockerClient, 47 | isDockerClientAutoClose: Boolean = false, 48 | outputFrameInterval: FiniteDuration = 500.millis, 49 | imageName: String = DefaultImageName, 50 | imageTag: Option[String] = DefaultImageTag, 51 | envVars: Map[String, String] = Map.empty 52 | )( 53 | hostPort1: Int, 54 | hostPort2: Int 55 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 56 | 57 | private val environmentVariables = Map( 58 | "discovery.type" -> "single-node", 59 | "xpack.security.enabled" -> "false" 60 | ) ++ envVars 61 | 62 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 63 | val containerPorts = DefaultContainerPorts.map(ExposedPort.tcp) 64 | val portBinding = new Ports() 65 | containerPorts.zip(Seq(hostPort1, hostPort2)).foreach { case (containerPort, hostPort) => 66 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 67 | } 68 | val result = super 69 | .newCreateContainerCmd() 70 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 71 | .withExposedPorts(containerPorts: _*) 72 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 73 | result 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docker-controller-scala-minio/src/test/scala/com/github/j5ik2o/dockerController/minio/MinioControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.minio 2 | 3 | import com.amazonaws.auth.{ AWSStaticCredentialsProvider, BasicAWSCredentials } 4 | import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration 5 | import com.amazonaws.regions.Regions 6 | import com.amazonaws.services.s3.model.CreateBucketRequest 7 | import com.amazonaws.services.s3.{ AmazonS3, AmazonS3Client } 8 | import com.github.j5ik2o.dockerController.WaitPredicates.WaitPredicate 9 | import com.github.j5ik2o.dockerController._ 10 | import org.scalatest.freespec.AnyFreeSpec 11 | 12 | import scala.concurrent.duration._ 13 | import scala.jdk.CollectionConverters._ 14 | 15 | class MinioControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 16 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 17 | logger.debug(s"testTimeFactor = $testTimeFactor") 18 | 19 | val minioAccessKeyId: String = "AKIAIOSFODNN7EXAMPLE" 20 | val minioSecretAccessKey: String = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 21 | val minioHost: String = DockerClientConfigUtil.dockerHost(dockerClientConfig) 22 | val minioPort: Int = temporaryServerPort() 23 | val minioEndpoint: String = s"http://$minioHost:$minioPort" 24 | val minioRegion: Regions = Regions.AP_NORTHEAST_1 25 | 26 | val controller: MinioController = MinioController(dockerClient)(minioPort, minioAccessKeyId, minioSecretAccessKey) 27 | 28 | // val waitPredicate: WaitPredicate = WaitPredicates.forListeningHostTcpPort(dockerHost, minioPort) 29 | val waitPredicate: WaitPredicate = 30 | WaitPredicates.forLogMessageByRegex(MinioController.RegexForWaitPredicate, Some((1 * testTimeFactor).seconds)) 31 | 32 | val waitPredicateSetting: WaitPredicateSetting = WaitPredicateSetting(Duration.Inf, waitPredicate) 33 | 34 | val bucketName = "test" 35 | 36 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 37 | 38 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 39 | Map( 40 | controller -> waitPredicateSetting 41 | ) 42 | 43 | protected val s3Client: AmazonS3 = { 44 | AmazonS3Client 45 | .builder() 46 | .withEndpointConfiguration(new EndpointConfiguration(minioEndpoint, minioRegion.getName)) 47 | .withCredentials( 48 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(minioAccessKeyId, minioSecretAccessKey)) 49 | ) 50 | .build() 51 | } 52 | 53 | protected def createBucket(): Unit = { 54 | if (!s3Client.listBuckets().asScala.exists(_.getName == bucketName)) { 55 | val request = new CreateBucketRequest(bucketName, minioRegion.getName) 56 | s3Client.createBucket(request) 57 | logger.info(s"bucket created: $bucketName") 58 | } 59 | while (!s3Client.listBuckets().asScala.exists(_.getName == bucketName)) { 60 | logger.info(s"Waiting for the bucket to be created: $bucketName") 61 | Thread.sleep(500) 62 | } 63 | } 64 | 65 | "MinioController" - { 66 | "run" in { 67 | createBucket() 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docker-controller-scala-minio/src/main/scala/com/github/j5ik2o/dockerController/minio/MinioController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.minio 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.minio.MinioController._ 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | import scala.util.matching.Regex 12 | 13 | object MinioController { 14 | final val DefaultImageName = "minio/minio" 15 | final val DefaultImageTag: Some[String] = Some("RELEASE.2025-07-23T15-54-02Z-cpuv1") 16 | final val DefaultContainerPort = 9000 17 | final val RegexForWaitPredicate: Regex = """^Docs: https://docs\.min\.io$""".r 18 | 19 | final val DefaultMinioAccessKeyId: String = "AKIAIOSFODNN7EXAMPLE" 20 | final val DefaultMinioSecretAccessKey: String = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 21 | 22 | def apply( 23 | dockerClient: DockerClient, 24 | isDockerClientAutoClose: Boolean = false, 25 | outputFrameInterval: FiniteDuration = 500.millis, 26 | imageName: String = DefaultImageName, 27 | imageTag: Option[String] = DefaultImageTag, 28 | envVars: Map[String, String] = Map.empty 29 | )( 30 | hostPort: Int, 31 | minioAccessKeyId: String = DefaultMinioAccessKeyId, 32 | minioSecretAccessKey: String = DefaultMinioSecretAccessKey 33 | ): MinioController = 34 | new MinioController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 35 | hostPort, 36 | minioAccessKeyId, 37 | minioSecretAccessKey 38 | ) 39 | } 40 | 41 | class MinioController( 42 | dockerClient: DockerClient, 43 | isDockerClientAutoClose: Boolean = false, 44 | outputFrameInterval: FiniteDuration = 500.millis, 45 | imageName: String = DefaultImageName, 46 | imageTag: Option[String] = DefaultImageTag, 47 | envVars: Map[String, String] = Map.empty 48 | )( 49 | hostPort: Int, 50 | minioAccessKeyId: String = DefaultMinioAccessKeyId, 51 | minioSecretAccessKey: String = DefaultMinioSecretAccessKey 52 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 53 | 54 | private val environmentVariables = Map( 55 | "MINIO_ROOT_USER" -> minioAccessKeyId, 56 | "MINIO_ROOT_PASSWORD" -> minioSecretAccessKey 57 | ) ++ envVars 58 | 59 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 60 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 61 | val portBinding = new Ports() 62 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 63 | super 64 | .newCreateContainerCmd() 65 | .withCmd("server", "--compat", "/data") 66 | .withEnv( 67 | environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _* 68 | ) 69 | .withExposedPorts(containerPort) 70 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /docker-controller-scala-zookeeper/src/main/scala/com/github/j5ik2o/dockerController/zooKeeper/ZooKeeperController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.zooKeeper 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.zooKeeper.ZooKeeperController._ 8 | import com.github.j5ik2o.dockerController.{ DockerControllerImpl, NetworkAlias } 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | import scala.util.matching.Regex 12 | 13 | object ZooKeeperController { 14 | final val DefaultImageName = "zookeeper" 15 | final val DefaultImageTag: Some[String] = Some("3.5") 16 | final val DefaultZooPort = 2181 17 | final val RegexForWaitPredicate: Regex = """binding to port /0.0.0.0:.*""".r 18 | 19 | def apply( 20 | dockerClient: DockerClient, 21 | isDockerClientAutoClose: Boolean = false, 22 | outputFrameInterval: FiniteDuration = 500.millis, 23 | imageName: String = DefaultImageName, 24 | imageTag: Option[String] = DefaultImageTag, 25 | envVars: Map[String, String] = Map.empty 26 | )( 27 | myId: Int, 28 | hostPort: Int, 29 | containerPort: Int = DefaultZooPort, 30 | networkAlias: Option[NetworkAlias] = None 31 | ): ZooKeeperController = 32 | new ZooKeeperController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 33 | myId, 34 | hostPort, 35 | containerPort, 36 | networkAlias 37 | ) 38 | } 39 | 40 | class ZooKeeperController( 41 | dockerClient: DockerClient, 42 | isDockerClientAutoClose: Boolean = false, 43 | outputFrameInterval: FiniteDuration = 500.millis, 44 | imageName: String = DefaultImageName, 45 | imageTag: Option[String] = DefaultImageTag, 46 | envVars: Map[String, String] = Map.empty 47 | )( 48 | myId: Int, 49 | hostPort: Int, 50 | val containerPort: Int, 51 | networkAlias: Option[NetworkAlias] = None 52 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 53 | 54 | private def environmentVariables(myId: Int): Map[String, String] = { 55 | Map( 56 | "ZOO_MY_ID" -> myId.toString, 57 | "ZOO_PORT" -> containerPort.toString 58 | ) ++ envVars 59 | } 60 | 61 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 62 | val zooPort = ExposedPort.tcp(containerPort) 63 | val portBinding = new Ports() 64 | portBinding.bind(zooPort, Ports.Binding.bindPort(hostPort)) 65 | val defaultHostConfig = newHostConfig.withPortBindings(portBinding) 66 | val hostConfig = networkAlias.fold(defaultHostConfig) { n => defaultHostConfig.withNetworkMode(n.network.id) } 67 | val result = super 68 | .newCreateContainerCmd() 69 | .withEnv(environmentVariables(myId).map { case (k, v) => s"$k=$v" }.toArray: _*) 70 | .withExposedPorts(zooPort, ExposedPort.tcp(2888), ExposedPort.tcp(3888)) 71 | .withHostConfig(hostConfig) 72 | networkAlias.fold(result) { n => result.withAliases(n.name) } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docker-controller-scala-mysql/src/main/scala/com/github/j5ik2o/dockerController/mysql/MySQLController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.mysql 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.{ CreateContainerCmd, RemoveContainerCmd } 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.mysql.MySQLController._ 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | 12 | object MySQLController { 13 | final val DefaultImageName: String = "mysql" 14 | final val DefaultImageTag: Option[String] = Some("9.4.0") 15 | final val DefaultContainerPort: Int = 3306 16 | 17 | def apply( 18 | dockerClient: DockerClient, 19 | isDockerClientAutoClose: Boolean = false, 20 | outputFrameInterval: FiniteDuration = 500.millis, 21 | imageName: String = DefaultImageName, 22 | imageTag: Option[String] = DefaultImageTag, 23 | envVars: Map[String, String] = Map.empty 24 | )( 25 | hostPort: Int, 26 | rootPassword: String, 27 | userNameAndPassword: Option[MySQLUserNameAndPassword] = None, 28 | databaseName: Option[String] = None 29 | ): MySQLController = 30 | new MySQLController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 31 | hostPort, 32 | rootPassword, 33 | userNameAndPassword, 34 | databaseName 35 | ) 36 | } 37 | 38 | class MySQLController( 39 | dockerClient: DockerClient, 40 | isDockerClientAutoClose: Boolean = false, 41 | outputFrameInterval: FiniteDuration = 500.millis, 42 | imageName: String = DefaultImageName, 43 | imageTag: Option[String] = DefaultImageTag, 44 | envVars: Map[String, String] = Map.empty 45 | )( 46 | hostPort: Int, 47 | rootPassword: String, 48 | userNameAndPassword: Option[MySQLUserNameAndPassword] = None, 49 | databaseName: Option[String] = None 50 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 51 | 52 | private val environmentVariables: Map[String, String] = { 53 | val env1 = Map[String, String]( 54 | "MYSQL_ROOT_PASSWORD" -> rootPassword 55 | ) ++ envVars 56 | val env2 = userNameAndPassword.fold(env1) { case MySQLUserNameAndPassword(u, p) => 57 | env1 ++ Map("MYSQL_USER" -> u, "MYSQL_PASSWORD" -> p) 58 | } 59 | databaseName.fold(env2) { name => env2 ++ Map("MYSQL_DATABASE" -> name) } 60 | } 61 | 62 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 63 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 64 | val portBinding = new Ports() 65 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 66 | val cmd = super 67 | .newCreateContainerCmd() 68 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 69 | .withExposedPorts(containerPort) 70 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 71 | cmd 72 | } 73 | 74 | override protected def newRemoveContainerCmd(): RemoveContainerCmd = { 75 | require(containerId.isDefined) 76 | dockerClient.removeContainerCmd(containerId.get).withForce(true) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /docker-controller-scala-mysql/src/test/scala/com/github/j5ik2o/dockerController/MySQLControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.j5ik2o.dockerController.flyway.{ FlywayConfig, FlywaySpecSupport } 4 | import com.github.j5ik2o.dockerController.mysql.MySQLController 5 | import org.scalatest.freespec.AnyFreeSpec 6 | 7 | import java.sql.{ Connection, DriverManager, ResultSet, Statement } 8 | import scala.concurrent.duration.{ Duration, DurationInt } 9 | import scala.util.control.NonFatal 10 | 11 | class MySQLControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport with FlywaySpecSupport { 12 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 13 | logger.debug(s"testTimeFactor = $testTimeFactor") 14 | 15 | val hostPort: Int = temporaryServerPort() 16 | val rootPassword: String = "test" 17 | 18 | override protected def flywayDriverClassName: String = classOf[com.mysql.cj.jdbc.Driver].getName 19 | override protected def flywayDbHost: String = dockerHost 20 | override protected def flywayDbHostPort: Int = hostPort 21 | override protected def flywayDbName: String = "test" 22 | override protected def flywayDbUserName: String = "root" 23 | override protected def flywayDbPassword: String = rootPassword 24 | 25 | override protected def flywayJDBCUrl: String = 26 | s"jdbc:mysql://$flywayDbHost:$flywayDbHostPort/$flywayDbName?allowPublicKeyRetrieval=true&useSSL=false&user=$flywayDbUserName&password=$flywayDbPassword" 27 | 28 | val controller: MySQLController = MySQLController(dockerClient)(hostPort, rootPassword, databaseName = Some("test")) 29 | 30 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 31 | 32 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 33 | Map( 34 | controller -> WaitPredicateSetting( 35 | Duration.Inf, 36 | WaitPredicates.forLogMessageByRegex( 37 | """.*MySQL init process done\. Ready for start up\.""".r, 38 | Some((1 * testTimeFactor).seconds) 39 | ) 40 | ) 41 | ) 42 | 43 | override protected def afterStartContainers(): Unit = { 44 | val flywayContext = createFlywayContext(FlywayConfig(Seq("flyway"))) 45 | flywayContext.flyway.migrate() 46 | } 47 | 48 | "MySQLController" - { 49 | "run" in { 50 | var conn: Connection = null 51 | var stmt: Statement = null 52 | var resultSet: ResultSet = null 53 | try { 54 | Class.forName(flywayDriverClassName) 55 | conn = DriverManager.getConnection(flywayJDBCUrl) 56 | stmt = conn.createStatement 57 | val result = stmt.executeUpdate("INSERT INTO users VALUES(1, 'kato')") 58 | assert(result == 1) 59 | resultSet = stmt.executeQuery("SELECT * FROM users") 60 | while (resultSet.next()) { 61 | val id = resultSet.getInt("id") 62 | val name = resultSet.getString("name") 63 | println(s"id = $id, name = $name") 64 | } 65 | } catch { 66 | case NonFatal(ex) => 67 | ex.printStackTrace() 68 | fail("occurred error", ex) 69 | } finally { 70 | if (resultSet != null) 71 | resultSet.close() 72 | if (stmt != null) 73 | stmt.close() 74 | if (conn != null) 75 | conn.close() 76 | } 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/scala/com/github/j5ik2o/dockerController/DockerComposeControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.core.{ DockerClientConfig, DockerClientImpl } 5 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient 6 | import org.apache.commons.io.IOUtils 7 | import org.scalatest.freespec.AnyFreeSpec 8 | import org.scalatest.{ BeforeAndAfter, BeforeAndAfterAll } 9 | import org.seasar.util.io.ResourceUtil 10 | import org.slf4j.{ Logger, LoggerFactory } 11 | 12 | import java.io.{ File, InputStream } 13 | import java.net.{ HttpURLConnection, URL } 14 | import scala.concurrent.duration.Duration 15 | import scala.jdk.CollectionConverters._ 16 | 17 | class DockerComposeControllerSpec extends AnyFreeSpec with BeforeAndAfter with BeforeAndAfterAll { 18 | val logger: Logger = LoggerFactory.getLogger(getClass) 19 | 20 | val dockerClientConfig: DockerClientConfig = DockerClientConfigUtil.buildConfigAwareOfDockerMachine() 21 | 22 | val dockerClient: DockerClient = { 23 | val httpClient: ApacheDockerHttpClient = new ApacheDockerHttpClient.Builder() 24 | .dockerHost(dockerClientConfig.getDockerHost).sslConfig(dockerClientConfig.getSSLConfig).build() 25 | DockerClientImpl.getInstance(dockerClientConfig, httpClient) 26 | } 27 | 28 | val host: String = DockerClientConfigUtil.dockerHost(dockerClientConfig) 29 | 30 | val hostPort: Int = RandomPortUtil.temporaryServerPort() 31 | 32 | val url = new URL(s"http://$host:$hostPort") 33 | 34 | def wget: Unit = { 35 | var connection: HttpURLConnection = null 36 | var in: InputStream = null 37 | try { 38 | connection = url.openConnection().asInstanceOf[HttpURLConnection] 39 | connection.setRequestMethod("GET") 40 | connection.connect() 41 | val responseCode = connection.getResponseCode 42 | assert(responseCode == HttpURLConnection.HTTP_OK) 43 | in = connection.getInputStream 44 | val lines = IOUtils.readLines(in, "UTF-8").asScala.mkString("\n") 45 | println(lines) 46 | } catch { 47 | case ex: Throwable => 48 | ex.printStackTrace() 49 | } finally { 50 | if (in != null) 51 | in.close() 52 | if (connection != null) 53 | connection.disconnect() 54 | } 55 | } 56 | 57 | var dockerController: DockerController = _ 58 | 59 | override protected def beforeAll(): Unit = { 60 | val buildDir: File = ResourceUtil.getBuildDir(getClass) 61 | val dockerComposeWorkingDir: File = new File(buildDir, "docker-compose") 62 | dockerController = DockerComposeController(dockerClient, isDockerClientAutoClose = true)( 63 | dockerComposeWorkingDir, 64 | "docker-compose-2.yml.ftl", 65 | Seq.empty, 66 | Map("nginxHostPort" -> hostPort.toString) 67 | ) 68 | dockerController.pullImageIfNotExists() 69 | dockerController.createContainer() 70 | } 71 | 72 | override protected def afterAll(): Unit = { 73 | dockerController.dispose() 74 | } 75 | 76 | before { 77 | dockerController.startContainer() 78 | dockerController.awaitCondition(Duration.Inf)( 79 | _.toString.contains("Configuration complete; ready for start up") 80 | ) 81 | Thread.sleep(1000) 82 | } 83 | 84 | after { 85 | dockerController.stopContainer() 86 | } 87 | 88 | "DockerComposeController" - { 89 | "run-1" in { 90 | wget 91 | } 92 | "run-2" in { 93 | wget 94 | } 95 | 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/scala/com/github/j5ik2o/dockerController/DockerComposeController2Spec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.core.{ DockerClientConfig, DockerClientImpl } 5 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient 6 | import org.apache.commons.io.IOUtils 7 | import org.scalatest.freespec.AnyFreeSpec 8 | import org.scalatest.{ BeforeAndAfter, BeforeAndAfterAll } 9 | import org.seasar.util.io.ResourceUtil 10 | import org.slf4j.{ Logger, LoggerFactory } 11 | 12 | import java.io.{ File, InputStream } 13 | import java.net.{ HttpURLConnection, URL } 14 | import java.time.Instant 15 | import scala.concurrent.duration.Duration 16 | import scala.jdk.CollectionConverters._ 17 | 18 | class DockerComposeController2Spec extends AnyFreeSpec with BeforeAndAfter with BeforeAndAfterAll { 19 | val logger: Logger = LoggerFactory.getLogger(getClass) 20 | 21 | val dockerClientConfig: DockerClientConfig = DockerClientConfigUtil.buildConfigAwareOfDockerMachine() 22 | 23 | val dockerClient: DockerClient = { 24 | val httpClient: ApacheDockerHttpClient = new ApacheDockerHttpClient.Builder() 25 | .dockerHost(dockerClientConfig.getDockerHost).sslConfig(dockerClientConfig.getSSLConfig).build() 26 | DockerClientImpl.getInstance(dockerClientConfig, httpClient) 27 | } 28 | 29 | val host: String = DockerClientConfigUtil.dockerHost(dockerClientConfig) 30 | 31 | val hostPort: Int = RandomPortUtil.temporaryServerPort() 32 | 33 | val url = new URL(s"http://$host:$hostPort") 34 | 35 | def wget: Unit = { 36 | var connection: HttpURLConnection = null 37 | var in: InputStream = null 38 | try { 39 | connection = url.openConnection().asInstanceOf[HttpURLConnection] 40 | connection.setRequestMethod("GET") 41 | connection.connect() 42 | val responseCode = connection.getResponseCode 43 | assert(responseCode == HttpURLConnection.HTTP_OK) 44 | in = connection.getInputStream 45 | val lines = IOUtils.readLines(in, "UTF-8").asScala.mkString("\n") 46 | println(lines) 47 | } catch { 48 | case ex: Throwable => 49 | ex.printStackTrace() 50 | } finally { 51 | if (in != null) 52 | in.close() 53 | if (connection != null) 54 | connection.disconnect() 55 | } 56 | } 57 | 58 | var dockerController: DockerController = _ 59 | 60 | override protected def beforeAll(): Unit = { 61 | val buildDir: File = ResourceUtil.getBuildDir(getClass) 62 | val dockerComposeWorkingDir: File = new File(buildDir, "docker-compose") 63 | dockerController = DockerComposeController(dockerClient, isDockerClientAutoClose = true)( 64 | dockerComposeWorkingDir, 65 | "docker-compose-3.yml.ftl", 66 | Seq("settings.env.ftl"), 67 | Map("hostPort" -> hostPort.toString, "message" -> Instant.now().toString) 68 | ) 69 | dockerController.pullImageIfNotExists() 70 | dockerController.createContainer() 71 | } 72 | 73 | override protected def afterAll(): Unit = { 74 | dockerController.dispose() 75 | } 76 | 77 | before { 78 | dockerController.startContainer() 79 | dockerController.awaitCondition(Duration.Inf)( 80 | _.toString.contains("Listening on port 3000") 81 | ) 82 | Thread.sleep(1000) 83 | } 84 | 85 | after { 86 | dockerController.stopContainer() 87 | } 88 | 89 | "DockerComposeController" - { 90 | "run-1" in { 91 | wget 92 | } 93 | "run-2" in { 94 | wget 95 | } 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | object Versions { 6 | val scala212Version = "2.12.19" 7 | val scala213Version = "2.13.14" 8 | val scala3Version = "3.3.3" 9 | val scalaTestVersion = "3.2.16" 10 | val logbackVersion = "1.2.12" 11 | val scalaCollectionCompatVersion = "2.11.0" 12 | val dockerJavaVersion = "3.6.0" 13 | val progressBarVersion = "0.9.5" 14 | val enumeratumVersion = "1.6.1" 15 | } 16 | 17 | object scalaLang { 18 | 19 | val scalaCollectionCompat = 20 | "org.scala-lang.modules" %% "scala-collection-compat" % Versions.scalaCollectionCompatVersion 21 | 22 | } 23 | 24 | object scalatest { 25 | val scalatest = "org.scalatest" %% "scalatest" % Versions.scalaTestVersion 26 | } 27 | 28 | object slf4j { 29 | val api = "org.slf4j" % "slf4j-api" % "1.7.36" 30 | 31 | } 32 | 33 | object amazonAws { 34 | val dynamodb = "com.amazonaws" % "aws-java-sdk-dynamodb" % "1.12.795" 35 | val s3 = "com.amazonaws" % "aws-java-sdk-s3" % "1.12.795" 36 | val sqs = "com.amazonaws" % "aws-java-sdk-sqs" % "1.12.795" 37 | } 38 | 39 | object apache { 40 | 41 | object zooKeeper { 42 | val zooKeeper = "org.apache.zookeeper" % "zookeeper" % "3.9.4" 43 | } 44 | 45 | object kafka { 46 | val kafkaClients = "org.apache.kafka" % "kafka-clients" % "4.1.1" 47 | } 48 | } 49 | 50 | object mysql { 51 | val connectorJava = "com.mysql" % "mysql-connector-j" % "9.5.0" 52 | } 53 | 54 | object postgresql { 55 | val postgresql = "org.postgresql" % "postgresql" % "42.7.8" 56 | } 57 | 58 | object elasticsearch { 59 | val restHighLevelClient = "org.elasticsearch.client" % "elasticsearch-rest-high-level-client" % "7.17.29" 60 | } 61 | 62 | object dockerJava { 63 | 64 | val dockerJava = "com.github.docker-java" % "docker-java" % Versions.dockerJavaVersion 65 | 66 | val dockerJavaTransportJersey = 67 | "com.github.docker-java" % "docker-java-transport-jersey" % Versions.dockerJavaVersion 68 | 69 | val dockerJavaTransportHttpclient5 = 70 | "com.github.docker-java" % "docker-java-transport-httpclient5" % Versions.dockerJavaVersion 71 | 72 | val dockerJavaTransportOkhttp = 73 | "com.github.docker-java" % "docker-java-transport-okhttp" % Versions.dockerJavaVersion 74 | 75 | } 76 | 77 | object tongfei { 78 | val progressbar = "me.tongfei" % "progressbar" % Versions.progressBarVersion 79 | 80 | } 81 | 82 | object seasar { 83 | val s2util = "org.seasar.util" % "s2util" % "0.0.1" 84 | 85 | } 86 | 87 | object freemarker { 88 | val freemarker = "org.freemarker" % "freemarker" % "2.3.34" 89 | 90 | } 91 | 92 | object logback { 93 | val classic = "ch.qos.logback" % "logback-classic" % Versions.logbackVersion 94 | 95 | } 96 | 97 | object commons { 98 | val io = "commons-io" % "commons-io" % "2.21.0" 99 | } 100 | 101 | object beachape { 102 | val enumeratum = "com.beachape" %% "enumeratum" % Versions.enumeratumVersion 103 | 104 | } 105 | 106 | object debasishg { 107 | val redisClient = "net.debasishg" %% "redisclient" % "3.42" 108 | } 109 | 110 | object twitter { 111 | val finagleMemcached = "com.twitter" %% "finagle-memcached" % "24.2.0" 112 | } 113 | 114 | object fasterxml { 115 | val jacksonModuleScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.20.1" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerComposeController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ AccessMode, Bind, SELContext, Volume } 7 | import org.apache.commons.io.FileUtils 8 | import org.seasar.util.io.ResourceUtil 9 | 10 | import java.io.File 11 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 12 | 13 | object DockerComposeController { 14 | 15 | def apply( 16 | dockerClient: DockerClient, 17 | isDockerClientAutoClose: Boolean = false, 18 | outputFrameInterval: FiniteDuration = 500.millis 19 | )( 20 | dockerComposeWorkingDir: File, 21 | ymlResourceName: String, 22 | environmentNames: Seq[String], 23 | context: Map[String, AnyRef] 24 | ): DockerController = 25 | new DockerComposeController(dockerClient, isDockerClientAutoClose, outputFrameInterval)( 26 | dockerComposeWorkingDir, 27 | ymlResourceName, 28 | environmentNames, 29 | context 30 | ) 31 | } 32 | 33 | private[dockerController] class DockerComposeController( 34 | dockerClient: DockerClient, 35 | isDockerClientAutoClose: Boolean, 36 | outputFrameInterval: FiniteDuration = 500.millis 37 | )( 38 | val dockerComposeWorkingDir: File, 39 | val ymlResourceName: String, 40 | val environmentResourceNames: Seq[String], 41 | val context: Map[String, AnyRef] 42 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)( 43 | "docker/compose", 44 | Some("1.24.1") 45 | ) { 46 | 47 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 48 | val id = Base58.randomString(16) 49 | if (!dockerComposeWorkingDir.exists()) dockerComposeWorkingDir.mkdir() 50 | val ymlFile = if (ymlResourceName.endsWith(".ftl")) { 51 | val file = new File(dockerComposeWorkingDir, s"docker-compose-$id.yml") 52 | DockerComposeFileGen.generate(ymlResourceName, context + ("id" -> id), file) 53 | file 54 | } else { 55 | val srcFile = ResourceUtil.getResourceAsFile(ymlResourceName) 56 | val destFile = new File(dockerComposeWorkingDir, srcFile.getName) 57 | FileUtils.copyFile(srcFile, destFile) 58 | destFile 59 | } 60 | 61 | environmentResourceNames.foreach { environmentResourceName => 62 | if (environmentResourceName.endsWith(".ftl")) { 63 | val Array(base, ext, _) = environmentResourceName.split("\\.") 64 | val file = new File(dockerComposeWorkingDir, s"$base-$id.$ext") 65 | DockerComposeFileGen.generate(environmentResourceName, context, file) 66 | } else { 67 | val srcFile = ResourceUtil.getResourceAsFile(environmentResourceName) 68 | val destFile = new File(dockerComposeWorkingDir, srcFile.getName) 69 | FileUtils.copyFile(srcFile, destFile) 70 | } 71 | } 72 | 73 | val baseDir = ymlFile.getParentFile 74 | val bind = new Bind(baseDir.getPath, new Volume(baseDir.getPath), AccessMode.ro, SELContext.none) 75 | val systemBind = new Bind("/var/run/docker.sock", new Volume("/docker.sock"), AccessMode.rw, SELContext.none) 76 | logger.debug(s"ymlFile = ${ymlFile.getName}") 77 | super 78 | .newCreateContainerCmd() 79 | .withCmd("up") 80 | .withEnv(s"COMPOSE_FILE=${ymlFile.getName}", "DOCKER_HOST=unix:///docker.sock") 81 | .withWorkingDir(baseDir.getPath) 82 | .withHostConfig( 83 | newHostConfig() 84 | .withBinds(bind, systemBind) 85 | ) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /docker-controller-scala-postgresql/src/test/scala/com/github/j5ik2o/dockerController/postgresql/PostgreSQLControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.postgresql 2 | 3 | import com.github.j5ik2o.dockerController.flyway.{ FlywayConfig, FlywaySpecSupport } 4 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 5 | import org.scalatest.freespec.AnyFreeSpec 6 | 7 | import java.sql.{ Connection, DriverManager, ResultSet, Statement } 8 | import scala.concurrent.duration._ 9 | import scala.util.control.NonFatal 10 | 11 | class PostgreSQLControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport with FlywaySpecSupport { 12 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 13 | logger.debug(s"testTimeFactor = $testTimeFactor") 14 | 15 | val hostPort: Int = temporaryServerPort() 16 | val dbName = "test" 17 | val rootUserName: String = "postgres" 18 | val rootPassword: Option[String] = Some("test") 19 | 20 | override protected def flywayDriverClassName: String = classOf[org.postgresql.Driver].getName 21 | override protected def flywayDbHost: String = dockerHost 22 | override protected def flywayDbHostPort: Int = hostPort 23 | override protected def flywayDbName: String = dbName 24 | override protected def flywayDbUserName: String = rootUserName 25 | override protected def flywayDbPassword: String = rootPassword.get 26 | 27 | override protected def flywayJDBCUrl: String = s"jdbc:postgresql://$flywayDbHost:$flywayDbHostPort/$flywayDbName" 28 | 29 | val controller: PostgreSQLController = 30 | PostgreSQLController(dockerClient)( 31 | flywayDbHostPort, 32 | flywayDbUserName, 33 | rootPassword, 34 | databaseName = Some(flywayDbName) 35 | ) 36 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 37 | 38 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 39 | Map( 40 | controller -> WaitPredicateSetting( 41 | Duration.Inf, 42 | WaitPredicates.forListeningHostTcpPort( 43 | dockerHost, 44 | hostPort, 45 | (1 * testTimeFactor).seconds, 46 | Some((8 * testTimeFactor).seconds) 47 | ) 48 | ) 49 | ) 50 | 51 | "PostgreSQLController" - { 52 | "run" in { 53 | var conn: Connection = null 54 | var stmt: Statement = null 55 | var resultSet: ResultSet = null 56 | try { 57 | Class.forName(flywayDriverClassName) 58 | conn = DriverManager.getConnection( 59 | flywayJDBCUrl, 60 | flywayDbUserName, 61 | flywayDbPassword 62 | ) 63 | stmt = conn.createStatement 64 | val result = stmt.executeUpdate("INSERT INTO users VALUES(1, 'kato')") 65 | assert(result == 1) 66 | resultSet = stmt.executeQuery("SELECT * FROM users") 67 | while (resultSet.next()) { 68 | val id = resultSet.getInt("id") 69 | val name = resultSet.getString("name") 70 | println(s"id = $id, name = $name") 71 | } 72 | } catch { 73 | case NonFatal(ex) => 74 | ex.printStackTrace() 75 | fail("occurred error", ex) 76 | } finally { 77 | if (resultSet != null) 78 | resultSet.close() 79 | if (stmt != null) 80 | stmt.close() 81 | if (conn != null) 82 | conn.close() 83 | } 84 | } 85 | } 86 | 87 | override protected def afterStartContainers(): Unit = { 88 | val flywayContext = createFlywayContext(FlywayConfig(Seq("flyway"))) 89 | flywayContext.flyway.migrate() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docker-controller-scala-elasticmq/src/test/scala/com/github/j5ik2o/dockerController/elasticmq/ElasticMQControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.elasticmq 2 | 3 | import com.amazonaws.auth.{ AWSCredentialsProviderChain, AWSStaticCredentialsProvider, BasicAWSCredentials } 4 | import com.amazonaws.client.builder.AwsClientBuilder 5 | import com.amazonaws.regions.Regions 6 | import com.amazonaws.services.sqs.AmazonSQSClientBuilder 7 | import com.amazonaws.services.sqs.model.{ CreateQueueRequest, SendMessageRequest, SetQueueAttributesRequest } 8 | import com.github.j5ik2o.dockerController.{ 9 | DockerController, 10 | DockerControllerSpecSupport, 11 | RandomPortUtil, 12 | WaitPredicates 13 | } 14 | import org.scalatest.freespec.AnyFreeSpec 15 | 16 | import java.util.UUID 17 | import scala.concurrent.duration._ 18 | 19 | class ElasticMQControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 20 | 21 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 22 | logger.debug(s"testTimeFactor = $testTimeFactor") 23 | 24 | val hostPorts: Seq[Int] = Seq(temporaryServerPort(), RandomPortUtil.temporaryServerPort()) 25 | val controller: ElasticMQController = ElasticMQController(dockerClient)(dockerHost, hostPorts) 26 | 27 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 28 | 29 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 30 | Map( 31 | controller -> WaitPredicateSetting( 32 | Duration.Inf, 33 | WaitPredicates.forListeningHostTcpPort( 34 | dockerHost, 35 | hostPorts.head, 36 | (1 * testTimeFactor).seconds, 37 | Some((5 * testTimeFactor).seconds) 38 | ) 39 | ) 40 | ) 41 | 42 | "ElasticMQController" - { 43 | "run" ignore { 44 | val client = AmazonSQSClientBuilder 45 | .standard() 46 | .withCredentials( 47 | new AWSCredentialsProviderChain(new AWSStaticCredentialsProvider(new BasicAWSCredentials("x", "x"))) 48 | ) 49 | .withEndpointConfiguration( 50 | new AwsClientBuilder.EndpointConfiguration( 51 | s"http://${dockerHost}:${hostPorts.head}", 52 | Regions.DEFAULT_REGION.getName 53 | ) 54 | ).build() 55 | 56 | val queueName = "test" 57 | val request = new CreateQueueRequest(queueName) 58 | .addAttributesEntry("VisibilityTimeout", "5") 59 | .addAttributesEntry("DelaySeconds", "1") 60 | 61 | val createQueueResult = client.createQueue(request) 62 | assert(createQueueResult.getSdkHttpMetadata.getHttpStatusCode == 200) 63 | val queueUrlResult = client.getQueueUrl(queueName) 64 | assert(queueUrlResult.getSdkHttpMetadata.getHttpStatusCode == 200) 65 | val queueUrl = queueUrlResult.getQueueUrl 66 | 67 | val setAttrsRequest = new SetQueueAttributesRequest() 68 | .withQueueUrl(queueUrl) 69 | .addAttributesEntry("ReceiveMessageWaitTimeSeconds", "5") 70 | val queueAttributesResult = client.setQueueAttributes(setAttrsRequest) 71 | assert(queueAttributesResult.getSdkHttpMetadata.getHttpStatusCode == 200) 72 | 73 | val text = UUID.randomUUID().toString 74 | val sendMessageRequest = new SendMessageRequest(queueUrl, text) 75 | val sendMessageResult = client.sendMessage(sendMessageRequest) 76 | assert(sendMessageResult.getSdkHttpMetadata.getHttpStatusCode == 200) 77 | 78 | val receiveMessageResult = client.receiveMessage(queueUrl) 79 | assert(receiveMessageResult.getSdkHttpMetadata.getHttpStatusCode == 200) 80 | assert(receiveMessageResult.getMessages.size() > 0) 81 | val message = receiveMessageResult.getMessages.get(0) 82 | assert(message.getBody == text) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docker-controller-scala-postgresql/src/main/scala/com/github/j5ik2o/dockerController/postgresql/PostgreSQLController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.postgresql 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.postgresql.PostgreSQLController.{ 9 | DefaultContainerPort, 10 | DefaultImageName, 11 | DefaultImageTag 12 | } 13 | 14 | import scala.concurrent.duration._ 15 | 16 | object PostgreSQLController { 17 | final val DefaultImageName: String = "postgres" 18 | final val DefaultImageTag: Option[String] = Some("17.6") 19 | final val DefaultContainerPort: Int = 5432 20 | 21 | def apply( 22 | dockerClient: DockerClient, 23 | isDockerClientAutoClose: Boolean = false, 24 | outputFrameInterval: FiniteDuration = 500.millis, 25 | imageName: String = DefaultImageName, 26 | imageTag: Option[String] = DefaultImageTag, 27 | envVars: Map[String, String] = Map.empty 28 | )( 29 | hostPort: Int, 30 | userName: String, 31 | password: Option[String] = None, 32 | databaseName: Option[String] = None, 33 | initDbArgs: Option[String] = None, 34 | initDbWalDir: Option[String] = None, 35 | hostAuthMethod: Option[String] = None, 36 | pgData: Option[String] = None 37 | ): PostgreSQLController = 38 | new PostgreSQLController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 39 | hostPort, 40 | userName, 41 | password, 42 | databaseName, 43 | initDbArgs, 44 | initDbWalDir, 45 | hostAuthMethod, 46 | pgData 47 | ) 48 | } 49 | 50 | class PostgreSQLController( 51 | dockerClient: DockerClient, 52 | isDockerClientAutoClose: Boolean = false, 53 | outputFrameInterval: FiniteDuration = 500.millis, 54 | imageName: String = DefaultImageName, 55 | imageTag: Option[String] = DefaultImageTag, 56 | envVars: Map[String, String] = Map.empty 57 | )( 58 | hostPort: Int, 59 | userName: String, 60 | password: Option[String] = None, 61 | databaseName: Option[String] = None, 62 | initDbArgs: Option[String] = None, 63 | initDbWalDir: Option[String] = None, 64 | hostAuthMethod: Option[String] = None, 65 | pgData: Option[String] = None 66 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 67 | 68 | private val environmentVariables: Map[String, String] = { 69 | envVars ++ 70 | Map[String, String]( 71 | "POSTGRES_USER" -> userName 72 | ) ++ 73 | password.map(s => Map("POSTGRES_PASSWORD" -> s)).getOrElse(Map.empty) ++ 74 | databaseName.map(s => Map("POSTGRES_DB" -> s)).getOrElse(Map.empty) ++ 75 | initDbArgs.map(s => Map("POSTGRES_INITDB_ARGS" -> s)).getOrElse(Map.empty) ++ 76 | initDbWalDir.map(s => Map("POSTGRES_INITDB_WALDIR" -> s)).getOrElse(Map.empty) ++ 77 | hostAuthMethod.map(s => Map("POSTGRES_HOST_AUTH_METHOD" -> s)).getOrElse(Map.empty) ++ 78 | pgData.map(s => Map("PGDATA" -> s)).getOrElse(Map.empty) 79 | } 80 | 81 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 82 | val containerPort = ExposedPort.tcp(DefaultContainerPort) 83 | val portBinding = new Ports() 84 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 85 | super 86 | .newCreateContainerCmd() 87 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 88 | .withExposedPorts(containerPort) 89 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/test/scala/com/github/j5ik2o/dockerController/DockerControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 5 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 6 | import com.github.dockerjava.core.{ DockerClientConfig, DockerClientImpl } 7 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient 8 | import org.apache.commons.io.IOUtils 9 | import org.scalatest.freespec.AnyFreeSpec 10 | import org.scalatest.{ BeforeAndAfter, BeforeAndAfterAll } 11 | import org.slf4j.{ Logger, LoggerFactory } 12 | 13 | import java.io.InputStream 14 | import java.net.{ HttpURLConnection, URL } 15 | import scala.concurrent.duration.Duration 16 | import scala.jdk.CollectionConverters._ 17 | 18 | class DockerControllerSpec extends AnyFreeSpec with BeforeAndAfter with BeforeAndAfterAll { 19 | 20 | val logger: Logger = LoggerFactory.getLogger(getClass) 21 | 22 | val dockerClientConfig: DockerClientConfig = DockerClientConfigUtil.buildConfigAwareOfDockerMachine() 23 | 24 | val dockerClient: DockerClient = { 25 | val httpClient: ApacheDockerHttpClient = new ApacheDockerHttpClient.Builder() 26 | .dockerHost(dockerClientConfig.getDockerHost).sslConfig(dockerClientConfig.getSSLConfig).build() 27 | DockerClientImpl.getInstance(dockerClientConfig, httpClient) 28 | } 29 | 30 | val host: String = 31 | if (dockerClientConfig.getDockerHost.getHost == null) 32 | "127.0.0.1" 33 | else 34 | dockerClientConfig.getDockerHost.getHost 35 | 36 | val hostPort: Int = RandomPortUtil.temporaryServerPort() 37 | 38 | logger.debug(s"host = $host") 39 | logger.debug(s"hostPort = $hostPort") 40 | 41 | var dockerController: DockerController = _ 42 | 43 | override protected def beforeAll(): Unit = { 44 | dockerController = DockerController(dockerClient, isDockerClientAutoClose = true)( 45 | imageName = "nginx", 46 | tag = Some("latest") 47 | ).configureCreateContainerCmd { cmd => 48 | val http = ExposedPort.tcp(80) 49 | val portBinding = new Ports() 50 | portBinding.bind(http, Ports.Binding.bindPort(hostPort)) 51 | cmd 52 | .withExposedPorts(http) 53 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 54 | } 55 | dockerController.pullImageIfNotExists() 56 | dockerController.createContainer() 57 | } 58 | 59 | override protected def afterAll(): Unit = { 60 | dockerController.dispose() 61 | } 62 | 63 | before { 64 | dockerController.startContainer() 65 | dockerController.awaitCondition(Duration.Inf)(_.toString.contains("Configuration complete; ready for start up")) 66 | Thread.sleep(1000) 67 | } 68 | 69 | after { 70 | dockerController.stopContainer() 71 | } 72 | 73 | val url = new URL(s"http://$host:$hostPort") 74 | 75 | def wget: Unit = { 76 | var connection: HttpURLConnection = null 77 | var in: InputStream = null 78 | try { 79 | connection = url.openConnection().asInstanceOf[HttpURLConnection] 80 | connection.setRequestMethod("GET") 81 | connection.connect() 82 | val responseCode = connection.getResponseCode 83 | assert(responseCode == HttpURLConnection.HTTP_OK) 84 | in = connection.getInputStream 85 | val lines = IOUtils.readLines(in, "UTF-8").asScala.mkString("\n") 86 | println(lines) 87 | } catch { 88 | case ex: Throwable => 89 | ex.printStackTrace() 90 | } finally { 91 | if (in != null) 92 | in.close() 93 | if (connection != null) 94 | connection.disconnect() 95 | } 96 | } 97 | 98 | "DockerController" - { 99 | "test-1" in { 100 | wget 101 | } 102 | "test-2" in { 103 | wget 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /docker-controller-scala-scalatest/src/main/scala/com/github/j5ik2o/dockerController/DockerControllerSpecSupport.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import org.scalatest._ 4 | 5 | trait DockerControllerSpecSupport extends SuiteMixin with DockerControllerHelper with RandomPortUtil { 6 | this: TestSuite => 7 | 8 | protected def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value = 9 | DockerContainerCreateRemoveLifecycle.ForEachTest 10 | 11 | protected def startStopLifecycle: DockerContainerStartStopLifecycle.Value = 12 | DockerContainerStartStopLifecycle.ForEachTest 13 | 14 | protected def createDockerContainers( 15 | createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value, 16 | testName: Option[String] 17 | ): Boolean = { 18 | if (this.createRemoveLifecycle == createRemoveLifecycle) { 19 | beforeCreateContainers() 20 | for (dockerController <- dockerControllers) { 21 | createDockerContainer(dockerController, testName) 22 | } 23 | afterCreateContainers() 24 | true 25 | } else false 26 | } 27 | 28 | protected def startDockerContainers( 29 | startStopLifecycle: DockerContainerStartStopLifecycle.Value, 30 | testName: Option[String] 31 | ): Boolean = { 32 | if (this.startStopLifecycle == startStopLifecycle) { 33 | beforeStartContainers() 34 | for (dockerController <- dockerControllers) { 35 | startDockerContainer(dockerController, testName) 36 | } 37 | afterStartContainers() 38 | true 39 | } else false 40 | } 41 | 42 | protected def stopDockerContainers( 43 | startStopLifecycle: DockerContainerStartStopLifecycle.Value, 44 | testName: Option[String] 45 | ): Boolean = { 46 | if (this.startStopLifecycle == startStopLifecycle) { 47 | beforeStopContainers() 48 | for (dockerController <- dockerControllers) { 49 | stopDockerContainer(dockerController, testName) 50 | } 51 | afterStopContainers() 52 | true 53 | } else false 54 | } 55 | 56 | protected def removeDockerContainers( 57 | createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value, 58 | testName: Option[String] 59 | ): Boolean = { 60 | if (this.createRemoveLifecycle == createRemoveLifecycle) { 61 | beforeRemoveContainers() 62 | for (dockerController <- dockerControllers) { 63 | removeDockerContainer(dockerController, testName) 64 | } 65 | afterRemoveContainers() 66 | true 67 | } else false 68 | } 69 | 70 | protected def beforeCreateContainers(): Unit = {} 71 | protected def afterCreateContainers(): Unit = {} 72 | protected def beforeStartContainers(): Unit = {} 73 | protected def afterStartContainers(): Unit = {} 74 | protected def beforeStopContainers(): Unit = {} 75 | protected def afterStopContainers(): Unit = {} 76 | protected def beforeRemoveContainers(): Unit = {} 77 | protected def afterRemoveContainers(): Unit = {} 78 | 79 | protected def disposeResources(): Unit = { 80 | dockerControllers.foreach(_.dispose()) 81 | logger.debug("dockerClient#close") 82 | dockerClient.close() 83 | } 84 | 85 | abstract override def run(testName: Option[String], args: Args): Status = { 86 | (createRemoveLifecycle, startStopLifecycle) match { 87 | case (DockerContainerCreateRemoveLifecycle.ForEachTest, DockerContainerStartStopLifecycle.ForAllTest) => 88 | throw new Error(s"Incorrect lifecycle settings: ($createRemoveLifecycle, $startStopLifecycle)") 89 | case _ => 90 | } 91 | if (expectedTestCount(args.filter) == 0) { 92 | new CompositeStatus(Set.empty) 93 | } else { 94 | var created = false 95 | var started = false 96 | try { 97 | created = createDockerContainers(DockerContainerCreateRemoveLifecycle.ForAllTest, testName) 98 | started = startDockerContainers(DockerContainerStartStopLifecycle.ForAllTest, testName) 99 | super.run(testName, args) 100 | } finally { 101 | try { 102 | if (started) 103 | stopDockerContainers(DockerContainerStartStopLifecycle.ForAllTest, testName) 104 | } finally { 105 | try { 106 | if (created) 107 | removeDockerContainers(DockerContainerCreateRemoveLifecycle.ForAllTest, testName) 108 | } finally { 109 | afterRemoveContainers() 110 | disposeResources() 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | abstract protected override def runTest(testName: String, args: Args): Status = { 118 | var created = false 119 | var started = false 120 | try { 121 | created = createDockerContainers(DockerContainerCreateRemoveLifecycle.ForEachTest, Some(testName)) 122 | started = startDockerContainers(DockerContainerStartStopLifecycle.ForEachTest, Some(testName)) 123 | super.runTest(testName, args) 124 | } finally { 125 | try { 126 | if (started) 127 | stopDockerContainers(DockerContainerStartStopLifecycle.ForEachTest, Some(testName)) 128 | } finally { 129 | if (created) 130 | removeDockerContainers(DockerContainerCreateRemoveLifecycle.ForEachTest, Some(testName)) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /docker-controller-scala-localstack/src/main/scala/com/github/j5ik2o/dockerController/localstack/LocalStackController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.localstack 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.CreateContainerCmd 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Ports } 7 | import com.github.j5ik2o.dockerController.DockerControllerImpl 8 | import com.github.j5ik2o.dockerController.localstack.LocalStackController._ 9 | 10 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 11 | 12 | object LocalStackController { 13 | final val DefaultImageName = "localstack/localstack" 14 | final val DefaultImageTag: Some[String] = Some("4.7") 15 | 16 | def apply( 17 | dockerClient: DockerClient, 18 | isDockerClientAutoClose: Boolean = false, 19 | outputFrameInterval: FiniteDuration = 500.millis, 20 | imageName: String = DefaultImageName, 21 | imageTag: Option[String] = DefaultImageTag, 22 | envVars: Map[String, String] = Map.empty 23 | )( 24 | services: Set[Service], 25 | edgeHostPort: Int, 26 | hostPorts: Map[Service, Int] = Map.empty, 27 | hostName: Option[String] = None, 28 | hostNameExternal: Option[String] = None, 29 | defaultRegion: Option[String] = None 30 | ): LocalStackController = 31 | new LocalStackController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 32 | services, 33 | edgeHostPort, 34 | hostPorts, 35 | hostName, 36 | hostNameExternal, 37 | defaultRegion 38 | ) 39 | } 40 | 41 | sealed abstract class Service(val entryName: String) 42 | 43 | object Service { 44 | 45 | case object ACM extends Service("acm") 46 | 47 | case object ApiGateway extends Service("apigateway") 48 | 49 | case object CloudFormation extends Service("cloudformation") 50 | 51 | case object CloudWatch extends Service("cloudwatch") 52 | 53 | case object CloudWatchLogs extends Service("logs") 54 | 55 | case object DynamoDB extends Service("dynamodb") 56 | 57 | case object DynamoDBStreams extends Service("dynamodbstreams") 58 | 59 | case object EC2 extends Service("ec2") 60 | 61 | case object Elasticsearch extends Service("es") 62 | 63 | case object EventBridge extends Service("eventbridge") 64 | 65 | case object Firehose extends Service("firehose") 66 | 67 | case object IAM extends Service("iam") 68 | 69 | case object Kinesis extends Service("kinesis") 70 | 71 | case object KMS extends Service("kms") 72 | 73 | case object Lambda extends Service("lambda") 74 | 75 | case object RedShift extends Service("redshift") 76 | 77 | case object Route53 extends Service("route53") 78 | 79 | case object S3 extends Service("s3") 80 | 81 | case object SecretManager extends Service("secretsmanager") 82 | 83 | case object SES extends Service("ses") 84 | 85 | case object SNS extends Service("sns") 86 | 87 | case object SQS extends Service("sqs") 88 | 89 | case object SSM extends Service("ssm") 90 | 91 | case object StepFunctions extends Service("stepfunctions") 92 | 93 | case object STS extends Service("sts") 94 | 95 | } 96 | 97 | class LocalStackController( 98 | dockerClient: DockerClient, 99 | isDockerClientAutoClose: Boolean = false, 100 | outputFrameInterval: FiniteDuration = 500.millis, 101 | imageName: String = DefaultImageName, 102 | imageTag: Option[String] = DefaultImageTag, 103 | envVars: Map[String, String] = Map.empty 104 | )( 105 | services: Set[Service], 106 | edgeHostPort: Int, 107 | hostPorts: Map[Service, Int], 108 | edgeBindHost: Option[String] = None, 109 | hostName: Option[String] = None, 110 | hostNameExternal: Option[String] = None, 111 | defaultRegion: Option[String] = None 112 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 113 | 114 | private val environmentVariables: Map[String, String] = Map( 115 | "EAGER_SERVICE_LOADING" -> "1", 116 | "SERVICES" -> services.map(_.entryName).mkString(",") 117 | ) ++ 118 | edgeBindHost.fold(Map.empty[String, String]) { e => Map("EDGE_BIND_HOST" -> e) } ++ 119 | hostName.fold(Map.empty[String, String]) { h => Map("HOSTNAME" -> h) } ++ 120 | hostNameExternal.fold(Map.empty[String, String]) { h => Map("HOSTNAME_EXTERNAL" -> h) } ++ 121 | defaultRegion.fold(Map.empty[String, String]) { r => Map("DEFAULT_REGION" -> r) } ++ 122 | hostPorts.foldLeft(Map.empty[String, String]) { case (result, (s, p)) => 123 | result ++ Map(s.entryName.toUpperCase + "_PORT_EXTERNAL" -> p.toString) 124 | } ++ 125 | envVars 126 | 127 | logger.debug(s"environmentVariables= $environmentVariables") 128 | 129 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 130 | val portBinding = new Ports() 131 | portBinding.bind(ExposedPort.tcp(4566), Ports.Binding.bindPort(edgeHostPort)) 132 | 133 | super 134 | .newCreateContainerCmd() 135 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 136 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerControllerHelper.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.core.{ DockerClientConfig, DockerClientImpl } 5 | import com.github.dockerjava.httpclient5.ApacheDockerHttpClient 6 | import com.github.dockerjava.transport.DockerHttpClient 7 | import com.github.j5ik2o.dockerController.WaitPredicates.WaitPredicate 8 | import org.slf4j.{ Logger, LoggerFactory } 9 | 10 | import scala.concurrent.duration.Duration 11 | 12 | trait DockerControllerHelper { 13 | 14 | case class WaitPredicateSetting(awaitDuration: Duration, waitPredicate: WaitPredicate) 15 | 16 | protected val logger: Logger = LoggerFactory.getLogger(getClass) 17 | 18 | protected val dockerClientConfig: DockerClientConfig = DockerClientConfigUtil.buildConfigAwareOfDockerMachine() 19 | 20 | protected val dockerHost: String = DockerClientConfigUtil.dockerHost(dockerClientConfig) 21 | 22 | logger.debug(s"DockerControllerHelper: Using Docker host: ${dockerClientConfig.getDockerHost}") 23 | logger.debug(s"DockerControllerHelper: Docker host URI: ${dockerClientConfig.getDockerHost.toString}") 24 | logger.debug(s"DockerControllerHelper: Docker host scheme: ${dockerClientConfig.getDockerHost.getScheme}") 25 | logger.debug(s"DockerControllerHelper: Docker host host: ${dockerClientConfig.getDockerHost.getHost}") 26 | logger.debug(s"DockerControllerHelper: Docker host port: ${dockerClientConfig.getDockerHost.getPort}") 27 | 28 | protected val dockerHttpClient: DockerHttpClient = new ApacheDockerHttpClient.Builder() 29 | .dockerHost(dockerClientConfig.getDockerHost) 30 | .sslConfig(dockerClientConfig.getSSLConfig) 31 | .build() 32 | 33 | protected val dockerClient: DockerClient = DockerClientImpl.getInstance(dockerClientConfig, dockerHttpClient) 34 | 35 | protected def dockerControllers: Vector[DockerController] 36 | 37 | protected def waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] 38 | 39 | protected def createDockerContainer( 40 | dockerController: DockerController, 41 | testName: Option[String] 42 | ): Unit = { 43 | logger.debug(s"createDockerContainer --- $testName") 44 | dockerController.pullImageIfNotExists() 45 | beforeDockerContainerCreate(dockerController, testName) 46 | dockerController.createContainer() 47 | afterDockerContainerCreated(dockerController, testName) 48 | } 49 | 50 | protected def startDockerContainer(dockerController: DockerController, testName: Option[String]): Unit = { 51 | logger.debug(s"startDockerContainer --- $testName") 52 | beforeDocketContainerStart(dockerController, testName) 53 | dockerController.startContainer() 54 | val waitPredicateOpt = waitPredicatesSettings.get(dockerController) 55 | waitPredicateOpt.foreach { waitPredicate => 56 | dockerController.awaitCondition(waitPredicate.awaitDuration)(waitPredicate.waitPredicate) 57 | } 58 | afterDocketContainerStarted(dockerController, testName) 59 | } 60 | 61 | protected def stopDockerContainer(dockerController: DockerController, testName: Option[String]): Unit = { 62 | logger.debug(s"stopDockerContainer --- $testName") 63 | beforeDockerContainerStop(dockerController, testName) 64 | dockerController.stopContainer() 65 | afterDockerContainerStopped(dockerController, testName) 66 | } 67 | 68 | protected def removeDockerContainer( 69 | dockerController: DockerController, 70 | testName: Option[String] 71 | ): Unit = { 72 | logger.debug(s"removeDockerContainer --- $testName") 73 | beforeDockerContainerRemove(dockerController, testName) 74 | dockerController.removeContainer() 75 | afterDockerContainerRemoved(dockerController, testName) 76 | } 77 | 78 | protected def beforeDockerContainerCreate(dockerController: DockerController, testName: Option[String]): Unit = { 79 | logger.debug(s"beforeDockerContainerCreate --- $testName") 80 | } 81 | 82 | protected def afterDockerContainerCreated(dockerController: DockerController, testName: Option[String]): Unit = { 83 | logger.debug(s"afterDockerContainerCreated --- $testName") 84 | } 85 | 86 | protected def beforeDockerContainerRemove(dockerController: DockerController, testName: Option[String]): Unit = { 87 | logger.debug(s"beforeDockerContainerRemove --- $testName") 88 | } 89 | 90 | protected def afterDockerContainerRemoved(dockerController: DockerController, testName: Option[String]): Unit = { 91 | logger.debug(s"afterDockerContainerRemoved --- $testName") 92 | } 93 | 94 | protected def beforeDocketContainerStart(dockerController: DockerController, testName: Option[String]): Unit = { 95 | logger.debug(s"beforeDocketContainerStart --- $testName") 96 | } 97 | 98 | protected def afterDocketContainerStarted(dockerController: DockerController, testName: Option[String]): Unit = { 99 | logger.debug(s"afterDocketContainerStarted --- $testName") 100 | } 101 | 102 | protected def beforeDockerContainerStop(dockerController: DockerController, testName: Option[String]): Unit = { 103 | logger.debug(s"beforeDockerContainerStopped --- $testName") 104 | } 105 | 106 | protected def afterDockerContainerStopped(dockerController: DockerController, testName: Option[String]): Unit = { 107 | logger.debug(s"afterDockerContainerStopped --- $testName") 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/WaitPredicates.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.model.Frame 4 | import org.slf4j.{ Logger, LoggerFactory } 5 | 6 | import java.net.{ HttpURLConnection, InetSocketAddress, Socket, URL } 7 | import scala.concurrent.duration.{ DurationInt, FiniteDuration } 8 | import scala.util.control.NonFatal 9 | import scala.util.matching.Regex 10 | 11 | object WaitPredicates { 12 | 13 | type WaitPredicate = Option[Frame] => Boolean 14 | 15 | protected val logger: Logger = LoggerFactory.getLogger(getClass) 16 | 17 | def forDebug( 18 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 19 | ): WaitPredicate = { frameOpt => 20 | frameOpt.exists { frame => 21 | val line = new String(frame.getPayload).stripLineEnd 22 | logger.debug(s"forDebug: line = $line") 23 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 24 | false 25 | } 26 | } 27 | 28 | def forLogMessageExactly( 29 | text: String, 30 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 31 | ): WaitPredicate = { frameOpt => 32 | frameOpt.exists { frame => 33 | val line = new String(frame.getPayload).stripLineEnd 34 | val result = line == text 35 | if (result) { 36 | logger.debug(s"forLogMessageExactly: result = $result, line = $line") 37 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 38 | } 39 | result 40 | } 41 | } 42 | 43 | def forLogMessageContained( 44 | text: String, 45 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 46 | ): WaitPredicate = { frameOpt => 47 | frameOpt.exists { frame => 48 | val line = new String(frame.getPayload).stripLineEnd 49 | val result = line.contains(text) 50 | if (result) { 51 | logger.debug(s"forLogMessageContained: result = $result, line = $line") 52 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 53 | } 54 | result 55 | } 56 | } 57 | 58 | def forLogMessageByRegex( 59 | regex: Regex, 60 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 61 | ): WaitPredicate = { frameOpt => 62 | frameOpt.exists { frame => 63 | val line = new String(frame.getPayload).stripLineEnd 64 | val result = regex.findFirstIn(line).isDefined 65 | if (result) { 66 | logger.debug(s"forLogMessageByRegex: result = $result, line = $line") 67 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 68 | } 69 | result 70 | } 71 | } 72 | 73 | def forListeningHostTcpPort( 74 | host: String, 75 | hostPort: Int, 76 | connectionTimeout: FiniteDuration = 500.milliseconds, 77 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 78 | ): WaitPredicate = { frameOpt => 79 | val line = frameOpt.map(frame => new String(frame.getPayload)).getOrElse("") 80 | val s: Socket = new Socket() 81 | try { 82 | s.connect(new InetSocketAddress(host, hostPort), connectionTimeout.toMillis.toInt) 83 | val result = s.isConnected 84 | if (result) { 85 | logger.debug(s"forListeningHostTcpPort: result = $result, line = $line") 86 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 87 | } 88 | result 89 | } catch { 90 | case NonFatal(_) => 91 | false 92 | } finally { 93 | if (s != null) 94 | s.close() 95 | } 96 | } 97 | 98 | def forListeningHttpPort( 99 | host: String, 100 | hostPort: Int, 101 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 102 | ): WaitPredicate = { _ => forListeningHttp(host, hostPort, awaitDurationOpt).isDefined } 103 | 104 | def forListeningHttpPortWithPredicate( 105 | host: String, 106 | hostPort: Int, 107 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 108 | )(p: HttpURLConnection => Boolean): WaitPredicate = { _ => 109 | forListeningHttp(host, hostPort, awaitDurationOpt).exists(p) 110 | } 111 | 112 | def forListeningHttpPortWithStatusOK( 113 | host: String, 114 | hostPort: Int, 115 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 116 | ): WaitPredicate = { 117 | forListeningHttpPortWithPredicate(host, hostPort, awaitDurationOpt)(_.getResponseCode == 200) 118 | } 119 | 120 | private def forListeningHttp( 121 | host: String, 122 | hostPort: Int, 123 | awaitDurationOpt: Option[FiniteDuration] = Some(500.milliseconds) 124 | ): Option[HttpURLConnection] = { 125 | var connection: HttpURLConnection = null 126 | try { 127 | val url = new URL(s"http://$host:$hostPort") 128 | logger.debug("try: HttpURLConnection#openConnection ...") 129 | connection = url.openConnection().asInstanceOf[HttpURLConnection] 130 | connection.setRequestMethod("GET") 131 | connection.connect() 132 | logger.debug("connected: HttpURLConnection#openConnection") 133 | awaitDurationOpt.foreach { awaitDuration => Thread.sleep(awaitDuration.toMillis) } 134 | Some(connection) 135 | } catch { 136 | case NonFatal(ex) => 137 | logger.debug("occurred error", ex) 138 | None 139 | } finally { 140 | if (connection != null) 141 | connection.disconnect() 142 | } 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /docker-controller-scala-dynamodb-local/src/test/scala/com/github/j5ik2o/dockerController/dynamodbLocal/DynamoDBLocalControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.dynamodbLocal 2 | 3 | import com.amazonaws.auth.{ AWSStaticCredentialsProvider, BasicAWSCredentials } 4 | import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration 5 | import com.amazonaws.regions.Regions 6 | import com.amazonaws.services.dynamodbv2.model._ 7 | import com.amazonaws.services.dynamodbv2.{ AmazonDynamoDB, AmazonDynamoDBClientBuilder } 8 | import com.github.j5ik2o.dockerController.WaitPredicates.WaitPredicate 9 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 10 | import org.scalatest.freespec.AnyFreeSpec 11 | 12 | import scala.concurrent.duration._ 13 | import scala.jdk.CollectionConverters._ 14 | 15 | class DynamoDBLocalControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 16 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 17 | logger.debug(s"testTimeFactor = $testTimeFactor") 18 | 19 | val hostPort: Int = temporaryServerPort() 20 | val controller: DynamoDBLocalController = new DynamoDBLocalController(dockerClient, imageTag = None)(hostPort) 21 | 22 | // val waitPredicate: WaitPredicate = WaitPredicates.forListeningHostTcpPort(dockerHost, hostPort) 23 | val waitPredicate: WaitPredicate = WaitPredicates.forLogMessageByRegex( 24 | DynamoDBLocalController.RegexOfWaitPredicate, 25 | Some((1 * testTimeFactor).seconds) 26 | ) 27 | 28 | val waitPredicateSetting: WaitPredicateSetting = WaitPredicateSetting(Duration.Inf, waitPredicate) 29 | 30 | val tableName = "test" 31 | 32 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 33 | 34 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 35 | Map( 36 | controller -> waitPredicateSetting 37 | ) 38 | 39 | val dynamoDBEndpoint: String = s"http://$dockerHost:$hostPort" 40 | val dynamoDBRegion: Regions = Regions.AP_NORTHEAST_1 41 | val dynamoDBAccessKeyId: String = "x" 42 | val dynamoDBSecretAccessKey: String = "x" 43 | 44 | protected val dynamoDBClient: AmazonDynamoDB = { 45 | AmazonDynamoDBClientBuilder 46 | .standard() 47 | .withEndpointConfiguration(new EndpointConfiguration(dynamoDBEndpoint, dynamoDBRegion.getName)) 48 | .withCredentials( 49 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(dynamoDBAccessKeyId, dynamoDBSecretAccessKey)) 50 | ) 51 | .build() 52 | } 53 | 54 | protected def createTable(): Unit = { 55 | val listTablesResult = dynamoDBClient.listTables(2) 56 | if (!listTablesResult.getTableNames.asScala.exists(_.contains(tableName))) { 57 | val createRequest = new CreateTableRequest() 58 | .withTableName(tableName) 59 | .withAttributeDefinitions( 60 | Seq( 61 | new AttributeDefinition().withAttributeName("pkey").withAttributeType(ScalarAttributeType.S), 62 | new AttributeDefinition().withAttributeName("skey").withAttributeType(ScalarAttributeType.S), 63 | new AttributeDefinition().withAttributeName("persistence-id").withAttributeType(ScalarAttributeType.S), 64 | new AttributeDefinition().withAttributeName("sequence-nr").withAttributeType(ScalarAttributeType.N), 65 | new AttributeDefinition().withAttributeName("tags").withAttributeType(ScalarAttributeType.S) 66 | ).asJava 67 | ).withKeySchema( 68 | Seq( 69 | new KeySchemaElement().withAttributeName("pkey").withKeyType(KeyType.HASH), 70 | new KeySchemaElement().withAttributeName("skey").withKeyType(KeyType.RANGE) 71 | ).asJava 72 | ).withProvisionedThroughput( 73 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 74 | ).withGlobalSecondaryIndexes( 75 | Seq( 76 | new GlobalSecondaryIndex() 77 | .withIndexName("TagsIndex").withKeySchema( 78 | Seq( 79 | new KeySchemaElement().withAttributeName("tags").withKeyType(KeyType.HASH) 80 | ).asJava 81 | ).withProjection(new Projection().withProjectionType(ProjectionType.ALL)) 82 | .withProvisionedThroughput( 83 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 84 | ), 85 | new GlobalSecondaryIndex() 86 | .withIndexName("GetJournalRowsIndex").withKeySchema( 87 | Seq( 88 | new KeySchemaElement().withAttributeName("persistence-id").withKeyType(KeyType.HASH), 89 | new KeySchemaElement().withAttributeName("sequence-nr").withKeyType(KeyType.RANGE) 90 | ).asJava 91 | ).withProjection(new Projection().withProjectionType(ProjectionType.ALL)) 92 | .withProvisionedThroughput( 93 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 94 | ) 95 | ).asJava 96 | ).withStreamSpecification( 97 | new StreamSpecification().withStreamEnabled(true).withStreamViewType(StreamViewType.NEW_IMAGE) 98 | ) 99 | val createResponse = dynamoDBClient.createTable(createRequest) 100 | require(createResponse.getSdkHttpMetadata.getHttpStatusCode == 200) 101 | } 102 | } 103 | 104 | "DynamoDBLocalController" - { 105 | "run-1" in { 106 | createTable() 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docker-controller-scala-kafka/src/test/scala/com/github/j5ik2o/dockerController/kafka/KafkaControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.kafka 2 | 3 | import com.github.j5ik2o.dockerController.WaitPredicates.WaitPredicate 4 | import com.github.j5ik2o.dockerController._ 5 | import org.apache.kafka.clients.consumer.{ ConsumerConfig, KafkaConsumer } 6 | import org.apache.kafka.clients.producer.{ KafkaProducer, ProducerConfig, ProducerRecord } 7 | import org.apache.kafka.common.TopicPartition 8 | import org.apache.kafka.common.serialization.{ StringDeserializer, StringSerializer } 9 | import org.scalatest.freespec.AnyFreeSpec 10 | 11 | import java.time.{ Duration => JavaDuration, LocalDateTime } 12 | import java.util.{ Collections, Properties } 13 | import scala.concurrent.duration._ 14 | import scala.util.control.Breaks 15 | 16 | class KafkaControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 17 | val testTimeFactor: Int = sys.env.getOrElse("TEST_TIME_FACTOR", "1").toInt 18 | logger.debug(s"testTimeFactor = $testTimeFactor") 19 | 20 | val topicName = "mytopic" 21 | 22 | val kafkaExternalHostPort: Int = temporaryServerPort() 23 | 24 | val kafkaController = new KafkaController(dockerClient)( 25 | kafkaExternalHostName = dockerHost, 26 | kafkaExternalHostPort = kafkaExternalHostPort, 27 | createTopics = Seq(topicName) 28 | ) 29 | 30 | override protected val dockerControllers: Vector[DockerController] = Vector(kafkaController) 31 | 32 | val kafkaWaitPredicate: WaitPredicate = 33 | WaitPredicates.forLogMessageByRegex(KafkaController.RegexForWaitPredicate, Some((1 * testTimeFactor).seconds)) 34 | val kafkaWaitPredicateSetting: WaitPredicateSetting = WaitPredicateSetting(Duration.Inf, kafkaWaitPredicate) 35 | 36 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = { 37 | Map( 38 | kafkaController -> kafkaWaitPredicateSetting 39 | ) 40 | } 41 | 42 | "KafkaController" - { 43 | "produce&consume" in { 44 | val consumerRunnable = new Runnable { 45 | override def run(): Unit = { 46 | val consumerProperties = new Properties() 47 | consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, s"$dockerHost:$kafkaExternalHostPort") 48 | consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") 49 | 50 | consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, "myConsumerGroup") 51 | consumerProperties.put( 52 | ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 53 | classOf[StringDeserializer].getName 54 | ) 55 | consumerProperties.put( 56 | ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 57 | classOf[StringDeserializer].getName 58 | ) 59 | val consumer = new KafkaConsumer[String, String](consumerProperties) 60 | consumer.subscribe(Collections.singletonList(topicName)) 61 | val b = new Breaks 62 | b.breakable { 63 | while (true) { 64 | try { 65 | logger.debug("consumer:=============================") 66 | val records = consumer.poll(JavaDuration.ofMillis(1000)) 67 | logger.debug("consumer:=============================") 68 | logger.debug("[record size] " + records.count()); 69 | records.forEach { record => 70 | logger.debug("consumer:=============================") 71 | logger.debug("consumer:" + LocalDateTime.now) 72 | logger.debug("consumer:topic: " + record.topic) 73 | logger.debug("consumer:partition: " + record.partition) 74 | logger.debug("consumer:key: " + record.key) 75 | logger.debug("consumer:value: " + record.value) 76 | logger.debug("consumer:offset: " + record.offset) 77 | val topicPartition = new TopicPartition(record.topic, record.partition) 78 | val offsetAndMetadataMap = consumer.committed(java.util.Collections.singleton(topicPartition)) 79 | val offsetAndMetadata = offsetAndMetadataMap.get(topicPartition) 80 | if (offsetAndMetadata != null) 81 | logger.debug("partition offset: " + offsetAndMetadata.offset) 82 | } 83 | } catch { 84 | case ex: org.apache.kafka.common.errors.InterruptException => 85 | logger.warn("occurred error", ex) 86 | b.break() 87 | case ex: InterruptedException => 88 | logger.warn("occurred error", ex) 89 | b.break() 90 | } 91 | } 92 | } 93 | try { 94 | consumer.close() 95 | } catch { 96 | case ex: org.apache.kafka.common.errors.InterruptException => 97 | logger.warn("occurred error", ex) 98 | case ex: InterruptedException => 99 | logger.warn("occurred error", ex) 100 | } 101 | } 102 | } 103 | val t = new Thread(consumerRunnable) 104 | t.start() 105 | val producerProperties = new Properties() 106 | producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, s"$dockerHost:$kafkaExternalHostPort") 107 | producerProperties.put( 108 | ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, 109 | classOf[StringSerializer].getName 110 | ) 111 | producerProperties.put( 112 | ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, 113 | classOf[StringSerializer].getName 114 | ) 115 | val producer = new KafkaProducer[String, String](producerProperties) 116 | (1 to 10).foreach { n => 117 | val record = new ProducerRecord[String, String](topicName, "my-value-" + n) 118 | val send = producer.send(record) 119 | val recordMetadata = send.get 120 | logger.debug("producer:=============================") 121 | logger.debug("producer:" + LocalDateTime.now) 122 | logger.debug("producer:topic: " + recordMetadata.topic) 123 | logger.debug("producer:partition: " + recordMetadata.partition) 124 | logger.debug("producer:offset: " + recordMetadata.offset) 125 | } 126 | producer.close() 127 | Thread.sleep(1000 * 10) 128 | t.interrupt() 129 | t.join() 130 | } 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /docker-controller-scala-kafka/src/main/scala/com/github/j5ik2o/dockerController/kafka/KafkaController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.kafka 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command._ 5 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 6 | import com.github.dockerjava.api.model.{ ExposedPort, Frame, Ports } 7 | import com.github.j5ik2o.dockerController.WaitPredicates.WaitPredicate 8 | import com.github.j5ik2o.dockerController.kafka.KafkaController._ 9 | import com.github.j5ik2o.dockerController.zooKeeper.ZooKeeperController 10 | import com.github.j5ik2o.dockerController._ 11 | 12 | import java.util.UUID 13 | import scala.concurrent.duration.{ Duration, DurationInt, FiniteDuration } 14 | import scala.util.matching.Regex 15 | 16 | object KafkaController { 17 | final val DefaultImageName: String = "wurstmeister/kafka" 18 | final val DefaultImageTag: Option[String] = Some("2.13-2.6.0") 19 | final val RegexForWaitPredicate: Regex = """.*\[KafkaServer id=\d\] started.*""".r 20 | 21 | def apply( 22 | dockerClient: DockerClient, 23 | isDockerClientAutoClose: Boolean = false, 24 | outputFrameInterval: FiniteDuration = 500.millis, 25 | imageName: String = DefaultImageName, 26 | imageTag: Option[String] = DefaultImageTag, 27 | envVars: Map[String, String] = Map.empty 28 | )( 29 | kafkaExternalHostName: String, 30 | kafkaExternalHostPort: Int, 31 | createTopics: Seq[String] = Seq.empty 32 | ): KafkaController = 33 | new KafkaController(dockerClient, isDockerClientAutoClose, outputFrameInterval, imageName, imageTag, envVars)( 34 | kafkaExternalHostName, 35 | kafkaExternalHostPort, 36 | createTopics 37 | ) 38 | } 39 | 40 | class KafkaController( 41 | dockerClient: DockerClient, 42 | isDockerClientAutoClose: Boolean = false, 43 | outputFrameInterval: FiniteDuration = 500.millis, 44 | imageName: String = DefaultImageName, 45 | imageTag: Option[String] = DefaultImageTag, 46 | envVars: Map[String, String] = Map.empty 47 | )( 48 | kafkaExternalHostName: String, 49 | kafkaExternalHostPort: Int, 50 | createTopics: Seq[String] 51 | ) extends DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, imageTag) { 52 | lazy val networkId: String = 53 | dockerClient.createNetworkCmd().withName("kafka-" + UUID.randomUUID().toString).exec().getId 54 | 55 | lazy val kafkaNetwork: Network = Network(networkId) 56 | lazy val zkAlias: NetworkAlias = NetworkAlias(kafkaNetwork, "zk1") 57 | lazy val kafkaAlias: NetworkAlias = NetworkAlias(kafkaNetwork, "kafka1") 58 | 59 | lazy val zooKeeperHostPort: Int = RandomPortUtil.temporaryServerPort() 60 | 61 | lazy val zooKeeperController: ZooKeeperController = ZooKeeperController(dockerClient)( 62 | myId = 1, 63 | hostPort = zooKeeperHostPort, 64 | containerPort = ZooKeeperController.DefaultZooPort, 65 | networkAlias = Some(zkAlias) 66 | ) 67 | 68 | protected val zooKeeperWaitPredicate: WaitPredicate = 69 | WaitPredicates.forLogMessageByRegex(ZooKeeperController.RegexForWaitPredicate) 70 | 71 | private lazy val kafkaContainerName = kafkaAlias.name 72 | private lazy val zooKeeperContainerName = zkAlias.name 73 | private lazy val zooKeeperContainerPort = zooKeeperController.containerPort 74 | 75 | private lazy val environmentVariables = Map( 76 | "KAFKA_AUTO_CREATE_TOPICS_ENABLE" -> (if (createTopics.isEmpty) "false" else "true"), 77 | "KAFKA_CREATE_TOPICS" -> createTopics.mkString(","), 78 | "KAFKA_BROKER_ID" -> "1", 79 | "KAFKA_ADVERTISED_LISTENERS" -> s"LISTENER_DOCKER_INTERNAL://$kafkaContainerName:19092,LISTENER_DOCKER_EXTERNAL://$kafkaExternalHostName:$kafkaExternalHostPort", 80 | "KAFKA_LISTENERS" -> s"LISTENER_DOCKER_INTERNAL://:19092,LISTENER_DOCKER_EXTERNAL://:$kafkaExternalHostPort", 81 | "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" -> "LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT", 82 | "KAFKA_INTER_BROKER_LISTENER_NAME" -> "LISTENER_DOCKER_INTERNAL", 83 | "KAFKA_ZOOKEEPER_CONNECT" -> s"$zooKeeperContainerName:$zooKeeperContainerPort", 84 | "KAFKA_LOG4J_LOGGERS" -> "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO", 85 | "KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR" -> "1" 86 | ) ++ envVars 87 | 88 | override def createContainer(f: CreateContainerCmd => CreateContainerCmd): CreateContainerResponse = { 89 | zooKeeperController.pullImageIfNotExists() 90 | zooKeeperController.createContainer() 91 | super.createContainer(f) 92 | } 93 | 94 | override def startContainer(f: StartContainerCmd => StartContainerCmd): Unit = { 95 | zooKeeperController.startContainer() 96 | super.startContainer(f) 97 | } 98 | 99 | override def stopContainer(f: StopContainerCmd => StopContainerCmd): Unit = { 100 | super.stopContainer(f) 101 | zooKeeperController.stopContainer() 102 | } 103 | 104 | override protected def newRemoveContainerCmd(): RemoveContainerCmd = { 105 | require(containerId.isDefined) 106 | dockerClient.removeContainerCmd(containerId.get).withForce(true) 107 | } 108 | 109 | override def removeContainer(f: RemoveContainerCmd => RemoveContainerCmd): Unit = { 110 | try { 111 | super.removeContainer(f) 112 | } finally { 113 | try { 114 | zooKeeperController.removeContainer() 115 | } finally { 116 | dockerClient.removeNetworkCmd(networkId).exec() 117 | } 118 | } 119 | } 120 | 121 | override def awaitCondition(duration: Duration)(predicate: Option[Frame] => Boolean): Unit = { 122 | zooKeeperController.awaitCondition(duration)(zooKeeperWaitPredicate) 123 | super.awaitCondition(duration)(predicate) 124 | } 125 | 126 | override protected def newCreateContainerCmd(): CreateContainerCmd = { 127 | val containerPort = ExposedPort.tcp(kafkaExternalHostPort) 128 | val portBinding = new Ports() 129 | portBinding.bind(containerPort, Ports.Binding.bindPort(kafkaExternalHostPort)) 130 | val hostConfig = newHostConfig().withPortBindings(portBinding).withNetworkMode(kafkaAlias.network.id) 131 | super 132 | .newCreateContainerCmd() 133 | .withEnv(environmentVariables.map { case (k, v) => s"$k=$v" }.toArray: _*) 134 | .withExposedPorts(containerPort) 135 | .withHostConfig(hostConfig).withAliases(kafkaAlias.name) 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /docker-controller-scala-localstack/src/test/scala/com/github/j5ik2o/dockerController/localstack/LocalStackControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController.localstack 2 | 3 | import com.amazonaws.auth.{ AWSStaticCredentialsProvider, BasicAWSCredentials } 4 | import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration 5 | import com.amazonaws.regions.Regions 6 | import com.amazonaws.services.dynamodbv2.model.{ 7 | AttributeDefinition, 8 | CreateTableRequest, 9 | GlobalSecondaryIndex, 10 | KeySchemaElement, 11 | KeyType, 12 | Projection, 13 | ProjectionType, 14 | ProvisionedThroughput, 15 | ScalarAttributeType, 16 | StreamSpecification, 17 | StreamViewType 18 | } 19 | import com.amazonaws.services.dynamodbv2.{ AmazonDynamoDB, AmazonDynamoDBClientBuilder } 20 | import com.amazonaws.services.s3.model.CreateBucketRequest 21 | import com.amazonaws.services.s3.{ AmazonS3, AmazonS3Client } 22 | import com.github.j5ik2o.dockerController.{ DockerController, DockerControllerSpecSupport, WaitPredicates } 23 | import org.scalatest.freespec.AnyFreeSpec 24 | 25 | import scala.concurrent.duration.Duration 26 | import scala.jdk.CollectionConverters._ 27 | 28 | class LocalStackControllerSpec extends AnyFreeSpec with DockerControllerSpecSupport { 29 | val accessKeyId: String = "AKIAIOSFODNN7EXAMPLE" 30 | val secretAccessKey: String = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 31 | val hostPort: Int = temporaryServerPort() 32 | val endpointForS3: String = s"http://$dockerHost:$hostPort" 33 | val endpointForDynamoDB: String = s"http://$dockerHost:$hostPort" 34 | val region: Regions = Regions.AP_NORTHEAST_1 35 | 36 | val controller: LocalStackController = 37 | LocalStackController(dockerClient)( 38 | services = Set(Service.S3, Service.DynamoDB), 39 | edgeHostPort = hostPort, 40 | hostNameExternal = Some(dockerHost), 41 | defaultRegion = Some(region.getName) 42 | ) 43 | 44 | override protected val dockerControllers: Vector[DockerController] = Vector(controller) 45 | 46 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 47 | Map( 48 | controller -> WaitPredicateSetting(Duration.Inf, WaitPredicates.forLogMessageExactly("Ready.")) 49 | ) 50 | 51 | val bucketName = "test" 52 | val tableName = "test" 53 | 54 | protected val s3Client: AmazonS3 = { 55 | AmazonS3Client 56 | .builder() 57 | .withEndpointConfiguration(new EndpointConfiguration(endpointForS3, region.getName)) 58 | .withCredentials( 59 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretAccessKey)) 60 | ) 61 | .withPathStyleAccessEnabled(true) 62 | .build() 63 | } 64 | 65 | protected def createBucket(): Unit = { 66 | if (!s3Client.listBuckets().asScala.exists(_.getName == bucketName)) { 67 | val request = new CreateBucketRequest(bucketName, region.getName) 68 | s3Client.createBucket(request) 69 | logger.info(s"bucket created: $bucketName") 70 | } 71 | while (!s3Client.listBuckets().asScala.exists(_.getName == bucketName)) { 72 | logger.info(s"Waiting for the bucket to be created: $bucketName") 73 | Thread.sleep(500) 74 | } 75 | } 76 | 77 | protected val dynamoDBClient: AmazonDynamoDB = { 78 | AmazonDynamoDBClientBuilder 79 | .standard() 80 | .withEndpointConfiguration(new EndpointConfiguration(endpointForDynamoDB, region.getName)) 81 | .withCredentials( 82 | new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretAccessKey)) 83 | ) 84 | .build() 85 | } 86 | 87 | protected def createTable(): Unit = { 88 | val listTablesResult = dynamoDBClient.listTables(2) 89 | if (!listTablesResult.getTableNames.asScala.exists(_.contains(tableName))) { 90 | val createRequest = new CreateTableRequest() 91 | .withTableName(tableName) 92 | .withAttributeDefinitions( 93 | Seq( 94 | new AttributeDefinition().withAttributeName("pkey").withAttributeType(ScalarAttributeType.S), 95 | new AttributeDefinition().withAttributeName("skey").withAttributeType(ScalarAttributeType.S), 96 | new AttributeDefinition().withAttributeName("persistence-id").withAttributeType(ScalarAttributeType.S), 97 | new AttributeDefinition().withAttributeName("sequence-nr").withAttributeType(ScalarAttributeType.N), 98 | new AttributeDefinition().withAttributeName("tags").withAttributeType(ScalarAttributeType.S) 99 | ).asJava 100 | ).withKeySchema( 101 | Seq( 102 | new KeySchemaElement().withAttributeName("pkey").withKeyType(KeyType.HASH), 103 | new KeySchemaElement().withAttributeName("skey").withKeyType(KeyType.RANGE) 104 | ).asJava 105 | ).withProvisionedThroughput( 106 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 107 | ).withGlobalSecondaryIndexes( 108 | Seq( 109 | new GlobalSecondaryIndex() 110 | .withIndexName("TagsIndex").withKeySchema( 111 | Seq( 112 | new KeySchemaElement().withAttributeName("tags").withKeyType(KeyType.HASH) 113 | ).asJava 114 | ).withProjection(new Projection().withProjectionType(ProjectionType.ALL)) 115 | .withProvisionedThroughput( 116 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 117 | ), 118 | new GlobalSecondaryIndex() 119 | .withIndexName("GetJournalRowsIndex").withKeySchema( 120 | Seq( 121 | new KeySchemaElement().withAttributeName("persistence-id").withKeyType(KeyType.HASH), 122 | new KeySchemaElement().withAttributeName("sequence-nr").withKeyType(KeyType.RANGE) 123 | ).asJava 124 | ).withProjection(new Projection().withProjectionType(ProjectionType.ALL)) 125 | .withProvisionedThroughput( 126 | new ProvisionedThroughput().withReadCapacityUnits(10L).withWriteCapacityUnits(10L) 127 | ) 128 | ).asJava 129 | ).withStreamSpecification( 130 | new StreamSpecification().withStreamEnabled(true).withStreamViewType(StreamViewType.NEW_IMAGE) 131 | ) 132 | val createResponse = dynamoDBClient.createTable(createRequest) 133 | require(createResponse.getSdkHttpMetadata.getHttpStatusCode == 200) 134 | } 135 | } 136 | 137 | "LocalStackController" - { 138 | "run" in { 139 | createBucket() 140 | createTable() 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-controller-scala 2 | 3 | [![Actions Status: CI](https://github.com/j5ik2o/docker-controller-scala/workflows/CI/badge.svg)](https://github.com/j5ik2o/docker-controller-scala/actions?query=workflow%3A"CI") 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.j5ik2o/docker-controller-scala-core_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.j5ik2o/docker-controller-scala-core_2.13) 5 | [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) 6 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 7 | [![Tokei](https://tokei.rs/b1/github/j5ik2o/event-store-adapter-scala)](https://github.com/XAMPPRocky/tokei) 8 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fj5ik2o%2Fdocker-controller-scala.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fj5ik2o%2Fdocker-controller-scala?ref=badge_shield) 9 | 10 | This library provides an easy and simple way to handle Docker Container or Docker Compose on ScalaTest, based on [docker-java](https://github.com/docker-java/docker-java). The implementation of this library is thin, and if you know [docker-java](https://github.com/docker-java/docker-java), your learning cost will be negligible. 11 | 12 | M1 Macs also be supported. 13 | 14 | ## Installation 15 | 16 | Add the following to your sbt build (2.12.x, 2.13.x, 3.0.x): 17 | 18 | ```scala 19 | val version = "..." 20 | 21 | libraryDependencies += Seq( 22 | "io.github.j5ik2o" %% "docker-controller-scala-core" % version, 23 | "io.github.j5ik2o" %% "docker-controller-scala-scalatest" % version, // for scalatest 24 | // RDB 25 | "io.github.j5ik2o" %% "docker-controller-scala-mysql" % version, // optional 26 | "io.github.j5ik2o" %% "docker-controller-scala-postgresql" % version, // optional 27 | "io.github.j5ik2o" %% "docker-controller-scala-flyway" % version, // optional 28 | // NoSQL 29 | "io.github.j5ik2o" %% "docker-controller-scala-memcached" % version, // optional 30 | "io.github.j5ik2o" %% "docker-controller-scala-redis" % version, // optional 31 | "io.github.j5ik2o" %% "docker-controller-scala-elasticsearch" % version, // optional 32 | // Kafka 33 | "io.github.j5ik2o" %% "docker-controller-scala-zookeeper" % version, // optional 34 | "io.github.j5ik2o" %% "docker-controller-scala-kafka" % version, // optional 35 | // AWS Services 36 | "io.github.j5ik2o" %% "docker-controller-scala-dynamodb-local" % version, // optional 37 | "io.github.j5ik2o" %% "docker-controller-scala-minio" % version, // optional 38 | "io.github.j5ik2o" %% "docker-controller-scala-localstack" % version, // optional 39 | "io.github.j5ik2o" %% "docker-controller-scala-elasticmq" % version, // optional 40 | ) 41 | ``` 42 | 43 | In most cases, you can just select the scalatest module and the module you need. 44 | 45 | ```scala 46 | libraryDependencies += Seq( 47 | "io.github.j5ik2o" %% "docker-controller-scala-scalatest" % version, 48 | "io.github.j5ik2o" %% "docker-controller-scala-mysql" % version, 49 | ) 50 | ``` 51 | 52 | ## Usage 53 | 54 | [DockerController](docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerController.scala) that the thin wrapper for [docker-java](https://github.com/docker-java/docker-java) controls Docker Image and Docker Container for testing. 55 | 56 | ### How to test with preset DockerController 57 | 58 | The `DockerController` for the corresponding preset is as follows. Please see the corresponding `**Spec` for specific usage. 59 | 60 | - RDBMS 61 | - [MySQLController](docker-controller-scala-mysql/src/main/scala/com/github/j5ik2o/dockerController/mysql/MySQLController.scala) / [MySQLControllerSpec](docker-controller-scala-mysql/src/test/scala/com/github/j5ik2o/dockerController/MySQLControllerSpec.scala) 62 | - [PostgreSQLController](docker-controller-scala-postgresql/src/main/scala/com/github/j5ik2o/dockerController/postgresql/PostgreSQLController.scala) / [PostgreSQLControllerSpec](docker-controller-scala-postgresql/src/test/scala/com/github/j5ik2o/dockerController/postgresql/PostgreSQLControllerSpec.scala) 63 | - NoSQL 64 | - [MemcachedController](docker-controller-scala-memcached/src/main/scala/com/github/j5ik2o/dockerController/memcached/MemcachedController.scala) / [MemcachedControllerSpec](docker-controller-scala-memcached/src/test/scala/com/github/j5ik2o/dockerController/memcached/MemcachedControllerSpec.scala) 65 | - [RedisController](docker-controller-scala-redis/src/main/scala/com/github/j5ik2o/dockerController/redis/RedisController.scala) / [RedisControllerSpec](docker-controller-scala-redis/src/test/scala/com/github/j5ik2o/dockerController/redis/RedisControllerSpec.scala) 66 | - [ElasticsearchController](docker-controller-scala-elasticsearch/src/main/scala/com/github/j5ik2o/dockerController/elasticsearch/ElasticsearchController.scala) / [ElasticsearchControllerSpec](docker-controller-scala-elasticsearch/src/test/scala/com/github/j5ik2o/dockerController/elasticsearch/ElasticsearchControllerSpec.scala) 67 | - [ZooKeeperController](docker-controller-scala-zookeeper/src/main/scala/com/github/j5ik2o/dockerController/zooKeeper/ZooKeeperController.scala) / [ZooKeeperControllerSpec](docker-controller-scala-zookeeper/src/test/scala/com/github/j5ik2o/dockerController/ZooKeeperControllerSpec.scala) 68 | - [KafkaController](docker-controller-scala-kafka/src/main/scala/com/github/j5ik2o/dockerController/kafka/KafkaController.scala) / [KafkaControllerSpec](docker-controller-scala-kafka/src/test/scala/com/github/j5ik2o/dockerController/kafka/KafkaControllerSpec.scala) 69 | - AWS Storages 70 | - [LocalStackController](docker-controller-scala-localstack/src/main/scala/com/github/j5ik2o/dockerController/localstack/LocalStackController.scala) / [LocalStackControllerSpec](docker-controller-scala-localstack/src/test/scala/com/github/j5ik2o/dockerController/localstack/LocalStackControllerSpec.scala) 71 | - [DynamoDBLocalController](docker-controller-scala-dynamodb-local/src/main/scala/com/github/j5ik2o/dockerController/dynamodbLocal/DynamoDBLocalController.scala) / [DynamoDBLocalControllerSpec](docker-controller-scala-dynamodb-local/src/test/scala/com/github/j5ik2o/dockerController/dynamodbLocal/DynamoDBLocalControllerSpec.scala) 72 | - [MinioController](docker-controller-scala-minio/src/main/scala/com/github/j5ik2o/dockerController/minio/MinioController.scala) / [MinioControllerSpec](docker-controller-scala-minio/src/test/scala/com/github/j5ik2o/dockerController/minio/MinioControllerSpec.scala) 73 | - [ElasticMQController](docker-controller-scala-elasticmq/src/main/scala/com/github/j5ik2o/dockerController/elasticmq/ElasticMQController.scala) / [ElasticMQControllerSpec](docker-controller-scala-elasticmq/src/test/scala/com/github/j5ik2o/dockerController/elasticmq/ElasticMQControllerSpec.scala) 74 | 75 | ### Use Flyway Migrate Command on MySQL/PostgreSQL 76 | 77 | If you'd like to use `flyway` module, you can use `docker-controller-scala-flyway`. 78 | 79 | ```scala 80 | libraryDependencies += Seq( 81 | "io.github.j5ik2o" %% "docker-controller-scala-scalatest" % version, 82 | "io.github.j5ik2o" %% "docker-controller-scala-mysql" % version, 83 | "io.github.j5ik2o" %% "docker-controller-scala-flyway" % version, // for flyway 84 | ) 85 | ``` 86 | 87 | Mix-in `FlywaySpecSupport` then, put the sql files in `src/resources/flyway`(`src/resources/**` can be set to any string.), run `flywayContext.flyway.migrate()` in `afterStartContainers` method. 88 | 89 | ### How to test with DockerController your customized 90 | 91 | To launch a docker container for testing 92 | 93 | ```scala 94 | // In ScalaTest, please mix-in DockerControllerSpecSupport. 95 | class NginxSpec extends AnyFreeSpec with DockerControllerSpecSupport { 96 | 97 | // choose whether to create and destroy containers per test class (ForAllTest) or per test (ForEachTest). 98 | override def createRemoveLifecycle: DockerContainerCreateRemoveLifecycle.Value = 99 | DockerContainerCreateRemoveLifecycle.ForEachTest 100 | 101 | // choose whether to start and stop containers per test class (ForAllTest) or per test (ForEachTest). 102 | override def startStopLifecycle: DockerContainerStartStopLifecycle.Value = 103 | DockerContainerStartStopLifecycle.ForEachTest 104 | 105 | val nginx: DockerController = DockerController(dockerClient)( 106 | imageName = "nginx", 107 | tag = Some("latest") 108 | ).configureCreateContainerCmd { cmd => 109 | // if customize the container generation, please do the following. 110 | // In this example, a random host port is specified. 111 | val hostPort: Int = temporaryServerPort() 112 | val containerPort: ExposedPort = ExposedPort.tcp(80) 113 | val portBinding: Ports = new Ports() 114 | portBinding.bind(containerPort, Ports.Binding.bindPort(hostPort)) 115 | logger.debug(s"hostPort = $hostPort, containerPort = $containerPort") 116 | cmd 117 | .withExposedPorts(containerPort) 118 | .withHostConfig(newHostConfig().withPortBindings(portBinding)) 119 | } 120 | 121 | // Specify DockerControllers to be launched. 122 | override protected val dockerControllers: Vector[DockerController] = { 123 | Vector(nginx) 124 | } 125 | 126 | // Set the condition to wait for the container to be started. 127 | override protected val waitPredicatesSettings: Map[DockerController, WaitPredicateSetting] = 128 | Map( 129 | nginx -> WaitPredicateSetting( 130 | Duration.Inf, 131 | WaitPredicates.forLogMessageContained("Configuration complete; ready for start up") 132 | ) 133 | ) 134 | 135 | "nginx" - { 136 | "run-1" in { 137 | val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get 138 | val url = new URL(s"http://$dockerHost:$hostPort") 139 | HttpRequestUtil.wget(url) 140 | } 141 | "run-2" in { 142 | val hostPort = nginx.inspectContainer().getNetworkSettings.bindingHostPort(ExposedPort.tcp(80)).get 143 | val url = new URL(s"http://$dockerHost:$hostPort") 144 | HttpRequestUtil.wget(url) 145 | } 146 | } 147 | } 148 | ``` 149 | 150 | ### How to use Docker Compose 151 | 152 | - Place the `docker-compose.yml.ftl`(ftl is Freemarker template) in `src/test/resources`. `docker-compose.yml.ftl` can be renamed to anything you want. 153 | - The variables in the ftl can be freely determined. 154 | 155 | ```yaml 156 | version: '3' 157 | services: 158 | nginx: 159 | image: nginx 160 | ports: 161 | - ${nginxHostPort}:80 162 | ``` 163 | 164 | - Use `DockerComposeController`, which is a subtype of `DockerController`. Other than this, it is the same as the test method above. 165 | - Pass the context containing the values of the variables to be used in the FTL to the constructor of `DockerComposeController`. 166 | 167 | ```scala 168 | class NginxSpec extends AnyFreeSpec with DockerControllerSpecSupport { 169 | // ... 170 | val buildDir: File = ResourceUtil.getBuildDir(getClass) 171 | val dockerComposeWorkingDir: File = new File(buildDir, "docker-compose") 172 | val dockerController = DockerComposeController(dockerClient)( 173 | dockerComposeWorkingDir, 174 | "docker-compose.yml.ftl", 175 | Map("nginxHostPort" -> hostPort.toString) 176 | ) 177 | 178 | override val dockerControllers: Vector[DockerController] = { 179 | Vector(dockerController) 180 | } 181 | // ... 182 | } 183 | ``` 184 | 185 | 186 | ## License 187 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fj5ik2o%2Fdocker-controller-scala.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fj5ik2o%2Fdocker-controller-scala?ref=badge_large) 188 | -------------------------------------------------------------------------------- /docker-controller-scala-core/src/main/scala/com/github/j5ik2o/dockerController/DockerController.scala: -------------------------------------------------------------------------------- 1 | package com.github.j5ik2o.dockerController 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.async.ResultCallback 5 | import com.github.dockerjava.api.command._ 6 | import com.github.dockerjava.api.model.{ Frame, Image, PullResponseItem } 7 | import me.tongfei.progressbar.{ DelegatingProgressBarConsumer, ProgressBar, ProgressBarBuilder, ProgressBarStyle } 8 | import org.slf4j.{ Logger, LoggerFactory } 9 | 10 | import java.lang 11 | import java.util.concurrent.{ LinkedBlockingQueue, TimeUnit } 12 | import java.util.{ Timer, TimerTask } 13 | import scala.annotation.tailrec 14 | import scala.collection.mutable 15 | import scala.concurrent.duration.{ Duration, DurationInt, FiniteDuration } 16 | import scala.jdk.CollectionConverters._ 17 | 18 | case class CmdConfigures( 19 | createContainerCmdConfigure: CreateContainerCmd => CreateContainerCmd = identity, 20 | removeContainerCmdConfigure: RemoveContainerCmd => RemoveContainerCmd = identity, 21 | startContainerCmdConfigure: StartContainerCmd => StartContainerCmd = identity, 22 | stopContainerCmdConfigure: StopContainerCmd => StopContainerCmd = identity, 23 | inspectContainerCmdConfigure: InspectContainerCmd => InspectContainerCmd = identity, 24 | listImageCmdConfigure: ListImagesCmd => ListImagesCmd = identity, 25 | pullImageCmdConfigure: PullImageCmd => PullImageCmd = identity 26 | ) 27 | 28 | trait DockerController { 29 | def containerId: Option[String] 30 | 31 | def dockerClient: DockerClient 32 | 33 | def isDockerClientAutoClose: Boolean 34 | 35 | def dispose(): Unit 36 | def imageName: String 37 | def tag: Option[String] 38 | 39 | def cmdConfigures: Option[CmdConfigures] 40 | def configureCmds(cmdConfigures: CmdConfigures): DockerController 41 | 42 | def createNetwork(name: String, f: CreateNetworkCmd => CreateNetworkCmd = identity): CreateNetworkResponse 43 | def removeNetwork(id: String, f: RemoveNetworkCmd => RemoveNetworkCmd = identity): Unit 44 | 45 | def configureCreateContainerCmd(f: CreateContainerCmd => CreateContainerCmd = identity): DockerController = { 46 | val newCmdConfigures = cmdConfigures match { 47 | case Some(cc) => 48 | cc.copy(createContainerCmdConfigure = f) 49 | case None => 50 | CmdConfigures(createContainerCmdConfigure = f) 51 | } 52 | configureCmds(newCmdConfigures) 53 | } 54 | 55 | def createContainer(f: CreateContainerCmd => CreateContainerCmd = identity): CreateContainerResponse 56 | def removeContainer(f: RemoveContainerCmd => RemoveContainerCmd = identity): Unit 57 | def startContainer(f: StartContainerCmd => StartContainerCmd = identity): Unit 58 | def stopContainer(f: StopContainerCmd => StopContainerCmd = identity): Unit 59 | def inspectContainer(f: InspectContainerCmd => InspectContainerCmd = identity): InspectContainerResponse 60 | def listImages(f: ListImagesCmd => ListImagesCmd = identity): Vector[Image] 61 | def existsImage(p: Image => Boolean): Boolean 62 | def pullImageIfNotExists(f: PullImageCmd => PullImageCmd = identity): Unit 63 | def pullImage(f: PullImageCmd => PullImageCmd = identity): Unit 64 | def awaitCondition(duration: Duration)(predicate: Option[Frame] => Boolean): Unit 65 | } 66 | 67 | object DockerController { 68 | 69 | def apply( 70 | dockerClient: DockerClient, 71 | isDockerClientAutoClose: Boolean = false, 72 | outputFrameInterval: FiniteDuration = 500.millis 73 | )( 74 | imageName: String, 75 | tag: Option[String] = None 76 | ): DockerController = 77 | new DockerControllerImpl(dockerClient, isDockerClientAutoClose, outputFrameInterval)(imageName, tag) 78 | } 79 | 80 | private[dockerController] class DockerControllerImpl( 81 | val dockerClient: DockerClient, 82 | val isDockerClientAutoClose: Boolean = false, 83 | outputFrameInterval: FiniteDuration = 500.millis 84 | )( 85 | val imageName: String, 86 | _tag: Option[String] = None 87 | ) extends DockerController { 88 | 89 | val tag: Option[String] = _tag.orElse(Some("latest")) 90 | 91 | protected val logger: Logger = LoggerFactory.getLogger(getClass) 92 | 93 | private var _containerId: Option[String] = None 94 | 95 | private def repoTag: String = tag.fold(imageName)(t => s"$imageName:$t") 96 | 97 | override def containerId: Option[String] = _containerId 98 | 99 | private var _cmdConfigures: Option[CmdConfigures] = None 100 | 101 | private final val MaxProgressBarLength = 120 102 | 103 | private val progressBarConsumer = 104 | new DelegatingProgressBarConsumer({ text => logger.info(text) }, MaxProgressBarLength) 105 | 106 | override def cmdConfigures: Option[CmdConfigures] = _cmdConfigures 107 | 108 | override def configureCmds(cmdConfigures: CmdConfigures): DockerController = { 109 | this._cmdConfigures = Some(cmdConfigures) 110 | this 111 | } 112 | 113 | protected def isPlatformLinuxAmd64AtM1Mac: Boolean = false 114 | 115 | protected def newCreateContainerCmd(): CreateContainerCmd = { 116 | var cmd = dockerClient.createContainerCmd(repoTag) 117 | val osArch = sys.props("os.arch") 118 | if (isPlatformLinuxAmd64AtM1Mac && osArch == "aarch64") { 119 | cmd = cmd.withPlatform("linux/amd64") 120 | } 121 | cmd 122 | } 123 | 124 | protected def newRemoveContainerCmd(): RemoveContainerCmd = { 125 | require(containerId.isDefined) 126 | dockerClient.removeContainerCmd(containerId.get) 127 | } 128 | 129 | protected def newInspectContainerCmd(): InspectContainerCmd = { 130 | require(containerId.isDefined) 131 | dockerClient.inspectContainerCmd(containerId.get) 132 | } 133 | 134 | protected def newListImagesCmd(): ListImagesCmd = { 135 | dockerClient.listImagesCmd() 136 | } 137 | 138 | protected def newPullImageCmd(): PullImageCmd = { 139 | require(imageName != null) 140 | val cmd = dockerClient.pullImageCmd(imageName) 141 | tag.fold(cmd)(t => cmd.withTag(t)) 142 | } 143 | 144 | protected def newLogContainerCmd(): LogContainerCmd = { 145 | require(containerId.isDefined) 146 | dockerClient 147 | .logContainerCmd(containerId.get) 148 | .withStdOut(true) 149 | .withStdErr(true) 150 | .withFollowStream(true) 151 | .withTailAll() 152 | } 153 | 154 | protected def newStartContainerCmd(): StartContainerCmd = { 155 | require(containerId.isDefined) 156 | dockerClient.startContainerCmd(containerId.get) 157 | } 158 | 159 | protected def newStopContainerCmd(): StopContainerCmd = { 160 | require(containerId.isDefined) 161 | dockerClient.stopContainerCmd(containerId.get) 162 | } 163 | 164 | override def createContainer(f: CreateContainerCmd => CreateContainerCmd): CreateContainerResponse = synchronized { 165 | logger.debug("createContainer --- start") 166 | val configureFunction: CreateContainerCmd => CreateContainerCmd = 167 | cmdConfigures.map(_.createContainerCmdConfigure).getOrElse(identity) 168 | val result = f(configureFunction(newCreateContainerCmd())).exec() 169 | _containerId = Some(result.getId) 170 | sys.addShutdownHook { 171 | logger.debug("shutdownHook: start") 172 | dispose() 173 | logger.debug("shutdownHook: finish") 174 | } 175 | logger.debug("createContainer --- finish") 176 | result 177 | } 178 | 179 | override def removeContainer(f: RemoveContainerCmd => RemoveContainerCmd): Unit = synchronized { 180 | logger.debug("removeContainer --- start") 181 | val configureFunction: RemoveContainerCmd => RemoveContainerCmd = 182 | cmdConfigures.map(_.removeContainerCmdConfigure).getOrElse(identity) 183 | try { 184 | f(configureFunction(newRemoveContainerCmd())).exec() 185 | } catch { 186 | case e: com.github.dockerjava.api.exception.NotFoundException => 187 | logger.debug("Container not found (NotFoundException: Status 404)") 188 | case e: com.github.dockerjava.api.exception.NotModifiedException => 189 | logger.debug("Container already removed (NotModifiedException: Status 304)") 190 | case e: Throwable => 191 | throw e 192 | } 193 | _containerId = None 194 | logger.debug("removeContainer --- finish") 195 | } 196 | 197 | override def inspectContainer(f: InspectContainerCmd => InspectContainerCmd): InspectContainerResponse = { 198 | logger.debug("inspectContainer --- start") 199 | val configureFunction: InspectContainerCmd => InspectContainerCmd = 200 | cmdConfigures.map(_.inspectContainerCmdConfigure).getOrElse(identity) 201 | val result = f(configureFunction(newInspectContainerCmd())).exec() 202 | logger.debug("inspectContainer --- finish") 203 | result 204 | } 205 | 206 | override def listImages(f: ListImagesCmd => ListImagesCmd): Vector[Image] = { 207 | logger.debug("listImages --- start") 208 | logger.debug(s"dockerClient: $dockerClient") 209 | val configureFunction: ListImagesCmd => ListImagesCmd = 210 | cmdConfigures.map(_.listImageCmdConfigure).getOrElse(identity) 211 | val result = f(configureFunction(newListImagesCmd())).exec().asScala.toVector 212 | logger.debug("listImages --- finish") 213 | result 214 | } 215 | 216 | override def existsImage(p: Image => Boolean): Boolean = { 217 | logger.debug("exists --- start") 218 | val result = listImages().exists(p) 219 | logger.debug("exists --- finish") 220 | result 221 | } 222 | 223 | override def pullImageIfNotExists(f: PullImageCmd => PullImageCmd): Unit = { 224 | logger.debug("pullImageIfNotExists --- start") 225 | if (!existsImage(p => Option(p.getRepoTags).exists(_.contains(repoTag)))) { 226 | pullImage(f) 227 | } 228 | logger.debug("pullImageIfNotExists --- finish") 229 | } 230 | 231 | override def pullImage(f: PullImageCmd => PullImageCmd): Unit = { 232 | logger.debug("pullContainer --- start") 233 | val progressBarMap = mutable.Map.empty[String, ProgressBar] 234 | try { 235 | f(newPullImageCmd()) 236 | .exec(new ResultCallback.Adapter[PullResponseItem] { 237 | override def onNext(frame: PullResponseItem): Unit = { 238 | if (frame.getProgressDetail != null) { 239 | val max = frame.getProgressDetail.getTotal 240 | val current = frame.getProgressDetail.getCurrent 241 | val progressBar = progressBarMap.getOrElseUpdate( 242 | frame.getId, 243 | newProgressBar(frame, max) 244 | ) 245 | progressBar.maxHint(max).stepTo(current) 246 | } 247 | } 248 | }) 249 | .awaitCompletion() 250 | } finally { 251 | progressBarMap.foreach { case (_, progressBar) => 252 | progressBar.close() 253 | } 254 | } 255 | logger.debug("pullContainer --- finish") 256 | } 257 | 258 | protected def newProgressBar(frame: PullResponseItem, max: lang.Long): ProgressBar = { 259 | new ProgressBarBuilder() 260 | .setTaskName(s"pull image: ${frame.getStatus}, ${frame.getId}") 261 | .setStyle(ProgressBarStyle.ASCII) 262 | .setConsumer(progressBarConsumer) 263 | .setInitialMax(max) 264 | .setMaxRenderedLength(90) 265 | .build() 266 | } 267 | 268 | override def startContainer(f: StartContainerCmd => StartContainerCmd): Unit = { 269 | logger.debug("startContainer --- start") 270 | val configureFunction: StartContainerCmd => StartContainerCmd = 271 | cmdConfigures.map(_.startContainerCmdConfigure).getOrElse(identity) 272 | f(configureFunction(newStartContainerCmd())).exec() 273 | logger.debug("startContainer --- finish") 274 | } 275 | 276 | override def stopContainer(f: StopContainerCmd => StopContainerCmd): Unit = { 277 | logger.debug("stopContainer --- start") 278 | val configureFunction: StopContainerCmd => StopContainerCmd = 279 | cmdConfigures.map(_.stopContainerCmdConfigure).getOrElse(identity) 280 | try { 281 | f(configureFunction(newStopContainerCmd())).exec() 282 | } catch { 283 | case e: com.github.dockerjava.api.exception.NotModifiedException => 284 | logger.debug("Container is already stopped (NotModifiedException: Status 304)") 285 | case e: Throwable => 286 | throw e 287 | } 288 | logger.debug("stopContainer --- finish") 289 | } 290 | 291 | override def awaitCondition(duration: Duration)(predicate: Option[Frame] => Boolean): Unit = { 292 | logger.debug("awaitCompletion --- start") 293 | val frameQueue: LinkedBlockingQueue[Frame] = new LinkedBlockingQueue[Frame]() 294 | 295 | newLogContainerCmd().exec(new ResultCallback.Adapter[Frame] { 296 | 297 | override def onNext(frame: Frame): Unit = { 298 | frameQueue.add(frame) 299 | } 300 | 301 | }) 302 | 303 | @volatile var terminate = false 304 | val waiter = new Runnable { 305 | override def run(): Unit = { 306 | @tailrec 307 | def loop(): Unit = { 308 | if ( 309 | !terminate && { 310 | val frameOpt = Option(frameQueue.poll(outputFrameInterval.toMillis, TimeUnit.MILLISECONDS)) 311 | frameOpt.foreach { frame => 312 | logger.debug(frame.toString) 313 | } 314 | !predicate(frameOpt) 315 | } 316 | ) { 317 | loop() 318 | } 319 | } 320 | try { 321 | loop() 322 | } catch { 323 | case _: InterruptedException => 324 | logger.debug("interrupted") 325 | case ex: Throwable => 326 | logger.debug("occurred error", ex) 327 | throw ex 328 | } 329 | } 330 | } 331 | 332 | val thread = new Thread(waiter) 333 | thread.start() 334 | if (duration.isFinite) { 335 | val timer = new Timer() 336 | timer.schedule( 337 | new TimerTask { 338 | override def run(): Unit = { 339 | terminate = true 340 | thread.interrupt() 341 | } 342 | }, 343 | duration.toMillis 344 | ) 345 | timer.cancel() 346 | } 347 | thread.join() 348 | logger.debug("awaitCompletion --- finish") 349 | } 350 | 351 | override def createNetwork( 352 | name: String, 353 | f: CreateNetworkCmd => CreateNetworkCmd = identity 354 | ): CreateNetworkResponse = { 355 | f(dockerClient.createNetworkCmd().withName(name)).exec() 356 | } 357 | 358 | override def removeNetwork(id: String, f: RemoveNetworkCmd => RemoveNetworkCmd): Unit = { 359 | f(dockerClient.removeNetworkCmd(id)).exec() 360 | } 361 | 362 | override def dispose(): Unit = synchronized { 363 | logger.debug("dispose: start") 364 | if (containerId.isDefined) { 365 | removeContainer() 366 | if (isDockerClientAutoClose) 367 | dockerClient.close() 368 | } 369 | logger.debug("dispose: finish") 370 | } 371 | 372 | } 373 | --------------------------------------------------------------------------------