├── .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 | [](https://github.com/j5ik2o/docker-controller-scala/actions?query=workflow%3A"CI")
4 | [](https://maven-badges.herokuapp.com/maven-central/io.github.j5ik2o/docker-controller-scala-core_2.13)
5 | [](https://renovatebot.com)
6 | [](https://opensource.org/licenses/MIT)
7 | [](https://github.com/XAMPPRocky/tokei)
8 | [](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 | [](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 |
--------------------------------------------------------------------------------