From 842e17ecedaf1ab189a643e26242e217b04702e1 Mon Sep 17 00:00:00 2001 From: Mortitz Halbritter Date: Fri, 7 Apr 2023 10:33:48 -0400 Subject: [PATCH] Add Docker Compose support Add `spring-boot-docker-compose` module with service connection support. Closes gh-34747 Co-authored-by: Phillip Webb Co-authored-by: "Andy Wilkinson --- .../DocumentConfigurationProperties.java | 7 +- settings.gradle | 1 + .../spring-boot-dependencies/build.gradle | 1 + .../spring-boot-docker-compose/build.gradle | 32 ++ .../docker/compose/core/ConnectionPorts.java | 57 +++ .../compose/core/DefaultConnectionPorts.java | 151 +++++++ .../compose/core/DefaultDockerCompose.java | 105 +++++ .../compose/core/DefaultRunningService.java | 99 +++++ .../boot/docker/compose/core/DockerCli.java | 151 +++++++ .../docker/compose/core/DockerCliCommand.java | 207 ++++++++++ .../core/DockerCliComposeConfigResponse.java | 41 ++ .../core/DockerCliComposePsResponse.java | 32 ++ .../core/DockerCliComposeVersionResponse.java | 29 ++ .../core/DockerCliContextResponse.java | 31 ++ .../core/DockerCliInspectResponse.java | 83 ++++ .../docker/compose/core/DockerCompose.java | 101 +++++ .../compose/core/DockerComposeFile.java | 117 ++++++ .../compose/core/DockerComposeOrigin.java | 38 ++ .../boot/docker/compose/core/DockerEnv.java | 76 ++++ .../docker/compose/core/DockerException.java | 37 ++ .../boot/docker/compose/core/DockerHost.java | 126 ++++++ .../boot/docker/compose/core/DockerJson.java | 83 ++++ .../core/DockerNotRunningException.java | 44 +++ .../core/DockerOutputParseException.java | 33 ++ .../core/DockerProcessStartException.java | 34 ++ .../docker/compose/core/ImageReference.java | 86 ++++ .../compose/core/ProcessExitException.java | 69 ++++ .../docker/compose/core/ProcessRunner.java | 156 ++++++++ .../compose/core/ProcessStartException.java | 34 ++ .../docker/compose/core/RunningService.java | 67 ++++ .../docker/compose/core/package-info.java | 20 + .../DockerComposeLifecycleManager.java | 150 +++++++ .../lifecycle/DockerComposeListener.java | 63 +++ .../lifecycle/DockerComposeProperties.java | 222 +++++++++++ .../DockerComposeServicesReadyEvent.java | 58 +++ .../lifecycle/DockerComposeSkipCheck.java | 79 ++++ .../lifecycle/LifecycleManagement.java | 69 ++++ .../compose/lifecycle/ShutdownCommand.java | 54 +++ .../compose/lifecycle/StartupCommand.java | 53 +++ .../compose/lifecycle/package-info.java | 20 + .../readiness/ReadinessProperties.java | 101 +++++ .../readiness/ReadinessTimeoutException.java | 61 +++ .../readiness/ServiceNotReadyException.java | 51 +++ .../readiness/ServiceReadinessCheck.java | 47 +++ .../readiness/ServiceReadinessChecks.java | 137 +++++++ .../TcpConnectServiceReadinessCheck.java | 80 ++++ .../compose/readiness/package-info.java | 20 + ...DockerComposeConnectionDetailsFactory.java | 109 +++++ .../DockerComposeConnectionSource.java | 51 +++ ...ServiceConnectionsApplicationListener.java | 92 +++++ ...DockerComposeConnectionDetailsFactory.java | 84 ++++ .../ElasticsearchEnvironment.java | 43 ++ .../elasticsearch/package-info.java | 20 + .../connection/jdbc/JdbcUrlBuilder.java | 69 ++++ .../service/connection/jdbc/package-info.java | 21 + .../mariadb/MariaDbEnvironment.java | 83 ++++ ...DockerComposeConnectionDetailsFactory.java | 80 ++++ ...DockerComposeConnectionDetailsFactory.java | 72 ++++ .../connection/mariadb/package-info.java | 20 + ...DockerComposeConnectionDetailsFactory.java | 88 +++++ .../connection/mongo/MongoEnvironment.java | 60 +++ .../connection/mongo/package-info.java | 20 + .../connection/mysql/MySqlEnvironment.java | 72 ++++ ...DockerComposeConnectionDetailsFactory.java | 80 ++++ ...DockerComposeConnectionDetailsFactory.java | 72 ++++ .../connection/mysql/package-info.java | 20 + .../service/connection/package-info.java | 20 + .../postgres/PostgresEnvironment.java | 63 +++ ...DockerComposeConnectionDetailsFactory.java | 80 ++++ ...DockerComposeConnectionDetailsFactory.java | 72 ++++ .../connection/postgres/package-info.java | 20 + .../ConnectionFactoryOptionsBuilder.java | 100 +++++ .../connection/r2dbc/package-info.java | 21 + ...DockerComposeConnectionDetailsFactory.java | 87 ++++ .../connection/rabbit/RabbitEnvironment.java | 47 +++ .../connection/rabbit/package-info.java | 20 + ...DockerComposeConnectionDetailsFactory.java | 65 +++ .../connection/redis/package-info.java | 20 + ...DockerComposeConnectionDetailsFactory.java | 69 ++++ .../connection/zipkin/package-info.java | 20 + .../main/resources/META-INF/spring.factories | 18 + .../core/DefaultConnectionPortsTests.java | 88 +++++ .../core/DefaultDockerComposeTests.java | 163 ++++++++ .../core/DefaultRunningServiceTests.java | 125 ++++++ .../compose/core/DockerCliCommandTests.java | 99 +++++ .../DockerCliComposeConfigResponseTests.java | 49 +++ .../core/DockerCliComposePsResponseTests.java | 47 +++ .../DockerCliComposeVersionResponseTests.java | 46 +++ .../core/DockerCliContextResponseTests.java | 47 +++ .../core/DockerCliInspectResponseTests.java | 78 ++++ .../docker/compose/core/DockerCliTests.java | 45 +++ .../compose/core/DockerComposeFileTests.java | 137 +++++++ .../core/DockerComposeOriginTests.java | 71 ++++ .../docker/compose/core/DockerEnvTests.java | 54 +++ .../docker/compose/core/DockerHostTests.java | 184 +++++++++ .../docker/compose/core/DockerJsonTests.java | 74 ++++ .../compose/core/ImageReferenceTests.java | 103 +++++ .../compose/core/ProcessRunnerTests.java | 61 +++ .../DockerComposeLifecycleManagerTests.java | 373 ++++++++++++++++++ .../lifecycle/DockerComposeListenerTests.java | 88 +++++ .../DockerComposePropertiesTests.java | 74 ++++ .../DockerComposeServicesReadyEventTests.java | 55 +++ .../lifecycle/LifecycleManagementTests.java | 62 +++ .../lifecycle/ShutdownCommandTests.java | 53 +++ .../lifecycle/StartupCommandTests.java | 49 +++ .../readiness/ReadinessPropertiesTests.java | 62 +++ .../ReadinessTimeoutExceptionTests.java | 55 +++ .../ServiceNotReadyExceptionTests.java | 42 ++ .../ServiceReadinessChecksTests.java | 174 ++++++++ .../TcpConnectServiceReadinessCheckTests.java | 126 ++++++ ...nectionDetailsFactoryIntegrationTests.java | 56 +++ .../ElasticsearchEnvironmentTests.java | 55 +++ ...rComposeConnectionDetailsFactoryTests.java | 101 +++++ .../connection/jdbc/JdbcUrlBuilderTests.java | 89 +++++ .../mariadb/MariaDbEnvironmentTests.java | 167 ++++++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++ ...rComposeConnectionDetailsFactoryTests.java | 190 +++++++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++ .../mongo/MongoEnvironmentTests.java | 86 ++++ ...rComposeConnectionDetailsFactoryTests.java | 130 ++++++ .../mysql/MySqlEnvironmentTests.java | 95 +++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++ ...rComposeConnectionDetailsFactoryTests.java | 137 +++++++ ...rComposeConnectionDetailsFactoryTests.java | 137 +++++++ .../postgres/PostgresEnvironmentTests.java | 83 ++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++ ...rComposeConnectionDetailsFactoryTests.java | 115 ++++++ ...rComposeConnectionDetailsFactoryTests.java | 115 ++++++ .../ConnectionFactoryOptionsBuilderTests.java | 94 +++++ ...nectionDetailsFactoryIntegrationTests.java | 52 +++ .../rabbit/RabbitEnvironmentTests.java | 61 +++ ...rComposeConnectionDetailsFactoryTests.java | 99 +++++ ...nectionDetailsFactoryIntegrationTests.java | 54 +++ ...rComposeConnectionDetailsFactoryTests.java | 71 ++++ ...AbstractDockerComposeIntegrationTests.java | 69 ++++ .../test/AbstractIntegrationTests.java | 102 +++++ ...rComposeConnectionDetailsFactoryTests.java | 71 ++++ ...nectionDetailsFactoryIntegrationTests.java | 45 +++ .../compose/core/docker-compose-config.json | 29 ++ .../compose/core/docker-compose-ps.json | 16 + .../compose/core/docker-compose-version.json | 3 + .../docker/compose/core/docker-context.json | 8 + .../core/docker-inspect-bridge-network.json | 250 ++++++++++++ .../core/docker-inspect-host-network.json | 237 +++++++++++ .../docker/compose/core/docker-inspect.json | 248 ++++++++++++ .../elasticsearch/elasticsearch-compose.yaml | 11 + .../connection/mariadb/mariadb-compose.yaml | 11 + .../connection/mongo/mongo-compose.yaml | 9 + .../connection/mysql/mysql-compose.yaml | 10 + .../connection/postgres/postgres-compose.yaml | 9 + .../connection/rabbit/rabbit-compose.yaml | 8 + .../connection/redis/redis-compose.yaml | 5 + .../connection/zipkin/zipkin-compose.yaml | 5 + .../spring-boot-docs/build.gradle | 2 + .../docs/asciidoc/application-properties.adoc | 2 + .../boot/SpringApplicationShutdownHook.java | 8 +- src/nohttp/suppressions.xml | 1 + 160 files changed, 11454 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/build.gradle create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ConnectionPorts.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultConnectionPorts.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponse.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponse.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponse.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliContextResponse.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponse.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeFile.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOrigin.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerEnv.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerHost.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerJson.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerNotRunningException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerOutputParseException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerProcessStartException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ImageReference.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessExitException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessStartException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListener.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEvent.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeSkipCheck.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagement.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommand.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/StartupCommand.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessProperties.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyException.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessCheck.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecks.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheck.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilder.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultConnectionPortsTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultRunningServiceTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponseTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponseTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponseTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliContextResponseTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponseTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeOriginTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerEnvTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerHostTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerJsonTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ImageReferenceTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ProcessRunnerTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListenerTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEventTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagementTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommandTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/StartupCommandTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutExceptionTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyExceptionTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecksTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheckTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/XElasticsearchDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/XMariaDbDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/XMongoDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlDbR2dbcDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlJdbcDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresDbR2dbcDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresJdbcDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilderTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/XRabbitDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/XZipkinDockerComposeConnectionDetailsFactoryTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-config.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-ps.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-version.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-context.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-bridge-network.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-host-network.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect.json create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/zipkin/zipkin-compose.yaml diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index 3a1c970bbf3..d34405f3d27 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,6 +78,7 @@ public class DocumentConfigurationProperties extends DefaultTask { snippets.add("application-properties.security", "Security Properties", this::securityPrefixes); snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes); snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes); + snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes); snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes); snippets.add("application-properties.testing", "Testing Properties", this::testingPrefixes); snippets.writeTo(this.outputDir.toPath()); @@ -211,6 +212,10 @@ public class DocumentConfigurationProperties extends DefaultTask { prefix.accept("management"); } + private void dockerComposePrefixes(Config prefix) { + prefix.accept("spring.docker.compose"); + } + private void devtoolsPrefixes(Config prefix) { prefix.accept("spring.devtools"); } diff --git a/settings.gradle b/settings.gradle index 338f9d05136..101c44479d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -66,6 +66,7 @@ include "spring-boot-project:spring-boot" include "spring-boot-project:spring-boot-autoconfigure" include "spring-boot-project:spring-boot-actuator" include "spring-boot-project:spring-boot-actuator-autoconfigure" +include "spring-boot-project:spring-boot-docker-compose" include "spring-boot-project:spring-boot-devtools" include "spring-boot-project:spring-boot-docs" include "spring-boot-project:spring-boot-test" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8c54d842caf..4e105e3713c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1188,6 +1188,7 @@ bom { "spring-boot-configuration-metadata", "spring-boot-configuration-processor", "spring-boot-devtools", + "spring-boot-docker-compose", "spring-boot-jarmode-layertools", "spring-boot-loader", "spring-boot-loader-tools", diff --git a/spring-boot-project/spring-boot-docker-compose/build.gradle b/spring-boot-project/spring-boot-docker-compose/build.gradle new file mode 100644 index 00000000000..7acd2c094ae --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/build.gradle @@ -0,0 +1,32 @@ +plugins { + id "java-library" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Docker Compose Support" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.module:jackson-module-parameter-names") + + optional(project(":spring-boot-project:spring-boot-autoconfigure")) + optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) + optional("io.r2dbc:r2dbc-spi") + optional("org.mongodb:mongodb-driver-core") + optional("org.springframework.data:spring-data-r2dbc") + + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation(project(":spring-boot-project:spring-boot-test")) + testImplementation("org.springframework:spring-core-test") + testImplementation("org.springframework:spring-test") + testImplementation("org.assertj:assertj-core") + testImplementation("org.mockito:mockito-core") + testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.junit.jupiter:junit-jupiter") +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ConnectionPorts.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ConnectionPorts.java new file mode 100644 index 00000000000..8b49127c46e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ConnectionPorts.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.List; + +/** + * Provides access to the ports that can be used to connect to a {@link RunningService}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + * @see RunningService + */ +public interface ConnectionPorts { + + /** + * Return the host port mapped to the given container port. + * @param containerPort the container port. This is usually the standard port for the + * service (e.g. port 80 for HTTP) + * @return the host port. This can be an ephemeral port that is different from the + * container port + * @throws IllegalStateException if the container port is not mapped + */ + int get(int containerPort); + + /** + * Return all host ports in use. + * @return a list of all host ports + * @see #getAll(String) + */ + List getAll(); + + /** + * Return all host ports in use that match the given protocol. + * @param protocol the protocol in use (for example 'tcp') or {@code null} to return + * all host ports + * @return a list of all host ports using the given protocol + */ + List getAll(String protocol); + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultConnectionPorts.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultConnectionPorts.java new file mode 100644 index 00000000000..d9823e653a1 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultConnectionPorts.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Default {@link ConnectionPorts} implementation backed by {@link DockerCli} responses. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultConnectionPorts implements ConnectionPorts { + + private Map mappings = new LinkedHashMap<>(); + + private Map portMappings = new LinkedHashMap<>(); + + DefaultConnectionPorts(DockerCliInspectResponse inspectResponse) { + this.mappings = !isHostNetworkMode(inspectResponse) + ? buildMappingsForNetworkSettings(inspectResponse.networkSettings()) + : buildMappingsForHostNetworking(inspectResponse.config()); + Map portMappings = new HashMap<>(); + this.mappings.forEach((containerPort, hostPort) -> portMappings.put(containerPort.number(), hostPort)); + this.portMappings = Collections.unmodifiableMap(portMappings); + } + + private static boolean isHostNetworkMode(DockerCliInspectResponse inspectResponse) { + HostConfig config = inspectResponse.hostConfig(); + return (config != null) && "host".equals(config.networkMode()); + } + + private Map buildMappingsForNetworkSettings(NetworkSettings networkSettings) { + if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) { + return Collections.emptyMap(); + } + Map mappings = new HashMap<>(); + networkSettings.ports().forEach((containerPortString, hostPorts) -> { + if (!CollectionUtils.isEmpty(hostPorts)) { + ContainerPort containerPort = ContainerPort.parse(containerPortString); + hostPorts.stream() + .filter(this::isIpV4) + .forEach((hostPort) -> mappings.put(containerPort, getPortNumber(hostPort))); + } + }); + return Collections.unmodifiableMap(mappings); + } + + private boolean isIpV4(HostPort hostPort) { + String ip = (hostPort != null) ? hostPort.hostIp() : null; + return !StringUtils.hasLength(ip) || ip.contains("."); + } + + private static int getPortNumber(HostPort hostPort) { + return Integer.parseInt(hostPort.hostPort()); + } + + private Map buildMappingsForHostNetworking(Config config) { + if (CollectionUtils.isEmpty(config.exposedPorts())) { + return Collections.emptyMap(); + } + Map mappings = new HashMap<>(); + for (String entry : config.exposedPorts().keySet()) { + ContainerPort containerPort = ContainerPort.parse(entry); + mappings.put(containerPort, containerPort.number()); + } + return Collections.unmodifiableMap(mappings); + } + + @Override + public int get(int containerPort) { + Integer hostPort = this.portMappings.get(containerPort); + Assert.state(hostPort != null, "No host port mapping found for container port %s".formatted(containerPort)); + return hostPort; + } + + @Override + public List getAll() { + return getAll(null); + } + + @Override + public List getAll(String protocol) { + List hostPorts = new ArrayList<>(); + this.mappings.forEach((containerPort, hostPort) -> { + if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) { + hostPorts.add(hostPort); + } + }); + return Collections.unmodifiableList(hostPorts); + } + + Map getMappings() { + return this.mappings; + } + + /** + * A container port consisting of a number and protocol. + * + * @param number the port number + * @param protocol the protocol (e.g. tcp) + */ + static record ContainerPort(int number, String protocol) { + + @Override + public String toString() { + return "%d/%s".formatted(this.number, this.protocol); + } + + static ContainerPort parse(String value) { + try { + String[] parts = value.split("/"); + Assert.state(parts.length == 2, "Unable to split string"); + return new ContainerPort(Integer.parseInt(parts[0]), parts[1]); + } + catch (RuntimeException ex) { + throw new IllegalStateException("Unable to parse container port '%s'".formatted(value), ex); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java new file mode 100644 index 00000000000..ec3e60ec099 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Default {@link DockerCompose} implementation backed by {@link DockerCli}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultDockerCompose implements DockerCompose { + + private final DockerCli cli; + + private final DockerHost hostname; + + DefaultDockerCompose(DockerCli cli, String host) { + this.cli = cli; + this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context())); + } + + @Override + public void up() { + this.cli.run(new DockerCliCommand.ComposeUp()); + } + + @Override + public void down(Duration timeout) { + this.cli.run(new DockerCliCommand.ComposeDown(timeout)); + } + + @Override + public void start() { + this.cli.run(new DockerCliCommand.ComposeStart()); + } + + @Override + public void stop(Duration timeout) { + this.cli.run(new DockerCliCommand.ComposeStop(timeout)); + } + + @Override + public boolean hasDefinedServices() { + return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty(); + } + + @Override + public boolean hasRunningServices() { + return runComposePs().stream().anyMatch(this::isRunning); + } + + @Override + public List getRunningServices() { + List runningPsResponses = runComposePs().stream().filter(this::isRunning).toList(); + if (runningPsResponses.isEmpty()) { + return Collections.emptyList(); + } + DockerComposeFile dockerComposeFile = this.cli.getDockerComposeFile(); + List result = new ArrayList<>(); + Map inspected = inspect(runningPsResponses); + for (DockerCliComposePsResponse psResponse : runningPsResponses) { + DockerCliInspectResponse inspectResponse = inspected.get(psResponse.id()); + result.add(new DefaultRunningService(this.hostname, dockerComposeFile, psResponse, inspectResponse)); + } + return Collections.unmodifiableList(result); + } + + private Map inspect(List runningPsResponses) { + List ids = runningPsResponses.stream().map(DockerCliComposePsResponse::id).toList(); + List inspectResponses = this.cli.run(new DockerCliCommand.Inspect(ids)); + return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity())); + } + + private List runComposePs() { + return this.cli.run(new DockerCliCommand.ComposePs()); + } + + private boolean isRunning(DockerCliComposePsResponse psResponse) { + return !"exited".equals(psResponse.state()); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java new file mode 100644 index 00000000000..dc7581d5268 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultRunningService.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; + +/** + * Default {@link RunningService} implementation backed by {@link DockerCli} responses. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultRunningService implements RunningService, OriginProvider { + + private final Origin origin; + + private final String name; + + private final ImageReference image; + + private final DockerHost host; + + private final DefaultConnectionPorts ports; + + private final Map labels; + + private DockerEnv env; + + DefaultRunningService(DockerHost host, DockerComposeFile composeFile, DockerCliComposePsResponse psResponse, + DockerCliInspectResponse inspectResponse) { + this.origin = new DockerComposeOrigin(composeFile, psResponse.name()); + this.name = psResponse.name(); + this.image = ImageReference.of(psResponse.image()); + this.host = host; + this.ports = new DefaultConnectionPorts(inspectResponse); + this.env = new DockerEnv(inspectResponse.config().env()); + this.labels = Collections.unmodifiableMap(inspectResponse.config().labels()); + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + @Override + public String name() { + return this.name; + } + + @Override + public ImageReference image() { + return this.image; + } + + @Override + public String host() { + return this.host.toString(); + } + + @Override + public ConnectionPorts ports() { + return this.ports; + } + + @Override + public Map env() { + return this.env.asMap(); + } + + @Override + public Map labels() { + return this.labels; + } + + @Override + public String toString() { + return this.name; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java new file mode 100644 index 00000000000..e84a10790b2 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.docker.compose.core.DockerCliCommand.Type; +import org.springframework.core.log.LogMessage; + +/** + * Wrapper around {@code docker} and {@code docker-compose} command line tools. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCli { + + private final Log logger = LogFactory.getLog(DockerCli.class); + + private final ProcessRunner processRunner; + + private final List dockerCommand; + + private final List dockerComposeCommand; + + private final DockerComposeFile composeFile; + + private final Set activeProfiles; + + /** + * Create a new {@link DockerCli} instance. + * @param workingDirectory the working directory or {@code null} + * @param composeFile the docker compose file to use + * @param activeProfiles the docker compose profiles to activate + */ + DockerCli(File workingDirectory, DockerComposeFile composeFile, Set activeProfiles) { + this.processRunner = new ProcessRunner(workingDirectory); + this.dockerCommand = getDockerCommand(this.processRunner); + this.dockerComposeCommand = getDockerComposeCommand(this.processRunner); + this.composeFile = composeFile; + this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); + } + + private List getDockerCommand(ProcessRunner processRunner) { + try { + String version = processRunner.run("docker", "version", "--format", "{{.Client.Version}}"); + this.logger.trace(LogMessage.format("Using docker %s", version)); + return List.of("docker"); + } + catch (ProcessStartException ex) { + throw new DockerProcessStartException("Unable to start docker process. Is docker correctly installed?", ex); + } + catch (ProcessExitException ex) { + if (ex.getStdErr().contains("docker daemon is not running") + || ex.getStdErr().contains("Cannot connect to the Docker daemon")) { + throw new DockerNotRunningException(ex.getStdErr(), ex); + } + throw ex; + + } + } + + private List getDockerComposeCommand(ProcessRunner processRunner) { + try { + DockerCliComposeVersionResponse response = DockerJson.deserialize( + processRunner.run("docker", "compose", "version", "--format", "json"), + DockerCliComposeVersionResponse.class); + this.logger.trace(LogMessage.format("Using docker compose $s", response.version())); + return List.of("docker", "compose"); + } + catch (ProcessExitException ex) { + // Ignore and try docker-compose + } + try { + DockerCliComposeVersionResponse response = DockerJson.deserialize( + processRunner.run("docker-compose", "version", "--format", "json"), + DockerCliComposeVersionResponse.class); + this.logger.trace(LogMessage.format("Using docker-compose $s", response.version())); + return List.of("docker-compose"); + } + catch (ProcessStartException ex) { + throw new DockerProcessStartException( + "Unable to start 'docker-compose' process or use 'docker compose'. Is docker correctly installed?", + ex); + } + } + + /** + * Run the given {@link DockerCli} command and return the response. + * @param the response type + * @param dockerCommand the command to run + * @return the response + */ + R run(DockerCliCommand dockerCommand) { + List command = createCommand(dockerCommand.getType()); + command.addAll(dockerCommand.getCommand()); + String json = this.processRunner.run(command.toArray(new String[0])); + return dockerCommand.deserialize(json); + } + + private List createCommand(Type type) { + return switch (type) { + case DOCKER -> new ArrayList<>(this.dockerCommand); + case DOCKER_COMPOSE -> { + List result = new ArrayList<>(this.dockerComposeCommand); + if (this.composeFile != null) { + result.add("--file"); + result.add(this.composeFile.toString()); + } + result.add("--ansi"); + result.add("never"); + for (String profile : this.activeProfiles) { + result.add("--profile"); + result.add(profile); + } + yield result; + } + }; + } + + /** + * Return the {@link DockerComposeFile} being used by this CLI instance. + * @return the docker compose file + */ + DockerComposeFile getDockerComposeFile() { + return this.composeFile; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java new file mode 100644 index 00000000000..0c0d509053e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/** + * Commands that can be executed by the {@link DockerCli}. + * + * @param the response type + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +abstract sealed class DockerCliCommand { + + private final Type type; + + private final Class responseType; + + private final boolean listResponse; + + private final List command; + + private DockerCliCommand(Type type, Class responseType, boolean listResponse, String... command) { + this.type = type; + this.responseType = responseType; + this.listResponse = listResponse; + this.command = List.of(command); + } + + Type getType() { + return this.type; + } + + List getCommand() { + return this.command; + } + + @SuppressWarnings("unchecked") + R deserialize(String json) { + if (this.responseType == Void.class) { + return null; + } + return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType) + : DockerJson.deserializeToList(json, this.responseType)); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DockerCliCommand other = (DockerCliCommand) obj; + boolean result = true; + result = result && this.type == other.type; + result = result && this.responseType == other.responseType; + result = result && this.listResponse == other.listResponse; + result = result && this.command.equals(other.command); + return result; + } + + @Override + public int hashCode() { + return Objects.hash(this.type, this.responseType, this.listResponse, this.command); + } + + @Override + public String toString() { + return "DockerCliCommand [type=%s, responseType=%s, listResponse=%s, command=%s]".formatted(this.type, + this.responseType, this.listResponse, this.command); + } + + protected static String[] join(Collection command, Collection args) { + List result = new ArrayList<>(command); + result.addAll(args); + return result.toArray(new String[0]); + } + + /** + * The {@code docker context} command. + */ + static final class Context extends DockerCliCommand> { + + Context() { + super(Type.DOCKER, DockerCliContextResponse.class, true, "context", "ls", "--format={{ json . }}"); + } + + } + + /** + * The {@code docker inspect} command. + */ + static final class Inspect extends DockerCliCommand> { + + Inspect(Collection ids) { + super(Type.DOCKER, DockerCliInspectResponse.class, true, + join(List.of("inspect", "--format={{ json . }}"), ids)); + } + + } + + /** + * The {@code docker compose config} command. + */ + static final class ComposeConfig extends DockerCliCommand { + + ComposeConfig() { + super(Type.DOCKER_COMPOSE, DockerCliComposeConfigResponse.class, false, "config", "--format=json"); + } + + } + + /** + * The {@code docker compose ps} command. + */ + static final class ComposePs extends DockerCliCommand> { + + ComposePs() { + super(Type.DOCKER_COMPOSE, DockerCliComposePsResponse.class, true, "ps", "--format=json"); + } + + } + + /** + * The {@code docker compose up} command. + */ + static final class ComposeUp extends DockerCliCommand { + + ComposeUp() { + super(Type.DOCKER_COMPOSE, Void.class, false, "up", "--no-color", "--quiet-pull", "--detach", "--wait"); + } + + } + + /** + * The {@code docker compose down} command. + */ + static final class ComposeDown extends DockerCliCommand { + + ComposeDown(Duration timeout) { + super(Type.DOCKER_COMPOSE, Void.class, false, "down", "--timeout", Long.toString(timeout.toSeconds())); + } + + } + + /** + * The {@code docker compose start} command. + */ + static final class ComposeStart extends DockerCliCommand { + + ComposeStart() { + super(Type.DOCKER_COMPOSE, Void.class, false, "start", "--no-color", "--quiet-pull", "--detach", "--wait"); + } + + } + + /** + * The {@code docker compose stop} command. + */ + static final class ComposeStop extends DockerCliCommand { + + ComposeStop(Duration timeout) { + super(Type.DOCKER_COMPOSE, Void.class, false, "stop", "--timeout", Long.toString(timeout.toSeconds())); + } + + } + + /** + * Command Types. + */ + enum Type { + + /** + * A command executed using {@code docker}. + */ + DOCKER, + + /** + * A command executed using {@code docker compose} or {@code docker-compose}. + */ + DOCKER_COMPOSE + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponse.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponse.java new file mode 100644 index 00000000000..a72dadf3f7f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponse.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Map; + +/** + * Response from {@link DockerCliCommand.ComposeConfig docker compose config}. + * + * @param name project name + * @param services services + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +record DockerCliComposeConfigResponse(String name, Map services) { + + /** + * Docker compose service. + * + * @param image the image + */ + record Service(String image) { + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponse.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponse.java new file mode 100644 index 00000000000..b574234e5f2 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * Response from {@link DockerCliCommand.ComposePs docker compose ps}. + * + * @param id the container ID + * @param name the name of the service + * @param image the image reference + * @param state the state of the container + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +record DockerCliComposePsResponse(String id, String name, String image, String state) { + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponse.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponse.java new file mode 100644 index 00000000000..147ed952e38 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * Response from {@code docker compose version}. + * + * @param version docker compose version + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +record DockerCliComposeVersionResponse(String version) { + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliContextResponse.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliContextResponse.java new file mode 100644 index 00000000000..619946ca99c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliContextResponse.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * Response from {@link DockerCliCommand.Context docker context}. + * + * @param name the name of the context + * @param current if the context is the current one + * @param dockerEndpoint the endpoint of the docker daemon + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +record DockerCliContextResponse(String name, boolean current, String dockerEndpoint) { + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponse.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponse.java new file mode 100644 index 00000000000..3c3b7d2c753 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponse.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.List; +import java.util.Map; + +/** + * Response from {@link DockerCliCommand.Inspect docker inspect}. + * + * @param id the container id + * @param config the config + * @param hostConfig the host config + * @param networkSettings the network settings + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +record DockerCliInspectResponse(String id, DockerCliInspectResponse.Config config, + DockerCliInspectResponse.NetworkSettings networkSettings, DockerCliInspectResponse.HostConfig hostConfig) { + + /** + * Configuration for the container that is portable between hosts. + * + * @param image the name (or reference) of the image + * @param labels user-defined key/value metadata + * @param exposedPorts the mapping of exposed ports + * @param env a list of environment variables in the form {@code VAR=value} + */ + record Config(String image, Map labels, Map exposedPorts, List env) { + + } + + /** + * Empty object used with {@link Config#exposedPorts()}. + */ + record ExposedPort() { + + } + + /** + * A container's resources (cgroups config, ulimits, etc). + * + * @param networkMode the network mode to use for this container + */ + record HostConfig(String networkMode) { + + } + + /** + * The network settings in the API. + * + * @param ports the mapping of container ports to host ports + */ + record NetworkSettings(Map> ports) { + + } + + /** + * Port mapping details. + * + * @param hostIp the host IP + * @param hostPort the host port + */ + record HostPort(String hostIp, String hostPort) { + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java new file mode 100644 index 00000000000..703ee41ae02 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.time.Duration; +import java.util.List; +import java.util.Set; + +/** + * Provides a high-level API to work with Docker compose. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface DockerCompose { + + /** + * Timeout duration used to request a forced shutdown. + */ + Duration FORCE_SHUTDOWN = Duration.ZERO; + + /** + * Run {@code docker compose up} to startup services. Waits until all contains are + * started and healthy. + */ + void up(); + + /** + * Run {@code docker compose down} to shutdown any running services. + * @param timeout the amount of time to wait or {@link #FORCE_SHUTDOWN} to shutdown + * without waiting. + */ + void down(Duration timeout); + + /** + * Run {@code docker compose start} to startup services. Waits until all contains are + * started and healthy. + */ + void start(); + + /** + * Run {@code docker compose stop} to shutdown any running services. + * @param timeout the amount of time to wait or {@link #FORCE_SHUTDOWN} to shutdown + * without waiting. + */ + void stop(Duration timeout); + + /** + * Return if services have been defined in the {@link DockerComposeFile} for the + * active profiles. + * @return {@code true} if services have been defined + * @see #hasDefinedServices() + */ + boolean hasDefinedServices(); + + /** + * Return if services defined in the {@link DockerComposeFile} for the active profile + * are running. + * @return {@code true} if services are running + * @see #hasDefinedServices() + * @see #getRunningServices() + */ + boolean hasRunningServices(); + + /** + * Return the running services for the active profile, or an empty list if no services + * are running. + * @return the list of running services + */ + List getRunningServices(); + + /** + * Factory method used to create a {@link DockerCompose} instance. + * @param file the docker compose file + * @param hostname the hostname used for services or {@code null} if the hostname + * should be deduced + * @param activeProfiles a set of the profiles that should be activated + * @return a {@link DockerCompose} instance + */ + static DockerCompose get(DockerComposeFile file, String hostname, Set activeProfiles) { + DockerCli cli = new DockerCli(null, file, activeProfiles); + return new DefaultDockerCompose(cli, hostname); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeFile.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeFile.java new file mode 100644 index 00000000000..b47d35e86fc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeFile.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * A reference to a docker compose file (usually named {@code compose.yaml}). + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + * @see #of(File) + * @see #find(File) + */ +public final class DockerComposeFile { + + private static final List SEARCH_ORDER = List.of("compose.yaml", "compose.yml", "docker-compose.yaml", + "docker-compose.yml"); + + private final File file; + + private DockerComposeFile(File file) { + try { + this.file = file.getCanonicalFile(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DockerComposeFile other = (DockerComposeFile) obj; + return this.file.equals(other.file); + } + + @Override + public int hashCode() { + return this.file.hashCode(); + } + + @Override + public String toString() { + return this.file.toString(); + } + + /** + * Find the docker compose file by searching in the given working directory. Files are + * considered in the same order that {@code docker compose} uses, namely: + *
    + *
  • {@code compose.yaml}
  • + *
  • {@code compose.yml}
  • + *
  • {@code docker-compose.yaml}
  • + *
  • {@code docker-compose.yml}
  • + *
+ * @param workingDirectory the working directory to search or {@code null} to use the + * current directory + * @return the located file or {@code null} if no docker compose file can be found + */ + public static DockerComposeFile find(File workingDirectory) { + File base = (workingDirectory != null) ? workingDirectory : new File("."); + if (!base.exists()) { + return null; + } + Assert.isTrue(base.isDirectory(), () -> "'%s' is not a directory".formatted(base)); + Path basePath = base.toPath(); + for (String candidate : SEARCH_ORDER) { + Path resolved = basePath.resolve(candidate); + if (Files.exists(resolved)) { + return of(resolved.toAbsolutePath().toFile()); + } + } + return null; + } + + /** + * Create a new {@link DockerComposeFile} for the given {@link File}. + * @param file the source file + * @return the docker compose file + */ + public static DockerComposeFile of(File file) { + Assert.notNull(file, "File must not be null"); + Assert.isTrue(file.exists(), () -> "'%s' does not exist".formatted(file)); + Assert.isTrue(file.isFile(), () -> "'%s' is not a file".formatted(file)); + return new DockerComposeFile(file); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOrigin.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOrigin.java new file mode 100644 index 00000000000..ed2fb98de9a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerComposeOrigin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import org.springframework.boot.origin.Origin; + +/** + * An origin which points to a service defined in docker compose. + * + * @param composeFile docker compose file + * @param serviceName name of the docker compose service + * @author Moritz Halbritter + * @author Andy Wilkinson + * @since 3.1.0 + */ +public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceName) implements Origin { + + @Override + public String toString() { + return "Docker compose service '%s' defined in '%s'".formatted(this.serviceName, + (this.composeFile != null) ? this.composeFile : "default compose file"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerEnv.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerEnv.java new file mode 100644 index 00000000000..47f761959d0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerEnv.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.CollectionUtils; + +/** + * Parses and provides access to docker {@code env} data. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerEnv { + + private final Map map; + + /** + * Create a new {@link DockerEnv} instance. + * @param env a list of env entries in the form {@code name=value} or {@code name}. + */ + DockerEnv(List env) { + this.map = parse(env); + } + + private Map parse(List env) { + if (CollectionUtils.isEmpty(env)) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap<>(); + env.stream().map(this::parseEntry).forEach((entry) -> result.put(entry.key(), entry.value())); + return Collections.unmodifiableMap(result); + } + + private Entry parseEntry(String entry) { + int index = entry.indexOf('='); + if (index != -1) { + String key = entry.substring(0, index); + String value = entry.substring(index + 1); + return new Entry(key, value); + } + return new Entry(entry, null); + } + + /** + * Return the env as a {@link Map}. + * @return the env as a map + */ + Map asMap() { + return this.map; + } + + private record Entry(String key, String value) { + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerException.java new file mode 100644 index 00000000000..767c6a394bf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * Base class for docker exceptions. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public abstract class DockerException extends RuntimeException { + + public DockerException(String message) { + super(message); + } + + public DockerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerHost.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerHost.java new file mode 100644 index 00000000000..f224f209959 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerHost.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.net.URI; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.util.StringUtils; + +/** + * A docker host as defined by the user or deduced. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +final class DockerHost { + + private static final String LOCALHOST = "127.0.0.1"; + + private String host; + + private DockerHost(String host) { + this.host = host; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DockerHost other = (DockerHost) obj; + return this.host.equals(other.host); + } + + @Override + public int hashCode() { + return this.host.hashCode(); + } + + @Override + public String toString() { + return this.host; + } + + /** + * Get or deduce a new {@link DockerHost} instance. + * @param host the host to use or {@code null} to deduce + * @param contextsSupplier a supplier to provide a list of + * {@link DockerCliContextResponse} + * @return a new docker host instance + */ + static DockerHost get(String host, Supplier> contextsSupplier) { + return get(host, System::getenv, contextsSupplier); + } + + /** + * Get or deduce a new {@link DockerHost} instance. + * @param host the host to use or {@code null} to deduce + * @param systemEnv access to the system environment + * @param contextsSupplier a supplier to provide a list of + * {@link DockerCliContextResponse} + * @return a new docker host instance + */ + static DockerHost get(String host, Function systemEnv, + Supplier> contextsSupplier) { + host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv); + host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv); + host = (StringUtils.hasText(host)) ? host : fromCurrentContext(contextsSupplier); + host = (StringUtils.hasText(host)) ? host : LOCALHOST; + return new DockerHost(host); + } + + private static String fromServicesHostEnv(Function systemEnv) { + return systemEnv.apply("SERVICES_HOST"); + } + + private static String fromDockerHostEnv(Function systemEnv) { + return fromEndpoint(systemEnv.apply("DOCKER_HOST")); + } + + private static String fromCurrentContext(Supplier> contextsSupplier) { + DockerCliContextResponse current = getCurrentContext(contextsSupplier.get()); + return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null; + } + + private static DockerCliContextResponse getCurrentContext(List candidates) { + return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null); + } + + private static String fromEndpoint(String endpoint) { + return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null; + } + + private static String fromUri(URI uri) { + try { + return switch (uri.getScheme()) { + case "http", "https", "tcp" -> uri.getHost(); + default -> null; + }; + } + catch (Exception ex) { + return null; + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerJson.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerJson.java new file mode 100644 index 00000000000..526089f0491 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerJson.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.util.List; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +/** + * Support class used to handle JSON returned from the {@link DockerCli}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +final class DockerJson { + + private static final ObjectMapper objectMapper = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(new ParameterNamesModule()) + .build(); + + private DockerJson() { + } + + /** + * Deserialize JSON to a list. Handles JSON arrays and multiple JSON objects in + * separate lines. + * @param the item type + * @param json the source JSON + * @param itemType the item type + * @return a list of items + */ + static List deserializeToList(String json, Class itemType) { + if (json.startsWith("[")) { + JavaType javaType = objectMapper.getTypeFactory().constructCollectionType(List.class, itemType); + return deserialize(json, javaType); + } + return json.trim().lines().map((line) -> deserialize(line, itemType)).toList(); + } + + /** + * Deserialize JSON to an object instance. + * @param the result type + * @param json the source JSON + * @param type the result type + * @return the deserialized result + */ + static T deserialize(String json, Class type) { + return deserialize(json, objectMapper.getTypeFactory().constructType(type)); + } + + private static T deserialize(String json, JavaType type) { + try { + return objectMapper.readValue(json.trim(), type); + } + catch (IOException ex) { + throw new DockerOutputParseException(json, ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerNotRunningException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerNotRunningException.java new file mode 100644 index 00000000000..144abe93f96 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerNotRunningException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * {@link DockerException} thrown if the docker daemon is not running. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class DockerNotRunningException extends DockerException { + + private final String errorOutput; + + DockerNotRunningException(String errorOutput, Throwable cause) { + super("Docker is not running", cause); + this.errorOutput = errorOutput; + } + + /** + * Return the error output returned from docker. + * @return the error output + */ + public String getErrorOutput() { + return this.errorOutput; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerOutputParseException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerOutputParseException.java new file mode 100644 index 00000000000..b36d1c5bd92 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerOutputParseException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * {@link DockerException} thrown if the docker JSON cannot be parsed. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class DockerOutputParseException extends DockerException { + + DockerOutputParseException(String json, Throwable cause) { + super("Failed to parse docker JSON:\n\n" + json, cause); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerProcessStartException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerProcessStartException.java new file mode 100644 index 00000000000..430666c1392 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerProcessStartException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * {@link DockerException} thrown if the docker process cannot be started. Usually + * indicates that docker is not installed. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class DockerProcessStartException extends DockerException { + + DockerProcessStartException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ImageReference.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ImageReference.java new file mode 100644 index 00000000000..81423e99e6f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ImageReference.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * A docker image reference of form + * {@code [/][/][:|@]}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + * @see docker + * documentation + */ +public final class ImageReference { + + private final String reference; + + private final String imageName; + + ImageReference(String reference) { + this.reference = reference; + int lastSlashIndex = reference.lastIndexOf('/'); + String imageTagDigest = (lastSlashIndex != -1) ? reference.substring(lastSlashIndex + 1) : reference; + int digestIndex = imageTagDigest.indexOf('@'); + String imageTag = (digestIndex != -1) ? imageTagDigest.substring(0, digestIndex) : imageTagDigest; + int colon = imageTag.indexOf(':'); + this.imageName = (colon != -1) ? imageTag.substring(0, colon) : imageTag; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageReference other = (ImageReference) obj; + return this.reference.equals(other.reference); + } + + @Override + public int hashCode() { + return this.reference.hashCode(); + } + + @Override + public String toString() { + return this.reference; + } + + /** + * Return the referenced image, excluding the registry or project. For example, a + * reference of {@code my_private.registry:5000/redis:5} would return {@code redis}. + * @return the referenced image + */ + public String getImageName() { + return this.imageName; + } + + /** + * Create an image reference from the given String value. + * @param value the string used to create the reference + * @return an {@link ImageReference} instance + */ + public static ImageReference of(String value) { + return (value != null) ? new ImageReference(value) : null; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessExitException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessExitException.java new file mode 100644 index 00000000000..2afeae28329 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessExitException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +/** + * Exception thrown by {@link ProcessRunner} when the process exits with a non-zero code. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ProcessExitException extends RuntimeException { + + private final int exitCode; + + private final String[] command; + + private final String stdOut; + + private final String stdErr; + + ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr) { + this(exitCode, command, stdOut, stdErr, null); + } + + ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, Throwable cause) { + super(buildMessage(exitCode, command, stdOut, stdErr), cause); + this.exitCode = exitCode; + this.command = command; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + private static String buildMessage(int exitCode, String[] command, String stdOut, String strErr) { + return "'%s' failed with exit code %d.\n\nStdout:\n%s\n\nStderr:\n%s".formatted(String.join(" ", command), + exitCode, stdOut, strErr); + } + + int getExitCode() { + return this.exitCode; + } + + String[] getCommand() { + return this.command; + } + + String getStdOut() { + return this.stdOut; + } + + String getStdErr() { + return this.stdErr; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java new file mode 100644 index 00000000000..a4921f18a2f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; + +/** + * Runs a process and captures the result. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ProcessRunner { + + private static final String USR_LOCAL_BIN = "/usr/local/bin"; + + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase().contains("mac"); + + private static final Log logger = LogFactory.getLog(ProcessRunner.class); + + private final File workingDirectory; + + /** + * Create a new {@link ProcessRunner} instance. + */ + ProcessRunner() { + this(null); + } + + /** + * Create a new {@link ProcessRunner} instance. + * @param workingDirectory the working directory for the process + */ + ProcessRunner(File workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** + * Runs the given {@code command}. If the process exits with an error code other than + * zero, an {@link ProcessExitException} will be thrown. + * @param command the command to run + * @return the output of the command + * @throws ProcessExitException if execution failed + */ + String run(String... command) { + logger.trace(LogMessage.of(() -> "Running '%s'".formatted(String.join(" ", command)))); + Process process = startProcess(command); + ReaderThread stdOutReader = new ReaderThread(process.getInputStream(), "stdout"); + ReaderThread stdErrReader = new ReaderThread(process.getErrorStream(), "stderr"); + logger.trace("Waiting for process exit"); + int exitCode = waitForProcess(process); + logger.trace(LogMessage.format("Process exited with exit code %d", exitCode)); + String stdOut = stdOutReader.toString(); + String stdErr = stdErrReader.toString(); + if (exitCode != 0) { + throw new ProcessExitException(exitCode, command, stdOut, stdErr); + } + return stdOut; + } + + private Process startProcess(String[] command) { + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.directory(this.workingDirectory); + try { + return processBuilder.start(); + } + catch (IOException ex) { + String path = processBuilder.environment().get("PATH"); + if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) + && !command[0].startsWith(USR_LOCAL_BIN + "/")) { + String[] localCommand = command.clone(); + localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; + return startProcess(localCommand); + } + throw new ProcessStartException(command, ex); + } + } + + private int waitForProcess(Process process) { + try { + return process.waitFor(); + } + catch (InterruptedException ex) { + throw new IllegalStateException("Interrupted waiting for %s".formatted(process)); + } + } + + /** + * Thread used to read stream input from the process. + */ + private static class ReaderThread extends Thread { + + private final InputStream source; + + private final ByteArrayOutputStream content = new ByteArrayOutputStream(); + + private final CountDownLatch latch = new CountDownLatch(1); + + ReaderThread(InputStream source, String name) { + this.source = source; + setName("OutputReader-" + name); + setDaemon(true); + start(); + } + + @Override + public void run() { + try { + this.source.transferTo(this.content); + this.latch.countDown(); + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to read process stream", ex); + } + } + + @Override + public String toString() { + try { + this.latch.await(); + return new String(this.content.toByteArray(), StandardCharsets.UTF_8); + } + catch (InterruptedException ex) { + return null; + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessStartException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessStartException.java new file mode 100644 index 00000000000..92a66cb9352 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessStartException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; + +/** + * Exception thrown by {@link ProcessRunner} when a processes will not start. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ProcessStartException extends RuntimeException { + + ProcessStartException(String[] command, IOException ex) { + super("Unable to start command %s".formatted(String.join(" ", command)), ex); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java new file mode 100644 index 00000000000..6cb7be8548a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/RunningService.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Map; + +/** + * Provides details of a running docker compose service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface RunningService { + + /** + * Return the name of the service. + * @return the service name + */ + String name(); + + /** + * Return the image being used by the service. + * @return the service image + */ + ImageReference image(); + + /** + * Return the host that can be used to connect to the service. + * @return the service host + */ + String host(); + + /** + * Return the ports that can be used to connect to the service. + * @return the service ports + */ + ConnectionPorts ports(); + + /** + * Return the environment defined for the service. + * @return the service env + */ + Map env(); + + /** + * Return the labels attached to the service. + * @return the service labels + */ + Map labels(); + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/package-info.java new file mode 100644 index 00000000000..fff829479d3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Core interfaces and classes for working with docker compose. + */ +package org.springframework.boot.docker.compose.core; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java new file mode 100644 index 00000000000..025d2dc8e64 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.SpringApplicationShutdownHandlers; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.docker.compose.core.DockerCompose; +import org.springframework.boot.docker.compose.core.DockerComposeFile; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Shutdown; +import org.springframework.boot.docker.compose.lifecycle.DockerComposeProperties.Startup; +import org.springframework.boot.docker.compose.readiness.ServiceReadinessChecks; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.core.log.LogMessage; + +/** + * Manages the lifecycle for docker compose services. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @see DockerComposeListener + */ +class DockerComposeLifecycleManager { + + private static final Log logger = LogFactory.getLog(DockerComposeLifecycleManager.class); + + private static final Object IGNORE_LABEL = "org.springframework.boot.ignore"; + + private final File workingDirectory; + + private final ApplicationContext applicationContext; + + private final ClassLoader classLoader; + + private final SpringApplicationShutdownHandlers shutdownHandlers; + + private final DockerComposeProperties properties; + + private final Set> eventListeners; + + private final DockerComposeSkipCheck skipCheck; + + private final ServiceReadinessChecks serviceReadinessChecks; + + DockerComposeLifecycleManager(ApplicationContext applicationContext, Binder binder, + SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties, + Set> eventListeners) { + this(null, applicationContext, binder, shutdownHandlers, properties, eventListeners, + new DockerComposeSkipCheck(), null); + } + + DockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder, + SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties, + Set> eventListeners, DockerComposeSkipCheck skipCheck, + ServiceReadinessChecks serviceReadinessChecks) { + this.workingDirectory = workingDirectory; + this.applicationContext = applicationContext; + this.classLoader = applicationContext.getClassLoader(); + this.shutdownHandlers = shutdownHandlers; + this.properties = properties; + this.eventListeners = eventListeners; + this.skipCheck = skipCheck; + this.serviceReadinessChecks = (serviceReadinessChecks != null) ? serviceReadinessChecks + : new ServiceReadinessChecks(this.classLoader, applicationContext.getEnvironment(), binder); + } + + void startup() { + if (!this.properties.isEnabled()) { + logger.trace("Docker compose support not enabled"); + return; + } + if (this.skipCheck.shouldSkip(this.classLoader, logger, this.properties.getSkip())) { + logger.trace("Docker compose support skipped"); + return; + } + DockerComposeFile composeFile = getComposeFile(); + Set activeProfiles = this.properties.getProfiles().getActive(); + DockerCompose dockerCompose = getDockerCompose(composeFile, activeProfiles); + if (!dockerCompose.hasDefinedServices()) { + logger.warn(LogMessage.format("No services defined in docker compose file '%s' with active profiles %s", + composeFile, activeProfiles)); + return; + } + LifecycleManagement lifecycleManagement = this.properties.getLifecycleManagement(); + Startup startup = this.properties.getStartup(); + Shutdown shutdown = this.properties.getShutdown(); + if (lifecycleManagement.shouldStartup() && !dockerCompose.hasRunningServices()) { + startup.getCommand().applyTo(dockerCompose); + if (lifecycleManagement.shouldShutdown()) { + this.shutdownHandlers.add(() -> shutdown.getCommand().applyTo(dockerCompose, shutdown.getTimeout())); + } + } + List runningServices = new ArrayList<>(dockerCompose.getRunningServices()); + runningServices.removeIf(this::isIgnored); + this.serviceReadinessChecks.waitUntilReady(runningServices); + publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, runningServices)); + } + + protected DockerComposeFile getComposeFile() { + DockerComposeFile composeFile = (this.properties.getFile() != null) + ? DockerComposeFile.of(this.properties.getFile()) : DockerComposeFile.find(this.workingDirectory); + logger.info(LogMessage.format("Found docker compose file '%s'", composeFile)); + return composeFile; + } + + protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles) { + return DockerCompose.get(composeFile, this.properties.getHost(), activeProfiles); + } + + private boolean isIgnored(RunningService service) { + return service.labels().containsKey(IGNORE_LABEL); + } + + /** + * Publish a {@link DockerComposeServicesReadyEvent} directly to the event listeners + * since we cannot call {@link ApplicationContext#publishEvent} this early. + * @param event the event to publish + */ + private void publishEvent(DockerComposeServicesReadyEvent event) { + SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(); + this.eventListeners.forEach(multicaster::addApplicationListener); + multicaster.multicastEvent(event); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListener.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListener.java new file mode 100644 index 00000000000..301d654ddbf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListener.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.Set; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationShutdownHandlers; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * {@link ApplicationListener} used to setup a {@link DockerComposeLifecycleManager}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeListener implements ApplicationListener { + + private final SpringApplicationShutdownHandlers shutdownHandlers; + + DockerComposeListener() { + this(SpringApplication.getShutdownHandlers()); + } + + DockerComposeListener(SpringApplicationShutdownHandlers shutdownHandlers) { + this.shutdownHandlers = SpringApplication.getShutdownHandlers(); + } + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + ConfigurableApplicationContext applicationContext = event.getApplicationContext(); + Binder binder = Binder.get(applicationContext.getEnvironment()); + DockerComposeProperties properties = DockerComposeProperties.get(binder); + Set> eventListeners = event.getSpringApplication().getListeners(); + createDockerComposeLifecycleManager(applicationContext, binder, properties, eventListeners).startup(); + } + + protected DockerComposeLifecycleManager createDockerComposeLifecycleManager( + ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties, + Set> eventListeners) { + return new DockerComposeLifecycleManager(applicationContext, binder, this.shutdownHandlers, properties, + eventListeners); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java new file mode 100644 index 00000000000..d6058e88014 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeProperties.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.io.File; +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; + +/** + * Configuration properties for the 'docker compose'. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +@ConfigurationProperties(DockerComposeProperties.NAME) +public class DockerComposeProperties { + + static final String NAME = "spring.docker.compose"; + + /** + * Whether docker compose support is enabled. + */ + private boolean enabled = true; + + /** + * Path to a specific docker compose configuration file. + */ + private File file; + + /** + * Docker compose lifecycle management. + */ + private LifecycleManagement lifecycleManagement = LifecycleManagement.START_AND_STOP; + + /** + * Hostname or IP of the machine where the docker containers are started. + */ + private String host; + + /** + * Start configuration. + */ + private final Startup startup = new Startup(); + + /** + * Stop configuration. + */ + private final Shutdown shutdown = new Shutdown(); + + /** + * Profiles configuration. + */ + private final Profiles profiles = new Profiles(); + + private final Skip skip = new Skip(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public File getFile() { + return this.file; + } + + public void setFile(File file) { + this.file = file; + } + + public LifecycleManagement getLifecycleManagement() { + return this.lifecycleManagement; + } + + public void setLifecycleManagement(LifecycleManagement lifecycleManagement) { + this.lifecycleManagement = lifecycleManagement; + } + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public Startup getStartup() { + return this.startup; + } + + public Shutdown getShutdown() { + return this.shutdown; + } + + public Profiles getProfiles() { + return this.profiles; + } + + public Skip getSkip() { + return this.skip; + } + + static DockerComposeProperties get(Binder binder) { + return binder.bind(NAME, DockerComposeProperties.class).orElseGet(DockerComposeProperties::new); + } + + /** + * Startup properties. + */ + public static class Startup { + + /** + * Command used to start docker compose. + */ + private StartupCommand command = StartupCommand.UP; + + public StartupCommand getCommand() { + return this.command; + } + + public void setCommand(StartupCommand command) { + this.command = command; + } + + } + + /** + * Shutdown properties. + */ + public static class Shutdown { + + /** + * Command used to stop docker compose. + */ + private ShutdownCommand command = ShutdownCommand.DOWN; + + /** + * Timeout for stopping docker compose. Use '0' for forced stop. + */ + private Duration timeout = Duration.ofSeconds(10); + + public ShutdownCommand getCommand() { + return this.command; + } + + public void setCommand(ShutdownCommand command) { + this.command = command; + } + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + } + + /** + * Profiles properties. + */ + public static class Profiles { + + /** + * Docker compose profiles that should be active. + */ + private Set active = new LinkedHashSet<>(); + + public Set getActive() { + return this.active; + } + + public void setActive(Set active) { + this.active = active; + } + + } + + /** + * Skip options. + */ + public static class Skip { + + /** + * Whether to skip in tests. + */ + private boolean inTests = true; + + public boolean isInTests() { + return this.inTests; + } + + public void setInTests(boolean inTests) { + this.inTests = inTests; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEvent.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEvent.java new file mode 100644 index 00000000000..cae40b15aaf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.List; + +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} published when docker compose {@link RunningService} instance + * are available. This even is published from the {@link ApplicationPreparedEvent} that + * performs the docker compose startup. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class DockerComposeServicesReadyEvent extends ApplicationEvent { + + private final List runningServices; + + DockerComposeServicesReadyEvent(ApplicationContext source, List runningServices) { + super(source); + this.runningServices = runningServices; + } + + @Override + public ApplicationContext getSource() { + return (ApplicationContext) super.getSource(); + } + + /** + * Return the relevant docker compose services that are running. + * @return the running services + */ + public List getRunningServices() { + return this.runningServices; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeSkipCheck.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeSkipCheck.java new file mode 100644 index 00000000000..f63c925b287 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeSkipCheck.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.SpringApplicationAotProcessor; +import org.springframework.util.ClassUtils; + +/** + * Checks if docker compose support should be skipped. + * + * @author Phillip Webb + */ +class DockerComposeSkipCheck { + + private static final Set REQUIRED_CLASSES = Set.of("org.junit.jupiter.api.Test", "org.junit.Test"); + + private static final Set SKIPPED_STACK_ELEMENTS; + + static { + Set skipped = new LinkedHashSet<>(); + skipped.add("org.junit.runners."); + skipped.add("org.junit.platform."); + skipped.add("org.springframework.boot.test."); + skipped.add(SpringApplicationAotProcessor.class.getName()); + skipped.add("cucumber.runtime."); + SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped); + } + + boolean shouldSkip(ClassLoader classLoader, Log logger, DockerComposeProperties.Skip properties) { + if (properties.isInTests() && hasAtLeastOneRequiredClass(classLoader)) { + Thread thread = Thread.currentThread(); + for (StackTraceElement element : thread.getStackTrace()) { + if (isSkippedStackElement(element)) { + return true; + } + } + } + return false; + } + + private boolean hasAtLeastOneRequiredClass(ClassLoader classLoader) { + for (String requiredClass : REQUIRED_CLASSES) { + if (ClassUtils.isPresent(requiredClass, classLoader)) { + return true; + } + } + return false; + } + + private static boolean isSkippedStackElement(StackTraceElement element) { + for (String skipped : SKIPPED_STACK_ELEMENTS) { + if (element.getClassName().startsWith(skipped)) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagement.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagement.java new file mode 100644 index 00000000000..4c8b0dac60d --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagement.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +/** + * Docker compose lifecycle management. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public enum LifecycleManagement { + + /** + * Don't start or stop docker compose. + */ + NONE(false, false), + + /** + * Only start docker compose if it's not running. + */ + START_ONLY(true, false), + + /** + * Start and stop docker compose if it's not running. + */ + START_AND_STOP(true, true); + + private final boolean startup; + + private final boolean shutdown; + + LifecycleManagement(boolean startup, boolean shutdown) { + this.startup = startup; + this.shutdown = shutdown; + } + + /** + * Return whether docker compose should be started. + * @return whether docker compose should be started. + */ + boolean shouldStartup() { + return this.startup; + } + + /** + * Return whether docker compose should be stopped. + * @return whether docker compose should be stopped + */ + boolean shouldShutdown() { + return this.shutdown; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommand.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommand.java new file mode 100644 index 00000000000..3772375fed2 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommand.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.time.Duration; +import java.util.function.BiConsumer; + +import org.springframework.boot.docker.compose.core.DockerCompose; + +/** + * Command used to shutdown docker compose. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public enum ShutdownCommand { + + /** + * Shutdown using {@code docker compose down}. + */ + DOWN(DockerCompose::down), + + /** + * Shutdown using {@code docker compose stop}. + */ + STOP(DockerCompose::stop); + + private final BiConsumer action; + + ShutdownCommand(BiConsumer action) { + this.action = action; + } + + void applyTo(DockerCompose dockerCompose, Duration timeout) { + this.action.accept(dockerCompose, timeout); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/StartupCommand.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/StartupCommand.java new file mode 100644 index 00000000000..3e1d315dabc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/StartupCommand.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.function.Consumer; + +import org.springframework.boot.docker.compose.core.DockerCompose; + +/** + * Command used to startup docker compose. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public enum StartupCommand { + + /** + * Startup using {@code docker compose up}. + */ + UP(DockerCompose::up), + + /** + * Startup using {@code docker compose start}. + */ + START(DockerCompose::start); + + private final Consumer action; + + StartupCommand(Consumer action) { + this.action = action; + } + + void applyTo(DockerCompose dockerCompose) { + this.action.accept(dockerCompose); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/package-info.java new file mode 100644 index 00000000000..c274a58eaae --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Lifecycle management for docker compose with the context of a Spring application. + */ +package org.springframework.boot.docker.compose.lifecycle; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessProperties.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessProperties.java new file mode 100644 index 00000000000..8e4a561df80 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessProperties.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; + +/** + * Readiness configuration properties. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +@ConfigurationProperties(ReadinessProperties.NAME) +public class ReadinessProperties { + + static final String NAME = "spring.docker.compose.readiness"; + + /** + * Timeout of the readiness checks. + */ + private Duration timeout = Duration.ofMinutes(2); + + /** + * TCP properties. + */ + private final Tcp tcp = new Tcp(); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public Tcp getTcp() { + return this.tcp; + } + + /** + * Get the properties using the given binder. + * @param binder the binder used to get the properties + * @return a bound {@link ReadinessProperties} instance + */ + static ReadinessProperties get(Binder binder) { + return binder.bind(ReadinessProperties.NAME, ReadinessProperties.class).orElseGet(ReadinessProperties::new); + } + + /** + * TCP properties. + */ + public static class Tcp { + + /** + * Timeout for connections. + */ + private Duration connectTimeout = Duration.ofMillis(200); + + /** + * Timeout for reads. + */ + private Duration readTimeout = Duration.ofMillis(200); + + public Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutException.java new file mode 100644 index 00000000000..59f71edec75 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutException.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; + +import org.springframework.boot.docker.compose.core.RunningService; + +/** + * Exception thrown if readiness checking has timed out. Related + * {@link ServiceNotReadyException} are available from {@link #getSuppressed()}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public final class ReadinessTimeoutException extends RuntimeException { + + private final Duration timeout; + + ReadinessTimeoutException(Duration timeout, List exceptions) { + super(buildMessage(timeout, exceptions)); + this.timeout = timeout; + exceptions.forEach(this::addSuppressed); + } + + private static String buildMessage(Duration timeout, List exceptions) { + List serviceNames = exceptions.stream() + .map(ServiceNotReadyException::getService) + .filter(Objects::nonNull) + .map(RunningService::name) + .toList(); + return "Readiness timeout of %s reached while waiting for services %s".formatted(timeout, serviceNames); + } + + /** + * Return the timeout that was reached. + * @return the timeout + */ + public Duration getTimeout() { + return this.timeout; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyException.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyException.java new file mode 100644 index 00000000000..c360867b3ca --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import org.springframework.boot.docker.compose.core.RunningService; + +/** + * Exception thrown when a single {@link RunningService} is not ready. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + * @see ServiceReadinessCheck + */ +public class ServiceNotReadyException extends RuntimeException { + + private final RunningService service; + + ServiceNotReadyException(RunningService service, String message) { + this(service, message, null); + } + + ServiceNotReadyException(RunningService service, String message, Throwable cause) { + super(message, cause); + this.service = service; + } + + /** + * Return the service that was not reeady. + * @return the non-ready service + */ + public RunningService getService() { + return this.service; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessCheck.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessCheck.java new file mode 100644 index 00000000000..0f4c9d2f62a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessCheck.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.core.env.Environment; + +/** + * Strategy used to check if a {@link RunningService} is ready. Implementations may be + * registered in {@code spring.factories}. The following constructor arguments types are + * supported: + *
    + *
  • {@link ClassLoader}
  • + *
  • {@link Environment}
  • + *
  • {@link Binder}
  • + *
+ * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface ServiceReadinessCheck { + + /** + * Checks whether the given {@code service} is ready. + * @param service service to check + * @throws ServiceNotReadyException if the service is not ready + */ + void check(RunningService service) throws ServiceNotReadyException; + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecks.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecks.java new file mode 100644 index 00000000000..4d1ea225262 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecks.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; +import org.springframework.core.log.LogMessage; + +/** + * A collection of {@link ServiceReadinessCheck} instances that can be used to + * {@link #wait() wait} for {@link RunningService services} to be ready. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class ServiceReadinessChecks { + + private static final Log logger = LogFactory.getLog(ServiceReadinessChecks.class); + + private static final String DISABLE_LABEL = "org.springframework.boot.readiness-check.disable"; + + private static final Duration SLEEP_BETWEEN_READINESS_TRIES = Duration.ofSeconds(1); + + private final Clock clock; + + private final Consumer sleep; + + private final ReadinessProperties properties; + + private final List checks; + + public ServiceReadinessChecks(ClassLoader classLoader, Environment environment, Binder binder) { + this(Clock.systemUTC(), ServiceReadinessChecks::sleep, + SpringFactoriesLoader.forDefaultResourceLocation(classLoader), classLoader, environment, binder, + TcpConnectServiceReadinessCheck::new); + } + + ServiceReadinessChecks(Clock clock, Consumer sleep, SpringFactoriesLoader loader, ClassLoader classLoader, + Environment environment, Binder binder, + Function tcpCheckFactory) { + ArgumentResolver argumentResolver = ArgumentResolver.of(ClassLoader.class, classLoader) + .and(Environment.class, environment) + .and(Binder.class, binder); + this.clock = clock; + this.sleep = sleep; + this.properties = ReadinessProperties.get(binder); + this.checks = new ArrayList<>(loader.load(ServiceReadinessCheck.class, argumentResolver)); + this.checks.add(tcpCheckFactory.apply(this.properties.getTcp())); + } + + /** + * Wait for the given services to be ready. + * @param runningServices the services to wait for + */ + public void waitUntilReady(List runningServices) { + Duration timeout = this.properties.getTimeout(); + Instant start = this.clock.instant(); + while (true) { + List exceptions = check(runningServices); + if (exceptions.isEmpty()) { + return; + } + Duration elapsed = Duration.between(start, this.clock.instant()); + if (elapsed.compareTo(timeout) > 0) { + throw new ReadinessTimeoutException(timeout, exceptions); + } + this.sleep.accept(SLEEP_BETWEEN_READINESS_TRIES); + } + } + + private List check(List runningServices) { + List exceptions = null; + for (RunningService service : runningServices) { + if (isDisabled(service)) { + continue; + } + logger.trace(LogMessage.format("Checking readiness of service '%s'", service)); + for (ServiceReadinessCheck check : this.checks) { + try { + check.check(service); + logger.trace(LogMessage.format("Service '%s' is ready", service)); + } + catch (ServiceNotReadyException ex) { + logger.trace(LogMessage.format("Service '%s' is not ready", service), ex); + exceptions = (exceptions != null) ? exceptions : new ArrayList<>(); + exceptions.add(ex); + } + } + } + return (exceptions != null) ? exceptions : Collections.emptyList(); + } + + private boolean isDisabled(RunningService service) { + return service.labels().containsKey(DISABLE_LABEL); + } + + private static void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheck.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheck.java new file mode 100644 index 00000000000..4e6f887e91c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheck.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; + +import org.springframework.boot.docker.compose.core.RunningService; + +/** + * Default {@link ServiceReadinessCheck} that readiness by connecting to the exposed TCP + * ports. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TcpConnectServiceReadinessCheck implements ServiceReadinessCheck { + + private final String DISABLE_LABEL = "org.springframework.boot.readiness-check.tcp.disable"; + + private final ReadinessProperties.Tcp properties; + + TcpConnectServiceReadinessCheck(ReadinessProperties.Tcp properties) { + this.properties = properties; + } + + @Override + public void check(RunningService service) { + if (service.labels().containsKey(this.DISABLE_LABEL)) { + return; + } + for (int port : service.ports().getAll("tcp")) { + check(service, port); + } + } + + private void check(RunningService service, int port) { + int connectTimeout = (int) this.properties.getConnectTimeout().toMillis(); + int readTimeout = (int) this.properties.getReadTimeout().toMillis(); + try (Socket socket = new Socket()) { + socket.setSoTimeout(readTimeout); + socket.connect(new InetSocketAddress(service.host(), port), connectTimeout); + check(service, port, socket); + } + catch (IOException ex) { + throw new ServiceNotReadyException(service, "IOException while connecting to port %s".formatted(port), ex); + } + } + + private void check(RunningService service, int port, Socket socket) throws IOException { + try { + // -1 is indicates the socket has been closed immediately + // Other responses or a timeout are considered as success + if (socket.getInputStream().read() == -1) { + throw new ServiceNotReadyException(service, + "Immediate disconnect while connecting to port %s".formatted(port)); + } + } + catch (SocketTimeoutException ex) { + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/package-info.java new file mode 100644 index 00000000000..17542adf3ae --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/readiness/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Service readiness checks. + */ +package org.springframework.boot.docker.compose.readiness; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..938d82dadaf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection; + +import java.util.Arrays; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Base class for {@link ConnectionDetailsFactory} implementations that provide + * {@link ConnectionDetails} from a {@link DockerComposeConnectionSource}. + * + * @param the connection details type + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public abstract class DockerComposeConnectionDetailsFactory + implements ConnectionDetailsFactory { + + private final String connectionName; + + private final String[] requiredClassNames; + + /** + * Create a new {@link DockerComposeConnectionDetailsFactory} instance. + * @param connectionName the required connection name + * @param requiredClassNames the names of classes that must be present + */ + protected DockerComposeConnectionDetailsFactory(String connectionName, String... requiredClassNames) { + this.connectionName = connectionName; + this.requiredClassNames = requiredClassNames; + } + + @Override + public final D getConnectionDetails(DockerComposeConnectionSource source) { + return (!accept(source)) ? null : getDockerComposeConnectionDetails(source); + } + + private boolean accept(DockerComposeConnectionSource source) { + return hasRequiredClasses() && this.connectionName.equals(getConnectionName(source.getRunningService())); + } + + private String getConnectionName(RunningService service) { + String connectionName = service.labels().get("org.springframework.boot.service-connection"); + return (connectionName != null) ? connectionName : service.image().getImageName(); + } + + private boolean hasRequiredClasses() { + return ObjectUtils.isEmpty(this.requiredClassNames) || Arrays.stream(this.requiredClassNames) + .allMatch((requiredClassName) -> ClassUtils.isPresent(requiredClassName, null)); + } + + /** + * Get the {@link ConnectionDetails} from the given {@link RunningService} + * {@code source}. May return {@code null} if no connection can be created. Result + * types should consider extending {@link DockerComposeConnectionDetails}. + * @param source the source + * @return the service connection or {@code null}. + */ + protected abstract D getDockerComposeConnectionDetails(DockerComposeConnectionSource source); + + /** + * Convenient base class for {@link ConnectionDetails} results that are backed by a + * {@link RunningService}. + */ + protected static class DockerComposeConnectionDetails implements ConnectionDetails, OriginProvider { + + private final Origin origin; + + /** + * Create a new {@link DockerComposeConnectionDetails} instance. + * @param runningService the source {@link RunningService} + */ + protected DockerComposeConnectionDetails(RunningService runningService) { + Assert.notNull(runningService, "RunningService must not be null"); + this.origin = Origin.from(runningService); + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java new file mode 100644 index 00000000000..1009069dcbc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection; + +import org.springframework.boot.docker.compose.core.RunningService; + +/** + * Passed to {@link DockerComposeConnectionDetailsFactory} to provide details of the + * {@link RunningService running docker compose service}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + * @see DockerComposeConnectionDetailsFactory + */ +public final class DockerComposeConnectionSource { + + private final RunningService runningService; + + /** + * Create a new {@link DockerComposeConnectionSource} instance. + * @param runningService the running docker compose service + */ + DockerComposeConnectionSource(RunningService runningService) { + this.runningService = runningService; + } + + /** + * Return the running docker compose service. + * @return the running service + */ + public RunningService getRunningService() { + return this.runningService; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java new file mode 100644 index 00000000000..a575ca49b92 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeServiceConnectionsApplicationListener.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.lifecycle.DockerComposeServicesReadyEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ApplicationListener} that listens for an {@link DockerComposeServicesReadyEvent} + * in order to establish service connections. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeServiceConnectionsApplicationListener + implements ApplicationListener { + + private final ConnectionDetailsFactories factories; + + DockerComposeServiceConnectionsApplicationListener() { + this(new ConnectionDetailsFactories()); + } + + DockerComposeServiceConnectionsApplicationListener(ConnectionDetailsFactories factories) { + this.factories = factories; + } + + @Override + public void onApplicationEvent(DockerComposeServicesReadyEvent event) { + ApplicationContext applicationContext = event.getSource(); + if (applicationContext instanceof BeanDefinitionRegistry registry) { + registerConnectionDetails(registry, event.getRunningServices()); + } + } + + private void registerConnectionDetails(BeanDefinitionRegistry registry, List runningServices) { + for (RunningService runningService : runningServices) { + DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService); + this.factories.getConnectionDetails(source) + .forEach((connectionDetailsType, connectionDetails) -> register(registry, runningService, + connectionDetailsType, connectionDetails)); + } + } + + @SuppressWarnings("unchecked") + private void register(BeanDefinitionRegistry registry, RunningService runningService, + Class connectionDetailsType, ConnectionDetails connectionDetails) { + String beanName = getBeanName(runningService, connectionDetailsType, connectionDetails); + Class beanType = (Class) connectionDetails.getClass(); + Supplier beanSupplier = () -> (T) connectionDetails; + registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier)); + } + + private String getBeanName(RunningService runningService, Class connectionDetailsType, + ConnectionDetails connectionDetails) { + List parts = new ArrayList<>(); + parts.add(ClassUtils.getShortNameAsProperty(connectionDetailsType)); + parts.add("for"); + parts.addAll(Arrays.asList(runningService.name().split("-"))); + return StringUtils.uncapitalize(parts.stream().map(StringUtils::capitalize).collect(Collectors.joining())); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..2c0b674d10b --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import java.util.List; + +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link ElasticsearchConnectionDetails} for an {@code elasticsearch} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ElasticsearchDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int ELASTICSEARCH_PORT = 9200; + + protected ElasticsearchDockerComposeConnectionDetailsFactory(String name) { + super("elasticsearch"); + } + + @Override + protected ElasticsearchConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new ElasticsearchDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link ElasticsearchConnectionDetails} backed by an {@code elasticsearch} + * {@link RunningService}. + */ + static class ElasticsearchDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements ElasticsearchConnectionDetails { + + private final ElasticsearchEnvironment environment; + + private final List nodes; + + ElasticsearchDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new ElasticsearchEnvironment(service.env()); + this.nodes = List.of(new Node(service.host(), service.ports().get(ELASTICSEARCH_PORT), Protocol.HTTP, + getUsername(), getPassword())); + } + + @Override + public String getUsername() { + return "elastic"; + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public List getNodes() { + return this.nodes; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironment.java new file mode 100644 index 00000000000..08f08f9fa58 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironment.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Elasticsearch environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ElasticsearchEnvironment { + + private String password; + + ElasticsearchEnvironment(Map env) { + Assert.state(!env.containsKey("ELASTIC_PASSWORD_FILE"), "ELASTIC_PASSWORD_FILE is not supported"); + this.password = env.get("ELASTIC_PASSWORD"); + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/package-info.java new file mode 100644 index 00000000000..875262ec426 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose Elasticsearch service connections. + */ +package org.springframework.boot.docker.compose.service.connection.elasticsearch; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java new file mode 100644 index 00000000000..3103dee935a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.jdbc; + +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Utility used to build a JDBC URL for a {@link RunningService}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class JdbcUrlBuilder { + + private static final String PARAMETERS_LABEL = "org.springframework.boot.jdbc.parameters"; + + private final String driverProtocol; + + private final int containerPort; + + /** + * Create a new {@link JdbcUrlBuilder} instance. + * @param driverProtocol the driver protocol + * @param containerPort the source container port + */ + public JdbcUrlBuilder(String driverProtocol, int containerPort) { + Assert.notNull(driverProtocol, "DriverProtocol must not be null"); + this.driverProtocol = driverProtocol; + this.containerPort = containerPort; + } + + /** + * Build a JDBC URL for the given {@link RunningService} and database. + * @param service the running service + * @param database the database to connect to + * @return a new JDBC URL + */ + public String build(RunningService service, String database) { + Assert.notNull(service, "Service must not be null"); + Assert.notNull(database, "Database must not be null"); + String parameters = getParameters(service); + return "jdbc:%s://%s:%d/%s%s".formatted(this.driverProtocol, service.host(), + service.ports().get(this.containerPort), database, parameters); + } + + private String getParameters(RunningService service) { + String parameters = service.labels().get(PARAMETERS_LABEL); + return (StringUtils.hasLength(parameters)) ? "?" + parameters : ""; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/package-info.java new file mode 100644 index 00000000000..c79a154e5e2 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utilities to help when creating + * {@link org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails}. + */ +package org.springframework.boot.docker.compose.service.connection.jdbc; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java new file mode 100644 index 00000000000..388d77d9528 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * MariaDB environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbEnvironment { + + private final String username; + + private final String password; + + private final String database; + + MariaDbEnvironment(Map env) { + this.username = extractUsername(env); + this.password = extractPassword(env); + this.database = extractDatabase(env); + } + + private String extractUsername(Map env) { + String user = env.get("MARIADB_USER"); + return (user != null) ? user : env.getOrDefault("MYSQL_USER", "root"); + } + + private String extractPassword(Map env) { + Assert.state(!env.containsKey("MARIADB_RANDOM_ROOT_PASSWORD"), "MARIADB_RANDOM_ROOT_PASSWORD is not supported"); + Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported"); + Assert.state(!env.containsKey("MARIADB_ROOT_PASSWORD_HASH"), "MARIADB_ROOT_PASSWORD_HASH is not supported"); + boolean allowEmpty = env.containsKey("MARIADB_ALLOW_EMPTY_PASSWORD") + || env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD"); + String password = env.get("MARIADB_PASSWORD"); + password = (password != null) ? password : env.get("MYSQL_PASSWORD"); + password = (password != null) ? password : env.get("MARIADB_ROOT_PASSWORD"); + password = (password != null) ? password : env.get("MYSQL_ROOT_PASSWORD"); + Assert.state(StringUtils.hasLength(password) || allowEmpty, "No MariaDB password found"); + return (password != null) ? password : ""; + } + + private String extractDatabase(Map env) { + String database = env.get("MARIADB_DATABASE"); + database = (database != null) ? database : env.get("MYSQL_DATABASE"); + Assert.state(database != null, "No MARIADB_DATABASE defined"); + return database; + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getDatabase() { + return this.database; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..f5c2e85a551 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for a {@code mariadb} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbJdbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + protected MariaDbJdbcDockerComposeConnectionDetailsFactory() { + super("mariadb"); + } + + @Override + protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MariaDbJdbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link JdbcConnectionDetails} backed by a {@code mariadb} {@link RunningService}. + */ + static class MariaDbJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements JdbcConnectionDetails { + + private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("mariadb", 3306); + + private final MariaDbEnvironment environment; + + private final String jdbcUrl; + + MariaDbJdbcDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new MariaDbEnvironment(service.env()); + this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase()); + } + + @Override + public String getUsername() { + return this.environment.getUsername(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.jdbcUrl; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..06b2f1a7424 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for a {@code mariadb} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbR2dbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + MariaDbR2dbcDockerComposeConnectionDetailsFactory() { + super("mariadb", "io.r2dbc.spi.ConnectionFactoryOptions"); + } + + @Override + protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MariaDbR2dbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link R2dbcConnectionDetails} backed by a {@code mariadb} {@link RunningService}. + */ + static class MariaDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements R2dbcConnectionDetails { + + private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder( + "mariadb", 3306); + + private final ConnectionFactoryOptions connectionFactoryOptions; + + MariaDbR2dbcDockerComposeConnectionDetails(RunningService service) { + super(service); + MariaDbEnvironment environment = new MariaDbEnvironment(service.env()); + this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), + environment.getUsername(), environment.getPassword()); + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return this.connectionFactoryOptions; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/package-info.java new file mode 100644 index 00000000000..7d847e216d0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose MariaDB service connections. + */ +package org.springframework.boot.docker.compose.service.connection.mariadb; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..cc606ce29df --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mongo; + +import com.mongodb.ConnectionString; + +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link MongoConnectionDetails} + * for a {@code mongo} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + + private static final int MONGODB_PORT = 27017; + + protected MongoDockerComposeConnectionDetailsFactory() { + super("mongo", "com.mongodb.ConnectionString"); + } + + @Override + protected MongoDockerComposeConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + return new MongoDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link ElasticsearchConnectionDetails} backed by a {@code mariadb} + * {@link RunningService}. + */ + static class MongoDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements MongoConnectionDetails { + + private final ConnectionString connectionString; + + MongoDockerComposeConnectionDetails(RunningService service) { + super(service); + this.connectionString = buildConnectionString(service); + + } + + private ConnectionString buildConnectionString(RunningService service) { + MongoEnvironment environment = new MongoEnvironment(service.env()); + StringBuilder builder = new StringBuilder("mongodb://"); + if (environment.getUsername() != null) { + builder.append(environment.getUsername()); + builder.append(":"); + builder.append((environment.getPassword() != null) ? environment.getPassword() : ""); + builder.append("@"); + } + builder.append(service.host()); + builder.append(":"); + builder.append(service.ports().get(MONGODB_PORT)); + builder.append("/"); + builder.append((environment.getDatabase() != null) ? environment.getDatabase() : "test"); + return new ConnectionString(builder.toString()); + } + + @Override + public ConnectionString getConnectionString() { + return this.connectionString; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java new file mode 100644 index 00000000000..9b881fdab6c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mongo; + +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * MongoDB environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MongoEnvironment { + + private final String username; + + private final String password; + + private final String database; + + MongoEnvironment(Map env) { + Assert.state(!env.containsKey("MONGO_INITDB_ROOT_USERNAME_FILE"), + "MONGO_INITDB_ROOT_USERNAME_FILE is not supported"); + Assert.state(!env.containsKey("MONGO_INITDB_ROOT_PASSWORD_FILE"), + "MONGO_INITDB_ROOT_PASSWORD_FILE is not supported"); + this.username = env.get("MONGO_INITDB_ROOT_USERNAME"); + this.password = env.get("MONGO_INITDB_ROOT_PASSWORD"); + this.database = env.get("MONGO_INITDB_DATABASE"); + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getDatabase() { + return this.database; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/package-info.java new file mode 100644 index 00000000000..f912ab7f052 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose MongoDB service connections. + */ +package org.springframework.boot.docker.compose.service.connection.mongo; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java new file mode 100644 index 00000000000..ceb8afe5aa0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * MySQL environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlEnvironment { + + private final String username; + + private final String password; + + private final String database; + + MySqlEnvironment(Map env) { + this.username = env.getOrDefault("MYSQL_USER", "root"); + this.password = extractPassword(env); + this.database = extractDatabase(env); + } + + private String extractPassword(Map env) { + Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported"); + boolean allowEmpty = env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD"); + String password = env.get("MYSQL_PASSWORD"); + password = (password != null) ? password : env.get("MYSQL_ROOT_PASSWORD"); + Assert.state(StringUtils.hasLength(password) || allowEmpty, "No MySQL password found"); + return (password != null) ? password : ""; + } + + private String extractDatabase(Map env) { + String database = env.get("MYSQL_DATABASE"); + Assert.state(database != null, "No MYSQL_DATABASE defined"); + return database; + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getDatabase() { + return this.database; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..5977911879d --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for a {@code mysql} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlJdbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + protected MySqlJdbcDockerComposeConnectionDetailsFactory() { + super("mysql"); + } + + @Override + protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MySqlJdbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link JdbcConnectionDetails} backed by a {@code mysql} {@link RunningService}. + */ + static class MySqlJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements JdbcConnectionDetails { + + private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("mysql", 3306); + + private final MySqlEnvironment environment; + + private final String jdbcUrl; + + MySqlJdbcDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new MySqlEnvironment(service.env()); + this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase()); + } + + @Override + public String getUsername() { + return this.environment.getUsername(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.jdbcUrl; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..6869007a6e8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for a {@code mysql} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlR2dbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + MySqlR2dbcDockerComposeConnectionDetailsFactory() { + super("mysql", "io.r2dbc.spi.ConnectionFactoryOptions"); + } + + @Override + protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MySqlR2dbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link R2dbcConnectionDetails} backed by a {@code mysql} {@link RunningService}. + */ + static class MySqlR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements R2dbcConnectionDetails { + + private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder( + "mysql", 3306); + + private final ConnectionFactoryOptions connectionFactoryOptions; + + MySqlR2dbcDockerComposeConnectionDetails(RunningService service) { + super(service); + MySqlEnvironment environment = new MySqlEnvironment(service.env()); + this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), + environment.getUsername(), environment.getPassword()); + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return this.connectionFactoryOptions; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/package-info.java new file mode 100644 index 00000000000..aa5ffe1ca9a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose MySQL service connections. + */ +package org.springframework.boot.docker.compose.service.connection.mysql; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/package-info.java new file mode 100644 index 00000000000..77e88d5a8e5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Service connection support for Docker Compose. + */ +package org.springframework.boot.docker.compose.service.connection; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java new file mode 100644 index 00000000000..435a936e51c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Postgres environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PostgresEnvironment { + + private final String username; + + private final String password; + + private final String database; + + PostgresEnvironment(Map env) { + this.username = env.getOrDefault("POSTGRES_USER", "postgres"); + this.password = extractPassword(env); + this.database = env.getOrDefault("POSTGRES_DB", this.username); + } + + private String extractPassword(Map env) { + String password = env.get("POSTGRES_PASSWORD"); + Assert.state(StringUtils.hasLength(password), "No POSTGRES_PASSWORD defined"); + return password; + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getDatabase() { + return this.database; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..2330a3dc81c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for a {@code postgres} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PostgresJdbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + protected PostgresJdbcDockerComposeConnectionDetailsFactory() { + super("postgres"); + } + + @Override + protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new PostgresJdbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link JdbcConnectionDetails} backed by a {@code postgres} {@link RunningService}. + */ + static class PostgresJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements JdbcConnectionDetails { + + private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("postgresql", 5432); + + private final PostgresEnvironment environment; + + private final String jdbcUrl; + + PostgresJdbcDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new PostgresEnvironment(service.env()); + this.jdbcUrl = jdbcUrlBuilder.build(service, this.environment.getDatabase()); + } + + @Override + public String getUsername() { + return this.environment.getUsername(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.jdbcUrl; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..835bd1fe63f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for a {@code postgres} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PostgresR2dbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + PostgresR2dbcDockerComposeConnectionDetailsFactory() { + super("postgres", "io.r2dbc.spi.ConnectionFactoryOptions"); + } + + @Override + protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new PostgresDbR2dbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link R2dbcConnectionDetails} backed by a {@code postgres} {@link RunningService}. + */ + static class PostgresDbR2dbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements R2dbcConnectionDetails { + + private static final ConnectionFactoryOptionsBuilder connectionFactoryOptionsBuilder = new ConnectionFactoryOptionsBuilder( + "postgresql", 5432); + + private final ConnectionFactoryOptions connectionFactoryOptions; + + PostgresDbR2dbcDockerComposeConnectionDetails(RunningService service) { + super(service); + PostgresEnvironment environment = new PostgresEnvironment(service.env()); + this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), + environment.getUsername(), environment.getPassword()); + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return this.connectionFactoryOptions; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/package-info.java new file mode 100644 index 00000000000..e7771e245a8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose Postgres service connections. + */ +package org.springframework.boot.docker.compose.service.connection.postgres; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilder.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilder.java new file mode 100644 index 00000000000..a4baf48fe1e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilder.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.r2dbc; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; + +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Utility used to build an R2DBC {@link ConnectionFactoryOptions} for a + * {@link RunningService}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class ConnectionFactoryOptionsBuilder { + + private static final String PARAMETERS_LABEL = "org.springframework.boot.r2dbc.parameters"; + + private String driver; + + private int sourcePort; + + /** + * Create a new {@link JdbcUrlBuilder} instance. + * @param driver the driver protocol + * @param containerPort the source container port + */ + public ConnectionFactoryOptionsBuilder(String driver, int containerPort) { + Assert.notNull(driver, "Driver must not be null"); + this.driver = driver; + this.sourcePort = containerPort; + } + + public ConnectionFactoryOptions build(RunningService service, String database, String user, String password) { + Assert.notNull(service, "Service must not be null"); + Assert.notNull(database, "Database must not be null"); + ConnectionFactoryOptions.Builder builder = ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, this.driver) + .option(ConnectionFactoryOptions.HOST, service.host()) + .option(ConnectionFactoryOptions.PORT, service.ports().get(this.sourcePort)) + .option(ConnectionFactoryOptions.DATABASE, database); + if (StringUtils.hasLength(user)) { + builder.option(ConnectionFactoryOptions.USER, user); + } + if (StringUtils.hasLength(password)) { + builder.option(ConnectionFactoryOptions.PASSWORD, password); + } + applyParameters(service, builder); + return builder.build(); + } + + private void applyParameters(RunningService service, ConnectionFactoryOptions.Builder builder) { + String parameters = service.labels().get(PARAMETERS_LABEL); + try { + if (StringUtils.hasText(parameters)) { + parseParameters(parameters).forEach((name, value) -> builder.option(Option.valueOf(name), value)); + } + } + catch (RuntimeException ex) { + throw new IllegalStateException( + "Unable to apply R2DBC label parameters '%s' defined on service %s".formatted(parameters, service)); + } + } + + private Map parseParameters(String parameters) { + Map result = new LinkedHashMap<>(); + for (String parameter : StringUtils.commaDelimitedListToStringArray(parameters)) { + String[] parts = parameter.split("="); + Assert.state(parts.length == 2, () -> "Unable to parse parameter '%s'".formatted(parameter)); + result.put(parts[0], parts[1]); + } + return Collections.unmodifiableMap(result); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/package-info.java new file mode 100644 index 00000000000..01507bff962 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/r2dbc/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utilities to help when creating + * {@link org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails}. + */ +package org.springframework.boot.docker.compose.service.connection.r2dbc; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..7bc79b1d129 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.rabbit; + +import java.util.List; + +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link RabbitConnectionDetails} + * for a {@code rabbitmq} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class RabbitDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + protected RabbitDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected RabbitConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new RabbitDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link RabbitConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class RabbitDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements RabbitConnectionDetails { + + private final RabbitEnvironment environment; + + private final List
addresses; + + protected RabbitDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitEnvironment(service.env()); + this.addresses = List.of(new Address(service.host(), service.ports().get(RABBITMQ_PORT))); + } + + @Override + public String getUsername() { + return this.environment.getUsername(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getVirtualHost() { + return "/"; + } + + @Override + public List
getAddresses() { + return this.addresses; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java new file mode 100644 index 00000000000..4cc471149f4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.rabbit; + +import java.util.Map; + +/** + * RabbitMQ environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class RabbitEnvironment { + + private final String username; + + private final String password; + + RabbitEnvironment(Map env) { + this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", "guest"); + this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", "guest"); + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/package-info.java new file mode 100644 index 00000000000..7f8975636a3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose RabbitMQ service connections. + */ +package org.springframework.boot.docker.compose.service.connection.rabbit; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..9503bde010a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.redis; + +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link RedisConnectionDetails} + * for a {@code redis} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class RedisDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + + private static final int REDIS_PORT = 6379; + + RedisDockerComposeConnectionDetailsFactory() { + super("redis"); + } + + @Override + protected RedisConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new RedisDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link RedisConnectionDetails} backed by a {@code redis} {@link RunningService}. + */ + static class RedisDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements RedisConnectionDetails { + + private final Standalone standalone; + + RedisDockerComposeConnectionDetails(RunningService service) { + super(service); + this.standalone = Standalone.of(service.host(), service.ports().get(REDIS_PORT)); + } + + @Override + public Standalone getStandalone() { + return this.standalone; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/package-info.java new file mode 100644 index 00000000000..a59d81ce62b --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose Redis service connections. + */ +package org.springframework.boot.docker.compose.service.connection.redis; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..f5db39d2fc1 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.zipkin; + +import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link ZipkinConnectionDetails} + * for a {@code zipkin} service. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ZipkinDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int ZIPKIN_PORT = 9411; + + ZipkinDockerComposeConnectionDetailsFactory() { + super("zipkin", "org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration"); + } + + @Override + protected ZipkinConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new ZipkinDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link ZipkinConnectionDetails} backed by a {@code zipkin} {@link RunningService}. + */ + static class ZipkinDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements ZipkinConnectionDetails { + + private final String host; + + private final int port; + + ZipkinDockerComposeConnectionDetails(RunningService source) { + super(source); + this.host = source.host(); + this.port = source.ports().get(ZIPKIN_PORT); + } + + @Override + public String getSpanEndpoint() { + return "http://" + this.host + ":" + this.port + "/api/v2/spans"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/package-info.java new file mode 100644 index 00000000000..b9bf53cef3c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/zipkin/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose Zipkin service connections. + */ +package org.springframework.boot.docker.compose.service.connection.zipkin; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..686a604b9ab --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -0,0 +1,18 @@ +# Application Listeners +org.springframework.context.ApplicationListener=\ +org.springframework.boot.docker.compose.lifecycle.DockerComposeListener,\ +org.springframework.boot.docker.compose.service.connection.DockerComposeServiceConnectionsApplicationListener + +# Connection Detail Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.mongo.MongoDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultConnectionPortsTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultConnectionPortsTests.java new file mode 100644 index 00000000000..91abcb0c769 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultConnectionPortsTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DefaultConnectionPorts.ContainerPort; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link DefaultConnectionPorts}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultConnectionPortsTests { + + @Test + void createWhenBridgeNetwork() throws IOException { + DefaultConnectionPorts ports = createForJson("docker-inspect-bridge-network.json"); + assertThat(ports.getMappings()).containsExactly(entry(new ContainerPort(6379, "tcp"), 32770)); + } + + @Test + void createWhenHostNetwork() throws Exception { + DefaultConnectionPorts ports = createForJson("docker-inspect-host-network.json"); + assertThat(ports.getMappings()).containsExactly(entry(new ContainerPort(6379, "tcp"), 6379)); + } + + private DefaultConnectionPorts createForJson(String path) throws IOException { + String json = new ClassPathResource(path, getClass()).getContentAsString(StandardCharsets.UTF_8); + DockerCliInspectResponse inspectResponse = DockerJson.deserialize(json, DockerCliInspectResponse.class); + return new DefaultConnectionPorts(inspectResponse); + } + + @Nested + class ContainerPortTests { + + @Test + void parse() { + ContainerPort port = ContainerPort.parse("123/tcp"); + assertThat(port).isEqualTo(new ContainerPort(123, "tcp")); + } + + @Test + void parseWhenNoSlashThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("123")) + .withMessage("Unable to parse container port '123'"); + } + + @Test + void parseWhenMultipleSlashesThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("123/tcp/ip")) + .withMessage("Unable to parse container port '123/tcp/ip'"); + } + + @Test + void parseWhenNotNumberThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> ContainerPort.parse("tcp/123")) + .withMessage("Unable to parse container port 'tcp/123'"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java new file mode 100644 index 00000000000..1bdd871063b --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultDockerComposeTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultDockerCompose}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultDockerComposeTests { + + private static final String HOST = "192.168.1.1"; + + private DockerCli cli = mock(DockerCli.class); + + @Test + void upRunsUpCommand() { + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + compose.up(); + then(this.cli).should().run(new DockerCliCommand.ComposeUp()); + } + + @Test + void downRunsDownCommand() { + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + Duration timeout = Duration.ofSeconds(1); + compose.down(timeout); + then(this.cli).should().run(new DockerCliCommand.ComposeDown(timeout)); + } + + @Test + void startRunsStartCommand() { + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + compose.start(); + then(this.cli).should().run(new DockerCliCommand.ComposeStart()); + } + + @Test + void stopRunsStopCommand() { + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + Duration timeout = Duration.ofSeconds(1); + compose.stop(timeout); + then(this.cli).should().run(new DockerCliCommand.ComposeStop(timeout)); + } + + @Test + void hasDefinedServicesWhenComposeConfigServicesIsEmptyReturnsFalse() { + willReturn(new DockerCliComposeConfigResponse("test", Collections.emptyMap())).given(this.cli) + .run(new DockerCliCommand.ComposeConfig()); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + assertThat(compose.hasDefinedServices()).isFalse(); + } + + @Test + void hasDefinedServicesWhenComposeConfigServicesIsNotEmptyReturnsTrue() { + willReturn(new DockerCliComposeConfigResponse("test", + Map.of("redis", new DockerCliComposeConfigResponse.Service("redis")))) + .given(this.cli) + .run(new DockerCliCommand.ComposeConfig()); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + assertThat(compose.hasDefinedServices()).isTrue(); + } + + @Test + void hasRunningServicesWhenPsListsRunningServiceReturnsTrue() { + willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"), + new DockerCliComposePsResponse("id", "name", "image", "running"))) + .given(this.cli) + .run(new DockerCliCommand.ComposePs()); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + assertThat(compose.hasRunningServices()).isTrue(); + } + + @Test + void hasRunningServicesWhenPsListReturnsAllExitedReturnsFalse() { + willReturn(List.of(new DockerCliComposePsResponse("id", "name", "image", "exited"), + new DockerCliComposePsResponse("id", "name", "image", "running"))) + .given(this.cli) + .run(new DockerCliCommand.ComposePs()); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + assertThat(compose.hasRunningServices()).isTrue(); + } + + @Test + void getRunningServicesReturnsServices() { + String id = "123"; + DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, "name", "redis", "running"); + Map exposedPorts = Collections.emptyMap(); + Config config = new Config("redis", Map.of("spring", "boot"), exposedPorts, List.of("a=b")); + NetworkSettings networkSettings = null; + HostConfig hostConfig = null; + DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings, + hostConfig); + willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs()); + willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id))); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, HOST); + List runningServices = compose.getRunningServices(); + assertThat(runningServices).hasSize(1); + RunningService runningService = runningServices.get(0); + assertThat(runningService.name()).isEqualTo("name"); + assertThat(runningService.image()).hasToString("redis"); + assertThat(runningService.host()).isEqualTo(HOST); + assertThat(runningService.ports().getAll()).isEmpty(); + assertThat(runningService.env()).containsExactly(entry("a", "b")); + assertThat(runningService.labels()).containsExactly(entry("spring", "boot")); + } + + @Test + void getRunningServicesWhenNoHostUsesHostFromContext() { + String id = "123"; + DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, "name", "redis", "running"); + Map exposedPorts = Collections.emptyMap(); + Config config = new Config("redis", Map.of("spring", "boot"), exposedPorts, List.of("a=b")); + NetworkSettings networkSettings = null; + HostConfig hostConfig = null; + DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings, + hostConfig); + willReturn(List.of(new DockerCliContextResponse("test", true, "https://192.168.1.1"))).given(this.cli) + .run(new DockerCliCommand.Context()); + willReturn(List.of(psResponse)).given(this.cli).run(new DockerCliCommand.ComposePs()); + willReturn(List.of(inspectResponse)).given(this.cli).run(new DockerCliCommand.Inspect(List.of(id))); + DefaultDockerCompose compose = new DefaultDockerCompose(this.cli, null); + List runningServices = compose.getRunningServices(); + assertThat(runningServices).hasSize(1); + RunningService runningService = runningServices.get(0); + assertThat(runningService.host()).isEqualTo("192.168.1.1"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultRunningServiceTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultRunningServiceTests.java new file mode 100644 index 00000000000..f91bc9f8b63 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DefaultRunningServiceTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings; +import org.springframework.boot.origin.Origin; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link DefaultRunningService}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultRunningServiceTests { + + @TempDir + File temp; + + private DefaultRunningService runningService; + + private DockerComposeFile composeFile; + + @BeforeEach + void setup() throws Exception { + this.composeFile = createComposeFile(); + DockerHost host = DockerHost.get("192.168.1.1", () -> Collections.emptyList()); + String id = "123"; + String name = "my-service"; + String image = "redis"; + String state = "running"; + DockerCliComposePsResponse psResponse = new DockerCliComposePsResponse(id, name, image, state); + Map labels = Map.of("spring", "boot"); + Map exposedPorts = Map.of("8080/tcp", new ExposedPort()); + List env = List.of("a=b"); + Config config = new Config(image, labels, exposedPorts, env); + Map> ports = Map.of("8080/tcp", List.of(new HostPort(null, "9090"))); + NetworkSettings networkSettings = new NetworkSettings(ports); + HostConfig hostConfig = new HostConfig("bridge"); + DockerCliInspectResponse inspectResponse = new DockerCliInspectResponse(id, config, networkSettings, + hostConfig); + this.runningService = new DefaultRunningService(host, this.composeFile, psResponse, inspectResponse); + } + + private DockerComposeFile createComposeFile() throws IOException { + File file = new File(this.temp, "compose.yaml"); + FileCopyUtils.copy(new byte[0], file); + return DockerComposeFile.of(file); + } + + @Test + void getOriginReturnsOrigin() { + assertThat(Origin.from(this.runningService)).isEqualTo(new DockerComposeOrigin(this.composeFile, "my-service")); + } + + @Test + void nameReturnsNameFromPsResponse() { + assertThat(this.runningService.name()).isEqualTo("my-service"); + } + + @Test + void imageReturnsImageFromPsResponse() { + assertThat(this.runningService.image()).hasToString("redis"); + } + + @Test + void hostReturnsHost() { + assertThat(this.runningService.host()).isEqualTo("192.168.1.1"); + } + + @Test + void portsReturnsPortsFromInspectResponse() { + ConnectionPorts ports = this.runningService.ports(); + assertThat(ports.getAll("tcp")).containsExactly(9090); + assertThat(ports.get(8080)).isEqualTo(9090); + } + + @Test + void envReturnsEnvFromInspectResponse() { + assertThat(this.runningService.env()).containsExactly(entry("a", "b")); + } + + @Test + void labelReturnsLabelsFromInspectResponse() { + assertThat(this.runningService.labels()).containsExactly(entry("spring", "boot")); + } + + @Test + void toStringReturnsServiceName() { + assertThat(this.runningService).hasToString("my-service"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java new file mode 100644 index 00000000000..c9a14745007 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliCommand}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliCommandTests { + + @Test + void context() { + DockerCliCommand command = new DockerCliCommand.Context(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); + assertThat(command.getCommand()).containsExactly("context", "ls", "--format={{ json . }}"); + assertThat(command.deserialize("[]")).isInstanceOf(List.class); + } + + @Test + void inspect() { + DockerCliCommand command = new DockerCliCommand.Inspect(List.of("123", "345")); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); + assertThat(command.getCommand()).containsExactly("inspect", "--format={{ json . }}", "123", "345"); + assertThat(command.deserialize("[]")).isInstanceOf(List.class); + } + + @Test + void composeConfig() { + DockerCliCommand command = new DockerCliCommand.ComposeConfig(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("config", "--format=json"); + assertThat(command.deserialize("{}")).isInstanceOf(DockerCliComposeConfigResponse.class); + } + + @Test + void composePs() { + DockerCliCommand command = new DockerCliCommand.ComposePs(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("ps", "--format=json"); + assertThat(command.deserialize("[]")).isInstanceOf(List.class); + } + + @Test + void composeUp() { + DockerCliCommand command = new DockerCliCommand.ComposeUp(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("up", "--no-color", "--quiet-pull", "--detach", "--wait"); + assertThat(command.deserialize("[]")).isNull(); + } + + @Test + void composeDown() { + DockerCliCommand command = new DockerCliCommand.ComposeDown(Duration.ofSeconds(1)); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("down", "--timeout", "1"); + assertThat(command.deserialize("[]")).isNull(); + } + + @Test + void composeStart() { + DockerCliCommand command = new DockerCliCommand.ComposeStart(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("start", "--no-color", "--quiet-pull", "--detach", "--wait"); + assertThat(command.deserialize("[]")).isNull(); + } + + @Test + void composeStop() { + DockerCliCommand command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1)); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand()).containsExactly("stop", "--timeout", "1"); + assertThat(command.deserialize("[]")).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponseTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponseTests.java new file mode 100644 index 00000000000..146b764ca3f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeConfigResponseTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DockerCliComposeConfigResponse.Service; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliComposeConfigResponse}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliComposeConfigResponseTests { + + @Test + void deserializeJson() throws IOException { + String json = new ClassPathResource("docker-compose-config.json", getClass()) + .getContentAsString(StandardCharsets.UTF_8); + DockerCliComposeConfigResponse response = DockerJson.deserialize(json, DockerCliComposeConfigResponse.class); + DockerCliComposeConfigResponse expected = new DockerCliComposeConfigResponse("redis-docker", + Map.of("redis", new Service("redis:7.0"))); + assertThat(response).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponseTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponseTests.java new file mode 100644 index 00000000000..b67cc0067a6 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposePsResponseTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliComposePsResponse}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliComposePsResponseTests { + + @Test + void deserializeJson() throws IOException { + String json = new ClassPathResource("docker-compose-ps.json", getClass()) + .getContentAsString(StandardCharsets.UTF_8); + DockerCliComposePsResponse response = DockerJson.deserialize(json, DockerCliComposePsResponse.class); + DockerCliComposePsResponse expected = new DockerCliComposePsResponse("f5af31dae7f6", "redis-docker-redis-1", + "redis:7.0", "running"); + assertThat(response).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponseTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponseTests.java new file mode 100644 index 00000000000..8874daaa433 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliComposeVersionResponseTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliComposeVersionResponse}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliComposeVersionResponseTests { + + @Test + void deserializeJson() throws IOException { + String json = new ClassPathResource("docker-compose-version.json", getClass()) + .getContentAsString(StandardCharsets.UTF_8); + DockerCliComposeVersionResponse response = DockerJson.deserialize(json, DockerCliComposeVersionResponse.class); + DockerCliComposeVersionResponse expected = new DockerCliComposeVersionResponse("123"); + assertThat(response).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliContextResponseTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliContextResponseTests.java new file mode 100644 index 00000000000..5fa8d49b267 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliContextResponseTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliContextResponse}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliContextResponseTests { + + @Test + void deserializeJson() throws IOException { + String json = new ClassPathResource("docker-context.json", getClass()) + .getContentAsString(StandardCharsets.UTF_8); + DockerCliContextResponse response = DockerJson.deserialize(json, DockerCliContextResponse.class); + DockerCliContextResponse expected = new DockerCliContextResponse("default", true, + "unix:///var/run/docker.sock"); + assertThat(response).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponseTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponseTests.java new file mode 100644 index 00000000000..d90f979aab4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliInspectResponseTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.ExposedPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort; +import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.NetworkSettings; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCliInspectResponse}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerCliInspectResponseTests { + + @Test + void deserializeJson() throws IOException { + String json = new ClassPathResource("docker-inspect.json", getClass()) + .getContentAsString(StandardCharsets.UTF_8); + DockerCliInspectResponse response = DockerJson.deserialize(json, DockerCliInspectResponse.class); + LinkedHashMap expectedLabels = linkedMapOf("com.docker.compose.config-hash", + "cfdc8e119d85a53c7d47edb37a3b160a8c83ba48b0428ebc07713befec991dd0", + "com.docker.compose.container-number", "1", "com.docker.compose.depends_on", "", + "com.docker.compose.image", "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "com.docker.compose.oneoff", "False", "com.docker.compose.project", "redis-docker", + "com.docker.compose.project.config_files", "compose.yaml", "com.docker.compose.project.working_dir", + "/", "com.docker.compose.service", "redis", "com.docker.compose.version", "2.16.0"); + List expectedEnv = List.of("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "GOSU_VERSION=1.16", "REDIS_VERSION=7.0.8"); + Config expectedConfig = new Config("redis:7.0", expectedLabels, Map.of("6379/tcp", new ExposedPort()), + expectedEnv); + NetworkSettings expectedNetworkSettings = new NetworkSettings( + Map.of("6379/tcp", List.of(new HostPort("0.0.0.0", "32770"), new HostPort("::", "32770")))); + DockerCliInspectResponse expected = new DockerCliInspectResponse( + "f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc", expectedConfig, + expectedNetworkSettings, new HostConfig("redis-docker_default")); + assertThat(response).isEqualTo(expected); + } + + @SuppressWarnings("unchecked") + private LinkedHashMap linkedMapOf(Object... values) { + LinkedHashMap result = new LinkedHashMap<>(); + for (int i = 0; i < values.length; i = i + 2) { + result.put((K) values[i], (V) values[i + 1]); + } + return result; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliTests.java new file mode 100644 index 00000000000..1c163fe3dcf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerCli}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@DisabledIfProcessUnavailable({ "docker", "compose" }) +class DockerCliTests { + + @Test + void runBasicCommand() { + DockerCli cli = new DockerCli(null, null, Collections.emptySet()); + List context = cli.run(new DockerCliCommand.Context()); + assertThat(context).isNotEmpty(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java new file mode 100644 index 00000000000..918ac819ebf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerComposeFile}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeFileTests { + + @TempDir + File temp; + + @Test + void hashCodeAndEquals() throws Exception { + File f1 = new File(this.temp, "compose.yml"); + File f2 = new File(this.temp, "docker-compose.yml"); + FileCopyUtils.copy(new byte[0], f1); + FileCopyUtils.copy(new byte[0], f2); + DockerComposeFile c1 = DockerComposeFile.of(f1); + DockerComposeFile c2 = DockerComposeFile.of(f1); + DockerComposeFile c3 = DockerComposeFile.find(f1.getParentFile()); + DockerComposeFile c4 = DockerComposeFile.of(f2); + assertThat(c1.hashCode()).isEqualTo(c2.hashCode()).isEqualTo(c3.hashCode()); + assertThat(c1).isEqualTo(c1).isEqualTo(c2).isEqualTo(c3).isNotEqualTo(c4); + } + + @Test + void toStringReturnsFileName() throws Exception { + DockerComposeFile composeFile = createComposeFile("compose.yml"); + assertThat(composeFile.toString()).endsWith("/compose.yml"); + } + + @Test + void findFindsSingleFile() throws Exception { + File file = new File(this.temp, "docker-compose.yml"); + FileCopyUtils.copy(new byte[0], file); + DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile()); + assertThat(composeFile.toString()).endsWith("/docker-compose.yml"); + } + + @Test + void findWhenMultipleFilesPicksBest() throws Exception { + File f1 = new File(this.temp, "docker-compose.yml"); + FileCopyUtils.copy(new byte[0], f1); + File f2 = new File(this.temp, "compose.yml"); + FileCopyUtils.copy(new byte[0], f2); + DockerComposeFile composeFile = DockerComposeFile.find(f1.getParentFile()); + assertThat(composeFile.toString()).endsWith("/compose.yml"); + } + + @Test + void findWhenNoComposeFilesReturnsNull() throws Exception { + File file = new File(this.temp, "not-a-compose.yml"); + FileCopyUtils.copy(new byte[0], file); + DockerComposeFile composeFile = DockerComposeFile.find(file.getParentFile()); + assertThat(composeFile).isNull(); + } + + @Test + void findWhenWorkingDirectoryDoesNotExistReturnsNull() { + File directory = new File(this.temp, "missing"); + DockerComposeFile composeFile = DockerComposeFile.find(directory); + assertThat(composeFile).isNull(); + } + + @Test + void findWhenWorkingDirectoryIsNotDirectoryThrowsException() throws Exception { + File file = new File(this.temp, "iamafile"); + FileCopyUtils.copy(new byte[0], file); + assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.find(file)) + .withMessageEndingWith("is not a directory"); + } + + @Test + void ofReturnsDockerComposeFile() throws Exception { + File file = new File(this.temp, "anyfile.yml"); + FileCopyUtils.copy(new byte[0], file); + DockerComposeFile composeFile = DockerComposeFile.of(file); + assertThat(composeFile).isNotNull(); + assertThat(composeFile.toString()).isEqualTo(file.getCanonicalPath()); + } + + @Test + void ofWhenFileIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(null)) + .withMessage("File must not be null"); + } + + @Test + void ofWhenFileDoesNotExistThrowsException() { + File file = new File(this.temp, "missing"); + assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(file)) + .withMessageEndingWith("does not exist"); + } + + @Test + void ofWhenFileIsNotFileThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> DockerComposeFile.of(this.temp)) + .withMessageEndingWith("is not a file"); + } + + private DockerComposeFile createComposeFile(String name) throws IOException { + File file = new File(this.temp, name); + FileCopyUtils.copy(new byte[0], file); + return DockerComposeFile.of(file); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeOriginTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeOriginTests.java new file mode 100644 index 00000000000..7d59606e64c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeOriginTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerComposeOrigin}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeOriginTests { + + @TempDir + File temp; + + @Test + void hasToString() throws Exception { + DockerComposeFile composeFile = createTempComposeFile(); + DockerComposeOrigin origin = new DockerComposeOrigin(composeFile, "service-1"); + assertThat(origin.toString()).startsWith("Docker compose service 'service-1' defined in '") + .endsWith("compose.yaml'"); + } + + @Test + void equalsAndHashcode() throws Exception { + DockerComposeFile composeFile = createTempComposeFile(); + DockerComposeOrigin origin1 = new DockerComposeOrigin(composeFile, "service-1"); + DockerComposeOrigin origin2 = new DockerComposeOrigin(composeFile, "service-1"); + DockerComposeOrigin origin3 = new DockerComposeOrigin(composeFile, "service-3"); + assertThat(origin1).isEqualTo(origin1); + assertThat(origin1).isEqualTo(origin2); + assertThat(origin1).hasSameHashCodeAs(origin2); + assertThat(origin2).isEqualTo(origin1); + assertThat(origin1).isNotEqualTo(origin3); + assertThat(origin2).isNotEqualTo(origin3); + assertThat(origin3).isNotEqualTo(origin1); + assertThat(origin3).isNotEqualTo(origin2); + } + + private DockerComposeFile createTempComposeFile() throws IOException { + File file = new File(this.temp, "compose.yaml"); + FileCopyUtils.copy(new byte[0], file); + return DockerComposeFile.of(file); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerEnvTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerEnvTests.java new file mode 100644 index 00000000000..1640caeed51 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerEnvTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link DockerEnv}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerEnvTests { + + @Test + void createWhenEnvIsNullReturnsEmpty() { + DockerEnv env = new DockerEnv(null); + assertThat(env.asMap()).isEmpty(); + } + + @Test + void createWhenEnvIsEmptyReturnsEmpty() { + DockerEnv env = new DockerEnv(Collections.emptyList()); + assertThat(env.asMap()).isEmpty(); + } + + @Test + void createParsesEnv() { + DockerEnv env = new DockerEnv(List.of("a=b", "c")); + assertThat(env.asMap()).containsExactly(entry("a", "b"), entry("c", null)); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerHostTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerHostTests.java new file mode 100644 index 00000000000..b10feb3d7a3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerHostTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerHost}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerHostTests { + + private static final String MAC_HOST = "unix:///var/run/docker.sock"; + + private static final String LINUX_HOST = "unix:///var/run/docker.sock"; + + private static final String WINDOWS_HOST = "npipe:////./pipe/docker_engine"; + + private static final String WSL_HOST = "unix:///var/run/docker.sock"; + + private static final String HTTP_HOST = "http://192.168.1.1"; + + private static final String HTTPS_HOST = "https://192.168.1.1"; + + private static final String TCP_HOST = "tcp://192.168.1.1"; + + private static final Function NO_SYSTEM_ENV = (key) -> null; + + private static final Supplier> NO_CONTEXT = () -> Collections.emptyList(); + + @Test + void getWhenHasHost() { + DockerHost host = DockerHost.get("192.168.1.1", NO_SYSTEM_ENV, NO_CONTEXT); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasServiceHostEnv() { + Map systemEnv = Map.of("SERVICES_HOST", "192.168.1.2"); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("192.168.1.2"); + } + + @Test + void getWhenHasMacDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", MAC_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasLinuxDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", LINUX_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasWindowsDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", WINDOWS_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasWslDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", WSL_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasHttpDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", HTTP_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasHttpsDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", HTTPS_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasTcpDockerHostEnv() { + Map systemEnv = Map.of("DOCKER_HOST", TCP_HOST); + DockerHost host = DockerHost.get(null, systemEnv::get, NO_CONTEXT); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasMacContext() { + List context = List.of(new DockerCliContextResponse("test", true, MAC_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasLinuxContext() { + List context = List.of(new DockerCliContextResponse("test", true, LINUX_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasWindowsContext() { + List context = List.of(new DockerCliContextResponse("test", true, WINDOWS_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasWslContext() { + List context = List.of(new DockerCliContextResponse("test", true, WSL_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("127.0.0.1"); + } + + @Test + void getWhenHasHttpContext() { + List context = List.of(new DockerCliContextResponse("test", true, HTTP_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasHttpsContext() { + List context = List.of(new DockerCliContextResponse("test", true, HTTPS_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenHasTcpContext() { + List context = List.of(new DockerCliContextResponse("test", true, TCP_HOST)); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("192.168.1.1"); + } + + @Test + void getWhenContextHasMultiple() { + List context = new ArrayList<>(); + context.add(new DockerCliContextResponse("test", false, "http://192.168.1.1")); + context.add(new DockerCliContextResponse("test", true, "http://192.168.1.2")); + context.add(new DockerCliContextResponse("test", false, "http://192.168.1.3")); + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, () -> context); + assertThat(host).hasToString("192.168.1.2"); + } + + @Test + void getWhenHasNone() { + DockerHost host = DockerHost.get(null, NO_SYSTEM_ENV, NO_CONTEXT); + assertThat(host).hasToString("127.0.0.1"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerJsonTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerJsonTests.java new file mode 100644 index 00000000000..0ff2135dc0a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerJsonTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerJson}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerJsonTests { + + @Test + void deserializeWhenSentenceCase() { + String json = """ + { "Value": 1 } + """; + TestResponse response = DockerJson.deserialize(json, TestResponse.class); + assertThat(response).isEqualTo(new TestResponse(1)); + } + + @Test + void deserializeWhenLowerCase() { + String json = """ + { "value": 1 } + """; + TestResponse response = DockerJson.deserialize(json, TestResponse.class); + assertThat(response).isEqualTo(new TestResponse(1)); + } + + @Test + void deserializeToListWhenArray() { + String json = """ + [{ "value": 1 }, { "value": 2 }] + """; + List response = DockerJson.deserializeToList(json, TestResponse.class); + assertThat(response).containsExactly(new TestResponse(1), new TestResponse(2)); + } + + @Test + void deserializeToListWhenMultipleLines() { + String json = """ + { "Value": 1 } + { "Value": 2 } + """; + List response = DockerJson.deserializeToList(json, TestResponse.class); + assertThat(response).containsExactly(new TestResponse(1), new TestResponse(2)); + } + + record TestResponse(int value) { + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ImageReferenceTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ImageReferenceTests.java new file mode 100644 index 00000000000..d8f78ea018b --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ImageReferenceTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ImageReference}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ImageReferenceTests { + + @Test + void getImageNameWhenImageOnly() { + ImageReference imageReference = ImageReference.of("redis"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenImageAndTag() { + ImageReference imageReference = ImageReference.of("redis:5"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenImageAndDigest() { + ImageReference imageReference = ImageReference + .of("redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenProjectAndImage() { + ImageReference imageReference = ImageReference.of("library/redis"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenRegistryLibraryAndImage() { + ImageReference imageReference = ImageReference.of("docker.io/library/redis"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenRegistryLibraryImageAndTag() { + ImageReference imageReference = ImageReference.of("docker.io/library/redis:5"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenRegistryLibraryImageAndDigest() { + ImageReference imageReference = ImageReference + .of("docker.io/library/redis@sha256:0ed5d5928d4737458944eb604cc8509e245c3e19d02ad83935398bc4b991aac7"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenRegistryWithPort() { + ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void getImageNameWhenRegistryWithPortAndTag() { + ImageReference imageReference = ImageReference.of("my_private.registry:5000/redis:5"); + assertThat(imageReference.getImageName()).isEqualTo("redis"); + } + + @Test + void toStringReturnsReferenceString() { + ImageReference imageReference = ImageReference.of("docker.io/library/redis"); + assertThat(imageReference).hasToString("docker.io/library/redis"); + } + + @Test + void equalsAndHashCode() { + ImageReference imageReference1 = ImageReference.of("docker.io/library/redis"); + ImageReference imageReference2 = ImageReference.of("docker.io/library/redis"); + ImageReference imageReference3 = ImageReference.of("docker.io/library/other"); + assertThat(imageReference1.hashCode()).isEqualTo(imageReference2.hashCode()); + assertThat(imageReference1).isEqualTo(imageReference1).isEqualTo(imageReference2).isNotEqualTo(imageReference3); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ProcessRunnerTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ProcessRunnerTests.java new file mode 100644 index 00000000000..3e7906947d0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/ProcessRunnerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.core; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link ProcessRunner}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@DisabledIfProcessUnavailable("docker") +class ProcessRunnerTests { + + private ProcessRunner processRunner = new ProcessRunner(); + + @Test + void run() { + String out = this.processRunner.run("docker", "--version"); + assertThat(out).isNotEmpty(); + } + + @Test + void runWhenProcessDoesNotStart() { + assertThatExceptionOfType(ProcessStartException.class) + .isThrownBy(() -> this.processRunner.run("iverymuchdontexist", "--version")); + } + + @Test + void runWhenProcessReturnsNonZeroExitCode() { + assertThatExceptionOfType(ProcessExitException.class) + .isThrownBy(() -> this.processRunner.run("docker", "-thisdoesntwork")) + .satisfies((ex) -> { + assertThat(ex.getExitCode()).isGreaterThan(0); + assertThat(ex.getStdOut()).isEmpty(); + assertThat(ex.getStdErr()).isNotEmpty(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java new file mode 100644 index 00000000000..e7065ed6f2d --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java @@ -0,0 +1,373 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.SpringApplicationShutdownHandlers; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.docker.compose.core.DockerCompose; +import org.springframework.boot.docker.compose.core.DockerComposeFile; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.readiness.ServiceReadinessChecks; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link DockerComposeLifecycleManager}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeLifecycleManagerTests { + + @TempDir + File temp; + + private DockerComposeFile dockerComposeFile; + + private DockerCompose dockerCompose; + + private Set activeProfiles; + + private GenericApplicationContext applicationContext; + + private TestSpringApplicationShutdownHandlers shutdownHandlers; + + private ServiceReadinessChecks serviceReadinessChecks; + + private List runningServices; + + private DockerComposeProperties properties; + + private LinkedHashSet> eventListeners; + + private DockerComposeLifecycleManager lifecycleManager; + + private DockerComposeSkipCheck skipCheck; + + @BeforeEach + void setup() throws IOException { + File file = new File(this.temp, "compose.yml"); + FileCopyUtils.copy(new byte[0], file); + this.dockerComposeFile = DockerComposeFile.of(file); + this.dockerCompose = mock(DockerCompose.class); + File workingDirectory = new File("."); + this.applicationContext = new GenericApplicationContext(); + this.applicationContext.refresh(); + Binder binder = Binder.get(this.applicationContext.getEnvironment()); + this.shutdownHandlers = new TestSpringApplicationShutdownHandlers(); + this.properties = DockerComposeProperties.get(binder); + this.eventListeners = new LinkedHashSet<>(); + this.skipCheck = mock(DockerComposeSkipCheck.class); + this.serviceReadinessChecks = mock(ServiceReadinessChecks.class); + this.lifecycleManager = new TestDockerComposeLifecycleManager(workingDirectory, this.applicationContext, binder, + this.shutdownHandlers, this.properties, this.eventListeners, this.skipCheck, + this.serviceReadinessChecks); + } + + @Test + void startupWhenEnabledFalseDoesNotStart() { + this.properties.setEnabled(false); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + assertThat(listener.getEvent()).isNull(); + then(this.dockerCompose).should(never()).hasDefinedServices(); + } + + @Test + void startupWhenInTestDoesNotStart() { + given(this.skipCheck.shouldSkip(any(), any(), any())).willReturn(true); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + assertThat(listener.getEvent()).isNull(); + then(this.dockerCompose).should(never()).hasDefinedServices(); + } + + @Test + void startupWhenHasNoDefinedServicesDoesNothing() { + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + this.lifecycleManager.startup(); + assertThat(listener.getEvent()).isNull(); + then(this.dockerCompose).should().hasDefinedServices(); + then(this.dockerCompose).should(never()).up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should(never()).down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + } + + @Test + void startupWhenLifecycleStartAndStopAndHasNoRunningServicesDoesStartupAndShutdown() { + this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should().up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should().down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + } + + @Test + void startupWhenLifecycleStartAndStopAndHasRunningServicesDoesNoStartupOrShutdown() { + this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should(never()).up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should(never()).down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + } + + @Test + void startupWhenLifecycleNoneDoesNoStartupOrShutdown() { + this.properties.setLifecycleManagement(LifecycleManagement.NONE); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should(never()).up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should(never()).down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + } + + @Test + void startupWhenLifecycleStartOnlyDoesStartupAndNoShutdown() { + this.properties.setLifecycleManagement(LifecycleManagement.START_ONLY); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should().up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should(never()).down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + this.shutdownHandlers.assertNoneAdded(); + } + + @Test + void startupWhenStartupCommandStartDoesStartupUsingStartAndShutdown() { + this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP); + this.properties.getStartup().setCommand(StartupCommand.START); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should(never()).up(); + then(this.dockerCompose).should().start(); + then(this.dockerCompose).should().down(isA(Duration.class)); + then(this.dockerCompose).should(never()).stop(isA(Duration.class)); + } + + @Test + void startupWhenShutdownCommandStopDoesStartupAndShutdownUsingStop() { + this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP); + this.properties.getShutdown().setCommand(ShutdownCommand.STOP); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should().up(); + then(this.dockerCompose).should(never()).start(); + then(this.dockerCompose).should(never()).down(isA(Duration.class)); + then(this.dockerCompose).should().stop(isA(Duration.class)); + } + + @Test + void startupWhenHasShutdownTimeoutUsesDuration() { + this.properties.setLifecycleManagement(LifecycleManagement.START_AND_STOP); + Duration timeout = Duration.ofDays(1); + this.properties.getShutdown().setTimeout(timeout); + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + then(this.dockerCompose).should().down(timeout); + } + + @Test + void startupWhenHasIgnoreLabelIgnoresService() { + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(Map.of("org.springframework.boot.ignore", "true")); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + assertThat(listener.getEvent()).isNotNull(); + assertThat(listener.getEvent().getRunningServices()).isEmpty(); + } + + @Test + void startupWaitsUntilReady() { + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + this.shutdownHandlers.run(); + then(this.serviceReadinessChecks).should().waitUntilReady(this.runningServices); + } + + @Test + void startupGetsDockerComposeWithActiveProfiles() { + this.properties.getProfiles().setActive(Set.of("my-profile")); + setupRunningServices(); + this.lifecycleManager.startup(); + assertThat(this.activeProfiles).containsExactly("my-profile"); + } + + @Test + void startupPublishesEvent() { + EventCapturingListener listener = new EventCapturingListener(); + this.eventListeners.add(listener); + setupRunningServices(); + this.lifecycleManager.startup(); + DockerComposeServicesReadyEvent event = listener.getEvent(); + assertThat(event).isNotNull(); + assertThat(event.getSource()).isEqualTo(this.applicationContext); + assertThat(event.getRunningServices()).isEqualTo(this.runningServices); + } + + private void setupRunningServices() { + setupRunningServices(Collections.emptyMap()); + } + + private void setupRunningServices(Map labels) { + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.hasRunningServices()).willReturn(true); + RunningService runningService = mock(RunningService.class); + given(runningService.labels()).willReturn(labels); + this.runningServices = List.of(runningService); + given(this.dockerCompose.getRunningServices()).willReturn(this.runningServices); + } + + /** + * Testable {@link SpringApplicationShutdownHandlers}. + */ + static class TestSpringApplicationShutdownHandlers implements SpringApplicationShutdownHandlers { + + private final List actions = new ArrayList<>(); + + @Override + public void add(Runnable action) { + this.actions.add(action); + } + + @Override + public void remove(Runnable action) { + this.actions.remove(action); + } + + void run() { + this.actions.forEach(Runnable::run); + } + + void assertNoneAdded() { + assertThat(this.actions).isEmpty(); + } + + } + + /** + * {@link ApplicationListener} to capture the {@link DockerComposeServicesReadyEvent}. + */ + static class EventCapturingListener implements ApplicationListener { + + private DockerComposeServicesReadyEvent event; + + @Override + public void onApplicationEvent(DockerComposeServicesReadyEvent event) { + this.event = event; + } + + DockerComposeServicesReadyEvent getEvent() { + return this.event; + } + + } + + /** + * Testable {@link DockerComposeLifecycleManager}. + */ + class TestDockerComposeLifecycleManager extends DockerComposeLifecycleManager { + + TestDockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder, + SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties, + Set> eventListeners, DockerComposeSkipCheck skipCheck, + ServiceReadinessChecks serviceReadinessChecks) { + super(workingDirectory, applicationContext, binder, shutdownHandlers, properties, eventListeners, skipCheck, + serviceReadinessChecks); + } + + @Override + protected DockerComposeFile getComposeFile() { + return DockerComposeLifecycleManagerTests.this.dockerComposeFile; + } + + @Override + protected DockerCompose getDockerCompose(DockerComposeFile composeFile, Set activeProfiles) { + DockerComposeLifecycleManagerTests.this.activeProfiles = activeProfiles; + return DockerComposeLifecycleManagerTests.this.dockerCompose; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListenerTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListenerTests.java new file mode 100644 index 00000000000..3b74d5e14e3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeListenerTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationShutdownHandlers; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DockerComposeListener}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeListenerTests { + + @Test + void onApplicationPreparedEventCreatesAndStartsDockerComposeLifecycleManager() { + SpringApplicationShutdownHandlers shutdownHandlers = mock(SpringApplicationShutdownHandlers.class); + SpringApplication application = mock(SpringApplication.class); + ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); + MockEnvironment environment = new MockEnvironment(); + given(context.getEnvironment()).willReturn(environment); + TestDockerComposeListener listener = new TestDockerComposeListener(shutdownHandlers, context); + ApplicationPreparedEvent event = new ApplicationPreparedEvent(application, new String[0], context); + listener.onApplicationEvent(event); + assertThat(listener.getManager()).isNotNull(); + then(listener.getManager()).should().startup(); + } + + class TestDockerComposeListener extends DockerComposeListener { + + private final ConfigurableApplicationContext context; + + private DockerComposeLifecycleManager manager; + + TestDockerComposeListener(SpringApplicationShutdownHandlers shutdownHandlers, + ConfigurableApplicationContext context) { + super(shutdownHandlers); + this.context = context; + } + + @Override + protected DockerComposeLifecycleManager createDockerComposeLifecycleManager( + ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties, + Set> eventListeners) { + this.manager = mock(DockerComposeLifecycleManager.class); + assertThat(applicationContext).isSameAs(this.context); + assertThat(binder).isNotNull(); + assertThat(properties).isNotNull(); + return this.manager; + } + + DockerComposeLifecycleManager getManager() { + return this.manager; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java new file mode 100644 index 00000000000..77c92b19427 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposePropertiesTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.io.File; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerComposeProperties}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposePropertiesTests { + + @Test + void getWhenNoPropertiesReturnsNew() { + Binder binder = new Binder(new MapConfigurationPropertySource()); + DockerComposeProperties properties = DockerComposeProperties.get(binder); + assertThat(properties.getFile()).isNull(); + assertThat(properties.getLifecycleManagement()).isEqualTo(LifecycleManagement.START_AND_STOP); + assertThat(properties.getHost()).isNull(); + assertThat(properties.getStartup().getCommand()).isEqualTo(StartupCommand.UP); + assertThat(properties.getShutdown().getCommand()).isEqualTo(ShutdownCommand.DOWN); + assertThat(properties.getShutdown().getTimeout()).isEqualTo(Duration.ofSeconds(10)); + assertThat(properties.getProfiles().getActive()).isEmpty(); + } + + @Test + void getWhenPropertiesReturnsBound() { + Map source = new LinkedHashMap<>(); + source.put("spring.docker.compose.file", "my-compose.yml"); + source.put("spring.docker.compose.lifecycle-management", "start-only"); + source.put("spring.docker.compose.host", "myhost"); + source.put("spring.docker.compose.startup.command", "start"); + source.put("spring.docker.compose.shutdown.command", "stop"); + source.put("spring.docker.compose.shutdown.timeout", "5s"); + source.put("spring.docker.compose.profiles.active", "myprofile"); + Binder binder = new Binder(new MapConfigurationPropertySource(source)); + DockerComposeProperties properties = DockerComposeProperties.get(binder); + assertThat(properties.getFile()).isEqualTo(new File("my-compose.yml")); + assertThat(properties.getLifecycleManagement()).isEqualTo(LifecycleManagement.START_ONLY); + assertThat(properties.getHost()).isEqualTo("myhost"); + assertThat(properties.getStartup().getCommand()).isEqualTo(StartupCommand.START); + assertThat(properties.getShutdown().getCommand()).isEqualTo(ShutdownCommand.STOP); + assertThat(properties.getShutdown().getTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(properties.getProfiles().getActive()).containsExactly("myprofile"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEventTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEventTests.java new file mode 100644 index 00000000000..cd58b3e9938 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeServicesReadyEventTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DockerComposeServicesReadyEvent}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DockerComposeServicesReadyEventTests { + + private ApplicationContext applicationContext = mock(ApplicationContext.class); + + private List runningServices = List.of(mock(RunningService.class)); + + private DockerComposeServicesReadyEvent event = new DockerComposeServicesReadyEvent(this.applicationContext, + this.runningServices); + + @Test + void getSourceReturnsSource() { + assertThat(this.event.getSource()).isSameAs(this.applicationContext); + } + + @Test + void getRunningServicesReturnsRunningServices() { + assertThat(this.event.getRunningServices()).isSameAs(this.runningServices); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagementTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagementTests.java new file mode 100644 index 00000000000..3f9396fe985 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/LifecycleManagementTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LifecycleManagement}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class LifecycleManagementTests { + + @Test + void shouldStartupWhenNone() { + assertThat(LifecycleManagement.NONE.shouldStartup()).isFalse(); + } + + @Test + void shouldShutdownWhenNone() { + assertThat(LifecycleManagement.NONE.shouldShutdown()).isFalse(); + } + + @Test + void shouldStartupWhenStartOnly() { + assertThat(LifecycleManagement.START_ONLY.shouldStartup()).isTrue(); + } + + @Test + void shouldShutdownWhenStartOnly() { + assertThat(LifecycleManagement.START_ONLY.shouldShutdown()).isFalse(); + } + + @Test + void shouldStartupWhenStartAndStop() { + assertThat(LifecycleManagement.START_AND_STOP.shouldStartup()).isTrue(); + } + + @Test + void shouldShutdownWhenStartAndStop() { + assertThat(LifecycleManagement.START_AND_STOP.shouldShutdown()).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommandTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommandTests.java new file mode 100644 index 00000000000..b13870a0b0c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/ShutdownCommandTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DockerCompose; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ShutdownCommand}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ShutdownCommandTests { + + private DockerCompose dockerCompose = mock(DockerCompose.class); + + private Duration duration = Duration.ofSeconds(10); + + @Test + void applyToWhenDown() { + ShutdownCommand.DOWN.applyTo(this.dockerCompose, this.duration); + then(this.dockerCompose).should().down(this.duration); + } + + @Test + void applyToWhenStart() { + ShutdownCommand.STOP.applyTo(this.dockerCompose, this.duration); + then(this.dockerCompose).should().stop(this.duration); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/StartupCommandTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/StartupCommandTests.java new file mode 100644 index 00000000000..b1b45489760 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/StartupCommandTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.lifecycle; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.DockerCompose; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link StartupCommand}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class StartupCommandTests { + + private DockerCompose dockerCompose = mock(DockerCompose.class); + + @Test + void applyToWhenUp() { + StartupCommand.UP.applyTo(this.dockerCompose); + then(this.dockerCompose).should().up(); + } + + @Test + void applyToWhenStart() { + StartupCommand.START.applyTo(this.dockerCompose); + then(this.dockerCompose).should().start(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessPropertiesTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessPropertiesTests.java new file mode 100644 index 00000000000..374733179f6 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessPropertiesTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReadinessProperties}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ReadinessPropertiesTests { + + @Test + void getWhenNoPropertiesReturnsNewInstance() { + Binder binder = new Binder(new MapConfigurationPropertySource()); + ReadinessProperties properties = ReadinessProperties.get(binder); + assertThat(properties.getTimeout()).isEqualTo(Duration.ofMinutes(2)); + assertThat(properties.getTcp().getConnectTimeout()).isEqualTo(Duration.ofMillis(200)); + assertThat(properties.getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(200)); + } + + @Test + void getWhenPropertiesReturnsBoundInstance() { + Map source = new LinkedHashMap<>(); + source.put("spring.docker.compose.readiness.timeout", "10s"); + source.put("spring.docker.compose.readiness.tcp.connect-timeout", "400ms"); + source.put("spring.docker.compose.readiness.tcp.read-timeout", "500ms"); + Binder binder = new Binder(new MapConfigurationPropertySource(source)); + ReadinessProperties properties = ReadinessProperties.get(binder); + assertThat(properties.getTimeout()).isEqualTo(Duration.ofSeconds(10)); + assertThat(properties.getTcp().getConnectTimeout()).isEqualTo(Duration.ofMillis(400)); + assertThat(properties.getTcp().getReadTimeout()).isEqualTo(Duration.ofMillis(500)); + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutExceptionTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutExceptionTests.java new file mode 100644 index 00000000000..8e6bbc0c925 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ReadinessTimeoutExceptionTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReadinessTimeoutException}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ReadinessTimeoutExceptionTests { + + @Test + void createCreatesException() { + Duration timeout = Duration.ofSeconds(10); + RunningService s1 = mock(RunningService.class); + given(s1.name()).willReturn("s1"); + RunningService s2 = mock(RunningService.class); + given(s2.name()).willReturn("s2"); + ServiceNotReadyException cause1 = new ServiceNotReadyException(s1, "1 not ready"); + ServiceNotReadyException cause2 = new ServiceNotReadyException(s2, "2 not ready"); + List exceptions = List.of(cause1, cause2); + ReadinessTimeoutException exception = new ReadinessTimeoutException(timeout, exceptions); + assertThat(exception).hasMessage("Readiness timeout of PT10S reached while waiting for services [s1, s2]"); + assertThat(exception).hasSuppressedException(cause1).hasSuppressedException(cause2); + assertThat(exception.getTimeout()).isEqualTo(timeout); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyExceptionTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyExceptionTests.java new file mode 100644 index 00000000000..a6aad170755 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceNotReadyExceptionTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ServiceNotReadyException}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ServiceNotReadyExceptionTests { + + @Test + void getServiceReturnsService() { + RunningService service = mock(RunningService.class); + ServiceNotReadyException exception = new ServiceNotReadyException(service, "fail"); + assertThat(exception.getService()).isEqualTo(service); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecksTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecksTests.java new file mode 100644 index 00000000000..1f931907d14 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/ServiceReadinessChecksTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.core.env.Environment; +import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; +import org.springframework.core.test.io.support.MockSpringFactoriesLoader; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link ServiceReadinessChecks}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ServiceReadinessChecksTests { + + private Clock clock; + + Instant now = Instant.now(); + + private MockSpringFactoriesLoader loader; + + private ClassLoader classLoader; + + private MockEnvironment environment; + + private Binder binder; + + private RunningService runningService; + + private List runningServices; + + private MockServiceReadinessCheck mockTcpCheck = new MockServiceReadinessCheck(); + + @BeforeEach + void setup() { + this.clock = mock(Clock.class); + given(this.clock.instant()).willAnswer((args) -> this.now); + this.loader = new MockSpringFactoriesLoader(); + this.classLoader = getClass().getClassLoader(); + this.environment = new MockEnvironment(); + this.binder = Binder.get(this.environment); + this.runningService = mock(RunningService.class); + this.runningServices = List.of(this.runningService); + } + + @Test + void loadCanResolveArguments() { + this.loader = spy(MockSpringFactoriesLoader.class); + createChecks(); + ArgumentCaptor captor = ArgumentCaptor.forClass(ArgumentResolver.class); + then(this.loader).should().load(eq(ServiceReadinessCheck.class), captor.capture()); + ArgumentResolver argumentResolver = captor.getValue(); + assertThat(argumentResolver.resolve(ClassLoader.class)).isEqualTo(this.classLoader); + assertThat(argumentResolver.resolve(Environment.class)).isEqualTo(this.environment); + assertThat(argumentResolver.resolve(Binder.class)).isEqualTo(this.binder); + } + + @Test + void waitUntilReadyWhenImmediatelyReady() { + MockServiceReadinessCheck check = new MockServiceReadinessCheck(); + this.loader.addInstance(ServiceReadinessCheck.class, check); + createChecks().waitUntilReady(this.runningServices); + assertThat(check.getChecked()).contains(this.runningService); + assertThat(this.mockTcpCheck.getChecked()).contains(this.runningService); + } + + @Test + void waitUntilReadyWhenTakesTimeToBeReady() { + MockServiceReadinessCheck check = new MockServiceReadinessCheck(2); + this.loader.addInstance(ServiceReadinessCheck.class, check); + createChecks().waitUntilReady(this.runningServices); + assertThat(check.getChecked()).hasSize(2).contains(this.runningService); + assertThat(this.mockTcpCheck.getChecked()).contains(this.runningService); + } + + @Test + void waitUntilReadyWhenTimeout() { + MockServiceReadinessCheck check = new MockServiceReadinessCheck(Integer.MAX_VALUE); + this.loader.addInstance(ServiceReadinessCheck.class, check); + assertThatExceptionOfType(ReadinessTimeoutException.class) + .isThrownBy(() -> createChecks().waitUntilReady(this.runningServices)) + .satisfies((ex) -> assertThat(ex.getSuppressed()).hasSize(1)); + assertThat(check.getChecked()).hasSizeGreaterThan(10); + } + + @Test + void waitForWhenServiceHasDisableLabelDoesNotCheck() { + given(this.runningService.labels()).willReturn(Map.of("org.springframework.boot.readiness-check.disable", "")); + MockServiceReadinessCheck check = new MockServiceReadinessCheck(); + this.loader.addInstance(ServiceReadinessCheck.class, check); + createChecks().waitUntilReady(this.runningServices); + assertThat(check.getChecked()).isEmpty(); + assertThat(this.mockTcpCheck.getChecked()).isEmpty(); + } + + void sleep(Duration duration) { + this.now = this.now.plus(duration); + } + + private ServiceReadinessChecks createChecks() { + return new ServiceReadinessChecks(this.clock, this::sleep, this.loader, this.classLoader, this.environment, + this.binder, (properties) -> this.mockTcpCheck); + } + + /** + * Mock {@link ServiceReadinessCheck}. + */ + static class MockServiceReadinessCheck implements ServiceReadinessCheck { + + private final Integer failUntil; + + private final List checked = new ArrayList<>(); + + MockServiceReadinessCheck() { + this(null); + } + + MockServiceReadinessCheck(Integer failUntil) { + this.failUntil = failUntil; + } + + @Override + public void check(RunningService service) throws ServiceNotReadyException { + this.checked.add(service); + if (this.failUntil != null && this.checked.size() < this.failUntil) { + throw new ServiceNotReadyException(service, "Waiting"); + } + } + + List getChecked() { + return this.checked; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheckTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheckTests.java new file mode 100644 index 00000000000..619afa0a223 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/readiness/TcpConnectServiceReadinessCheckTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.readiness; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.time.Duration; +import java.util.List; + +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TcpConnectServiceReadinessCheck}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class TcpConnectServiceReadinessCheckTests { + + private static final int EPHEMERAL_PORT = 0; + + private TcpConnectServiceReadinessCheck readinessCheck; + + @BeforeEach + void setup() { + ReadinessProperties.Tcp tcpProperties = new ReadinessProperties.Tcp(); + tcpProperties.setConnectTimeout(Duration.ofMillis(100)); + tcpProperties.setReadTimeout(Duration.ofMillis(100)); + this.readinessCheck = new TcpConnectServiceReadinessCheck(tcpProperties); + } + + @Test + void checkWhenServerWritesData() throws Exception { + withServer((socket) -> socket.getOutputStream().write('!'), (port) -> check(port)); + } + + @Test + void checkWhenNoSocketOutput() throws Exception { + // Simulate waiting for traffic from client to server. The sleep duration must + // be longer than the read timeout of the ready check! + withServer((socket) -> sleep(Duration.ofSeconds(10)), (port) -> check(port)); + } + + @Test + void checkWhenImmediateDisconnect() throws IOException { + withServer(Socket::close, + (port) -> assertThatExceptionOfType(ServiceNotReadyException.class).isThrownBy(() -> check(port)) + .withMessage("Immediate disconnect while connecting to port %d".formatted(port))); + } + + @Test + void checkWhenNoServerListening() { + assertThatExceptionOfType(ServiceNotReadyException.class).isThrownBy(() -> check(12345)) + .withMessage("IOException while connecting to port 12345"); + } + + private void withServer(ThrowingConsumer socketAction, ThrowingConsumer portAction) + throws IOException { + try (ServerSocket serverSocket = new ServerSocket()) { + serverSocket.bind(new InetSocketAddress("127.0.0.1", EPHEMERAL_PORT)); + Thread thread = new Thread(() -> { + try (Socket socket = serverSocket.accept()) { + socketAction.accept(socket); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + thread.setName("Acceptor-%d".formatted(serverSocket.getLocalPort())); + thread.setUncaughtExceptionHandler((ignored, ex) -> ex.printStackTrace()); + thread.setDaemon(true); + thread.start(); + portAction.accept(serverSocket.getLocalPort()); + } + } + + private void check(Integer port) { + this.readinessCheck.check(mockRunningService(port)); + } + + private RunningService mockRunningService(Integer port) { + RunningService runningService = mock(RunningService.class); + ConnectionPorts ports = mock(ConnectionPorts.class); + given(ports.getAll("tcp")).willReturn(List.of(port)); + given(runningService.host()).willReturn("localhost"); + given(runningService.ports()).willReturn(ports); + return runningService; + } + + private void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..93682588957 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ElasticsearchDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + ElasticsearchDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("elasticsearch-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + ElasticsearchConnectionDetails connectionDetails = run(ElasticsearchConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("elastic"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getPathPrefix()).isNull(); + assertThat(connectionDetails.getNodes()).hasSize(1); + Node node = connectionDetails.getNodes().get(0); + assertThat(node.hostname()).isNotNull(); + assertThat(node.port()).isGreaterThan(0); + assertThat(node.protocol()).isEqualTo(Protocol.HTTP); + assertThat(node.username()).isEqualTo("elastic"); + assertThat(node.password()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironmentTests.java new file mode 100644 index 00000000000..7c5bf5790da --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchEnvironmentTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ElasticsearchEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ElasticsearchEnvironmentTests { + + @Test + void createWhenHasElasticPasswordFileThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new ElasticsearchEnvironment(Map.of("ELASTIC_PASSWORD_FILE", "afile"))) + .withMessage("ELASTIC_PASSWORD_FILE is not supported"); + } + + @Test + void getPasswordWhenNoPassword() { + ElasticsearchEnvironment environment = new ElasticsearchEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasPassword() { + ElasticsearchEnvironment environment = new ElasticsearchEnvironment(Map.of("ELASTIC_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/XElasticsearchDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/XElasticsearchDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..2a4fd950d3a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/XElasticsearchDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link ElasticsearchDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XElasticsearchDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameIsElastic() { + RunningService service = createService(Collections.emptyMap()); + ElasticsearchService elasticsearchService = new ElasticsearchService(service); + assertThat(elasticsearchService.getUsername()).isEqualTo("elastic"); + } + + @Test + void passwordUsesEnvVariable() { + RunningService service = createService(Map.of("ELASTIC_PASSWORD", "some-secret-password")); + ElasticsearchService elasticsearchService = new ElasticsearchService(service); + assertThat(elasticsearchService.getPassword()).isEqualTo("some-secret-password"); + } + + @Test + void passwordHasFallback() { + RunningService service = createService(Collections.emptyMap()); + ElasticsearchService elasticsearchService = new ElasticsearchService(service); + assertThat(elasticsearchService.getPassword()).isNull(); + } + + @Test + void passwordDoesNotSupportFile() { + RunningService service = createService(Map.of("ELASTIC_PASSWORD_FILE", "/password.txt")); + ElasticsearchService elasticsearchService = new ElasticsearchService(service); + assertThatThrownBy(elasticsearchService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("ELASTIC_PASSWORD_FILE"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + ElasticsearchService elasticsearchService = new ElasticsearchService(service); + assertThat(elasticsearchService.getPort()).isEqualTo(19200); + } + + @Test + void matches() { + assertThat(ElasticsearchService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat( + ElasticsearchService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("elasticsearch:8.6.2"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(9200, 19200).env(env).build(); + } + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java new file mode 100644 index 00000000000..060bdc90db7 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.jdbc; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JdbcUrlBuilder}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class JdbcUrlBuilderTests { + + private JdbcUrlBuilder builder = new JdbcUrlBuilder("mydb", 1234); + + @Test + void createWhenDriverProtocolIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new JdbcUrlBuilder(null, 123)) + .withMessage("DriverProtocol must not be null"); + } + + @Test + void buildBuildsUrl() { + RunningService service = mockService(456); + String url = this.builder.build(service, "mydb"); + assertThat(url).isEqualTo("jdbc:mydb://myhost:456/mydb"); + } + + @Test + void buildWhenHasParamsLabelBuildsUrl() { + RunningService service = mockService(456, Map.of("org.springframework.boot.jdbc.parameters", "foo=bar")); + String url = this.builder.build(service, "mydb"); + assertThat(url).isEqualTo("jdbc:mydb://myhost:456/mydb?foo=bar"); + } + + @Test + void buildWhenServiceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.builder.build(null, "mydb")) + .withMessage("Service must not be null"); + } + + @Test + void buildWhenDatabaseIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.builder.build(mockService(456), null)) + .withMessage("Database must not be null"); + } + + private RunningService mockService(int mappedPort) { + return mockService(mappedPort, Collections.emptyMap()); + } + + private RunningService mockService(int mappedPort, Map labels) { + RunningService service = mock(RunningService.class); + ConnectionPorts ports = mock(ConnectionPorts.class); + given(ports.get(1234)).willReturn(mappedPort); + given(service.host()).willReturn("myhost"); + given(service.ports()).willReturn(ports); + given(service.labels()).willReturn(labels); + return service; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java new file mode 100644 index 00000000000..5d05239c23e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link MariaDbEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbEnvironmentTests { + + @Test + void createWhenHasMariadbRandomRootPasswordThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MariaDbEnvironment(Map.of("MARIADB_RANDOM_ROOT_PASSWORD", "true"))) + .withMessage("MARIADB_RANDOM_ROOT_PASSWORD is not supported"); + } + + @Test + void createWhenHasMysqlRandomRootPasswordThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MariaDbEnvironment(Map.of("MYSQL_RANDOM_ROOT_PASSWORD", "true"))) + .withMessage("MYSQL_RANDOM_ROOT_PASSWORD is not supported"); + } + + @Test + void createWhenHasMariadbRootPasswordHashThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MariaDbEnvironment(Map.of("MARIADB_ROOT_PASSWORD_HASH", "0FF"))) + .withMessage("MARIADB_ROOT_PASSWORD_HASH is not supported"); + } + + @Test + void createWhenHasNoPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new MariaDbEnvironment(Collections.emptyMap())) + .withMessage("No MariaDB password found"); + } + + @Test + void createWhenHasNoDatabaseThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new MariaDbEnvironment(Map.of("MARIADB_PASSWORD", "secret"))) + .withMessage("No MARIADB_DATABASE defined"); + } + + @Test + void getUsernameWhenHasMariadbUser() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_USER", "myself", "MARIADB_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("myself"); + } + + @Test + void getUsernameWhenHasMySqlUser() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MYSQL_USER", "myself", "MARIADB_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("myself"); + } + + @Test + void getUsernameWhenHasMariadbUserAndMySqlUser() { + MariaDbEnvironment environment = new MariaDbEnvironment(Map.of("MARIADB_USER", "myself", "MYSQL_USER", "me", + "MARIADB_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("myself"); + } + + @Test + void getUsernameWhenHasNoMariadbUserOrMySqlUser() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("root"); + } + + @Test + void getPasswordWhenHasMariadbPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasMysqlPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MYSQL_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasMysqlRootPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MYSQL_ROOT_PASSWORD", "secret", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasMariadbPasswordAndMysqlPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_PASSWORD", "secret", "MYSQL_PASSWORD", "donttell", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasMariadbPasswordAndMysqlRootPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_PASSWORD", "secret", "MYSQL_ROOT_PASSWORD", "donttell", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasNoPasswordAndMariadbAllowEmptyPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_ALLOW_EMPTY_PASSWORD", "true", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEmpty(); + } + + @Test + void getPasswordWhenHasNoPasswordAndMysqlAllowEmptyPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "true", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEmpty(); + } + + @Test + void getDatabaseWhenHasMariadbDatabase() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_ALLOW_EMPTY_PASSWORD", "true", "MARIADB_DATABASE", "db")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + + @Test + void getDatabaseWhenHasMysqlDatabase() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_ALLOW_EMPTY_PASSWORD", "true", "MYSQL_DATABASE", "db")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + + @Test + void getDatabaseWhenHasMariadbAndMysqlDatabase() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("MARIADB_ALLOW_EMPTY_PASSWORD", "true", "MARIADB_DATABASE", "db", "MYSQL_DATABASE", "otherdb")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..e3b4dbc1cb0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MariaDbJdbcDockerComposeConnectionDetailsFactory} + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MariaDbJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mariadb-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:mariadb://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..c72f7d8f8e9 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MariaDbR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MariaDbR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mariadb-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=mariadb", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/XMariaDbDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/XMariaDbDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..95907428933 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/XMariaDbDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mariadb; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link XMariaDbDockerComposeConnectionDetailsFactoryTests}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XMariaDbDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesMariaDbVariables() { + RunningService service = createService(Map.of("MARIADB_USER", "user-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_USER", "user-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameDefaultsToRoot() { + RunningService service = createService(Collections.emptyMap()); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getUsername()).isEqualTo("root"); + } + + @Test + void passwordUsesMariaDbVariables() { + RunningService service = createService(Map.of("MARIADB_PASSWORD", "password-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_PASSWORD", "password-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordUsesMariaDbRootVariables() { + RunningService service = createService(Map.of("MARIADB_ROOT_PASSWORD", "root-password-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo("root-password-1"); + } + + @Test + void passwordUsesMysqlRootVariables() { + RunningService service = createService(Map.of("MYSQL_ROOT_PASSWORD", "root-password-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo("root-password-1"); + } + + @Test + void passwordSupportsEmptyRootPasswordMariaDb() { + RunningService service = createService(Map.of("MARIADB_ALLOW_EMPTY_ROOT_PASSWORD", "")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo(""); + } + + @Test + void passwordDoesNotSupportRootPasswordHash() { + RunningService service = createService(Map.of("MARIADB_ROOT_PASSWORD_HASH", "true")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThatThrownBy(mariaDbService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MARIADB_ROOT_PASSWORD_HASH"); + } + + @Test + void passwordDoesNotSupportRandomRootPasswordMariaDb() { + RunningService service = createService(Map.of("MARIADB_RANDOM_ROOT_PASSWORD", "true")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThatThrownBy(mariaDbService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MARIADB_RANDOM_ROOT_PASSWORD"); + } + + @Test + void passwordDoesNotSupportRandomRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_RANDOM_ROOT_PASSWORD", "true")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThatThrownBy(mariaDbService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MYSQL_RANDOM_ROOT_PASSWORD"); + } + + @Test + void passwordHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MariaDbService mariaDbService = new MariaDbService(service); + assertThatThrownBy(mariaDbService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessage("Can't find password for user"); + } + + @Test + void passwordSupportsEmptyRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPassword()).isEqualTo(""); + } + + @Test + void databaseUsesMariaDbVariables() { + RunningService service = createService(Map.of("MARIADB_DATABASE", "database-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_DATABASE", "database-1")); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MariaDbService mariaDbService = new MariaDbService(service); + assertThatThrownBy(mariaDbService::getDatabase).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MARIADB_DATABASE") + .hasMessageContaining("MYSQL_DATABASE"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + MariaDbService mariaDbService = new MariaDbService(service); + assertThat(mariaDbService.getPort()).isEqualTo(33060); + } + + @Test + void matches() { + assertThat(MariaDbService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(MariaDbService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("mariadb:10.10"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(3306, 33060).env(env).build(); + } + + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..f4ff8fdfbe7 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mongo; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MongoDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MongoDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MongoDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mongo-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + MongoConnectionDetails connectionDetails = run(MongoConnectionDetails.class); + assertThat(connectionDetails.getConnectionString().toString()).startsWith("mongodb://root:secret@") + .endsWith("/mydatabase"); + assertThat(connectionDetails.getGridFs()).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironmentTests.java new file mode 100644 index 00000000000..825ce5e31ae --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironmentTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mongo; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link MongoEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MongoEnvironmentTests { + + @Test + void createWhenMonoInitdbRootUsernameFileSetThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MongoEnvironment(Map.of("MONGO_INITDB_ROOT_USERNAME_FILE", "file"))) + .withMessage("MONGO_INITDB_ROOT_USERNAME_FILE is not supported"); + } + + @Test + void createWhenMonoInitdbRootPasswordFileSetThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MongoEnvironment(Map.of("MONGO_INITDB_ROOT_PASSWORD_FILE", "file"))) + .withMessage("MONGO_INITDB_ROOT_PASSWORD_FILE is not supported"); + } + + @Test + void getUsernameWhenHasNoMongoInitdbRootUsernameSet() { + MongoEnvironment environment = new MongoEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isNull(); + } + + @Test + void getUsernameWhenHasMongoInitdbRootUsernameSet() { + MongoEnvironment environment = new MongoEnvironment(Map.of("MONGO_INITDB_ROOT_USERNAME", "user")); + assertThat(environment.getUsername()).isEqualTo("user"); + } + + @Test + void getPasswordWhenHasNoMongoInitdbRootPasswordSet() { + MongoEnvironment environment = new MongoEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasMongoInitdbRootPasswordSet() { + MongoEnvironment environment = new MongoEnvironment(Map.of("MONGO_INITDB_ROOT_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getDatabaseWhenHasNoMongoInitdbDatabaseSet() { + MongoEnvironment environment = new MongoEnvironment(Collections.emptyMap()); + assertThat(environment.getDatabase()).isNull(); + } + + @Test + void getDatabaseWhenHasMongoInitdbDatabaseSet() { + MongoEnvironment environment = new MongoEnvironment(Map.of("MONGO_INITDB_DATABASE", "db")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/XMongoDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/XMongoDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..c61228a6eff --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/XMongoDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mongo; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link MongoDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XMongoDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesEnvVariable() { + RunningService service = createService(Map.of("MONGO_INITDB_ROOT_USERNAME", "user-1")); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getUsername()).isEqualTo("user-1"); + } + + @Test + void doesNotSupportUsernameFile() { + RunningService service = createService(Map.of("MONGO_INITDB_ROOT_USERNAME_FILE", "/username.txt")); + MongoService mongoService = new MongoService(service); + assertThatThrownBy(mongoService::getUsername).isInstanceOf(IllegalStateException.class) + .hasMessage("MONGO_INITDB_ROOT_USERNAME_FILE is not supported"); + } + + @Test + void usernameHasDefault() { + RunningService service = createService(Collections.emptyMap()); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getUsername()).isNull(); + } + + @Test + void passwordUsesEnvVariable() { + RunningService service = createService(Map.of("MONGO_INITDB_ROOT_PASSWORD", "some-secret-1")); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getPassword()).isEqualTo("some-secret-1"); + } + + @Test + void doesNotSupportPasswordFile() { + RunningService service = createService(Map.of("MONGO_INITDB_ROOT_PASSWORD_FILE", "/username.txt")); + MongoService mongoService = new MongoService(service); + assertThatThrownBy(mongoService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessage("MONGO_INITDB_ROOT_PASSWORD_FILE is not supported"); + } + + @Test + void passwordHasDefault() { + RunningService service = createService(Collections.emptyMap()); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getPassword()).isNull(); + } + + @Test + void databaseUsesEnvVariable() { + RunningService service = createService(Map.of("MONGO_INITDB_DATABASE", "database-1")); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseHasDefault() { + RunningService service = createService(Collections.emptyMap()); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getDatabase()).isNull(); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + MongoService mongoService = new MongoService(service); + assertThat(mongoService.getPort()).isEqualTo(12345); + } + + @Test + void matches() { + assertThat(MongoService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(MongoService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("mongo:6.0"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(27017, 12345).env(env).build(); + } + + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java new file mode 100644 index 00000000000..98cb7e4f8ef --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link MySqlEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlEnvironmentTests { + + @Test + void createWhenHasMysqlRandomRootPasswordThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> new MySqlEnvironment(Map.of("MYSQL_RANDOM_ROOT_PASSWORD", "true"))) + .withMessage("MYSQL_RANDOM_ROOT_PASSWORD is not supported"); + } + + @Test + void createWhenHasNoPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new MySqlEnvironment(Collections.emptyMap())) + .withMessage("No MySQL password found"); + } + + @Test + void createWhenHasNoDatabaseThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new MySqlEnvironment(Map.of("MYSQL_PASSWORD", "secret"))) + .withMessage("No MYSQL_DATABASE defined"); + } + + @Test + void getUsernameWhenHasMySqlUser() { + MySqlEnvironment environment = new MySqlEnvironment( + Map.of("MYSQL_USER", "myself", "MYSQL_PASSWORD", "secret", "MYSQL_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("myself"); + } + + @Test + void getUsernameWhenHasNoMySqlUser() { + MySqlEnvironment environment = new MySqlEnvironment(Map.of("MYSQL_PASSWORD", "secret", "MYSQL_DATABASE", "db")); + assertThat(environment.getUsername()).isEqualTo("root"); + } + + @Test + void getPasswordWhenHasMysqlPassword() { + MySqlEnvironment environment = new MySqlEnvironment(Map.of("MYSQL_PASSWORD", "secret", "MYSQL_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasMysqlRootPassword() { + MySqlEnvironment environment = new MySqlEnvironment( + Map.of("MYSQL_ROOT_PASSWORD", "secret", "MYSQL_DATABASE", "db")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasNoPasswordAndMysqlAllowEmptyPassword() { + MySqlEnvironment environment = new MySqlEnvironment( + Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "true", "MYSQL_DATABASE", "db")); + assertThat(environment.getPassword()).isEmpty(); + } + + @Test + void getDatabaseWhenHasMysqlDatabase() { + MySqlEnvironment environment = new MySqlEnvironment( + Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "true", "MYSQL_DATABASE", "db")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..b33d7dc6ed8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MySqlJdbcDockerComposeConnectionDetailsFactory} + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MySqlJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mysql-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:mysql://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..e5718bbfc06 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MySqlR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MySqlR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mysql-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=mysql", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlDbR2dbcDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlDbR2dbcDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..4af76cd6e75 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlDbR2dbcDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for {@link MySqlR2dbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XMySqlDbR2dbcDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_USER", "user-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameDefaultsToRoot() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getUsername()).isEqualTo("root"); + } + + @Test + void passwordUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_PASSWORD", "password-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordUsesMysqlRootVariables() { + RunningService service = createService(Map.of("MYSQL_ROOT_PASSWORD", "root-password-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo("root-password-1"); + } + + @Test + void passwordDoesNotSupportRandomRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_RANDOM_ROOT_PASSWORD", "true")); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MYSQL_RANDOM_ROOT_PASSWORD"); + } + + @Test + void passwordHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessage("Can't find password for user"); + } + + @Test + void passwordSupportsEmptyRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo(""); + } + + @Test + void databaseUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_DATABASE", "database-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getDatabase).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MYSQL_DATABASE"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPort()).isEqualTo(33060); + } + + @Test + void matches() { + assertThat(MySqlService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(MySqlService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("mysql:8.0"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(3306, 33060).env(env).build(); + } + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlJdbcDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlJdbcDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..82c391e2712 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/XMySqlJdbcDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.mysql; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for {@link MySqlJdbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XMySqlJdbcDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_USER", "user-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameDefaultsToRoot() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getUsername()).isEqualTo("root"); + } + + @Test + void passwordUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_PASSWORD", "password-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordUsesMysqlRootVariables() { + RunningService service = createService(Map.of("MYSQL_ROOT_PASSWORD", "root-password-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo("root-password-1"); + } + + @Test + void passwordDoesNotSupportRandomRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_RANDOM_ROOT_PASSWORD", "true")); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MYSQL_RANDOM_ROOT_PASSWORD"); + } + + @Test + void passwordHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessage("Can't find password for user"); + } + + @Test + void passwordSupportsEmptyRootPasswordMysql() { + RunningService service = createService(Map.of("MYSQL_ALLOW_EMPTY_PASSWORD", "")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPassword()).isEqualTo(""); + } + + @Test + void databaseUsesMysqlVariables() { + RunningService service = createService(Map.of("MYSQL_DATABASE", "database-1")); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThatThrownBy(mysqlService::getDatabase).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MYSQL_DATABASE"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + MySqlService mysqlService = new MySqlService(service); + assertThat(mysqlService.getPort()).isEqualTo(33060); + } + + @Test + void matches() { + assertThat(MySqlService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(MySqlService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("mysql:8.0"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(3306, 33060).env(env).build(); + } + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java new file mode 100644 index 00000000000..1d57b2d5f43 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link PostgresEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class PostgresEnvironmentTests { + + @Test + void createWhenNoPostgresPasswordThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> new PostgresEnvironment(Collections.emptyMap())) + .withMessage("No POSTGRES_PASSWORD defined"); + } + + @Test + void getUsernameWhenNoPostgresUser() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRES_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("postgres"); + } + + @Test + void getUsernameWhenHasPostgresUser() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRES_USER", "me", "POSTGRES_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasPostgresPassword() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRES_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getDatabaseWhenNoPostgresDbOrPostgressUser() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRES_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("postgress"); + } + + @Test + void getDatabaseWhenNoPostgresDbAndPostgressUser() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRES_USER", "me", "POSTGRES_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("me"); + } + + @Test + void getDatabaseWhenHasPostgresDb() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRES_DB", "db", "POSTGRES_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..ff1a9466c08 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PostgresJdbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + PostgresJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("postgres-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..d741d3e1bcf --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PostgresR2dbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + PostgresR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("postgres-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresDbR2dbcDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresDbR2dbcDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..b42350423f7 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresDbR2dbcDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link PostgresR2dbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XPostgresDbR2dbcDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_USER", "user-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameHasFallback() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getUsername()).isEqualTo("postgres"); + } + + @Test + void passwordUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_PASSWORD", "password-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThatThrownBy(postgresService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("POSTGRES_PASSWORD"); + } + + @Test + void databaseUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_DB", "database-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseFallsBackToUsername() { + RunningService service = createService(Map.of("POSTGRES_USER", "user-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getDatabase()).isEqualTo("user-1"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getPort()).isEqualTo(54320); + } + + @Test + void matches() { + assertThat(PostgresService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(PostgresService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("postgres:15.2"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(5432, 54320).env(env).build(); + } + + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresJdbcDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresJdbcDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..b2741354dd0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/XPostgresJdbcDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.postgres; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link PostgresJdbcDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XPostgresJdbcDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_USER", "user-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameHasFallback() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getUsername()).isEqualTo("postgres"); + } + + @Test + void passwordUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_PASSWORD", "password-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getPassword()).isEqualTo("password-1"); + } + + @Test + void passwordHasNoFallback() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThatThrownBy(postgresService::getPassword).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("POSTGRES_PASSWORD"); + } + + @Test + void databaseUsesEnvVariable() { + RunningService service = createService(Map.of("POSTGRES_DB", "database-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getDatabase()).isEqualTo("database-1"); + } + + @Test + void databaseFallsBackToUsername() { + RunningService service = createService(Map.of("POSTGRES_USER", "user-1")); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getDatabase()).isEqualTo("user-1"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + PostgresService postgresService = new PostgresService(service); + assertThat(postgresService.getPort()).isEqualTo(54320); + } + + @Test + void matches() { + assertThat(PostgresService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(PostgresService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("postgres:15.2"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(5432, 54320).env(env).build(); + } + + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilderTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilderTests.java new file mode 100644 index 00000000000..c0d83f98115 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/r2dbc/ConnectionFactoryOptionsBuilderTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.r2dbc; + +import java.util.Collections; +import java.util.Map; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.docker.compose.core.ConnectionPorts; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConnectionFactoryOptionsBuilder}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ConnectionFactoryOptionsBuilderTests { + + private ConnectionFactoryOptionsBuilder builder = new ConnectionFactoryOptionsBuilder("mydb", 1234); + + @Test + void createWhenDriverProtocolIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new JdbcUrlBuilder(null, 123)) + .withMessage("DriverProtocol must not be null"); + } + + @Test + void buildBuildsOptions() { + RunningService service = mockService(456); + ConnectionFactoryOptions options = this.builder.build(service, "mydb", "user", "pass"); + assertThat(options).hasToString( + "ConnectionFactoryOptions{options={database=mydb, host=myhost, driver=mydb, password=REDACTED, port=456, user=user}}"); + } + + @Test + void buildWhenHasParamsLabelBuildsOptions() { + RunningService service = mockService(456, Map.of("org.springframework.boot.r2dbc.parameters", "foo=bar")); + ConnectionFactoryOptions options = this.builder.build(service, "mydb", "user", "pass"); + assertThat(options).hasToString( + "ConnectionFactoryOptions{options={foo=bar, database=mydb, host=myhost, driver=mydb, password=REDACTED, port=456, user=user}}"); + } + + @Test + void buildWhenServiceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.builder.build(null, "mydb", "user", "pass")) + .withMessage("Service must not be null"); + } + + @Test + void buildWhenDatabaseIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.build(mockService(456), null, "user", "pass")) + .withMessage("Database must not be null"); + } + + private RunningService mockService(int mappedPort) { + return mockService(mappedPort, Collections.emptyMap()); + } + + private RunningService mockService(int mappedPort, Map labels) { + RunningService service = mock(RunningService.class); + ConnectionPorts ports = mock(ConnectionPorts.class); + given(ports.get(1234)).willReturn(mappedPort); + given(service.host()).willReturn("myhost"); + given(service.ports()).willReturn(ports); + given(service.labels()).willReturn(labels); + return service; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..2a3930ed2fd --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.rabbit; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + RabbitDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("rabbit-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + RabbitConnectionDetails connectionDetails = run(RabbitConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getAddresses()).hasSize(1); + Address address = connectionDetails.getFirstAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java new file mode 100644 index 00000000000..8aaf45a3425 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.rabbit; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RabbitEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class RabbitEnvironmentTests { + + @Test + void getUsernameWhenNoRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getUsernameWhenNoRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/XRabbitDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/XRabbitDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..06dae5491c8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/XRabbitDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.rabbit; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link RabbitDockerComposeConnectionDetailsFactory} + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XRabbitDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void usernameUsesEnvVariable() { + RunningService service = createService(Map.of("RABBITMQ_DEFAULT_USER", "user-1")); + RabbitService rabbitService = new RabbitService(service); + assertThat(rabbitService.getUsername()).isEqualTo("user-1"); + } + + @Test + void usernameHasDefault() { + RunningService service = createService(Collections.emptyMap()); + RabbitService rabbitService = new RabbitService(service); + assertThat(rabbitService.getUsername()).isEqualTo("guest"); + } + + @Test + void passwordUsesEnvVariable() { + RunningService service = createService(Map.of("RABBITMQ_DEFAULT_PASS", "secret-1")); + RabbitService rabbitService = new RabbitService(service); + assertThat(rabbitService.getPassword()).isEqualTo("secret-1"); + } + + @Test + void passwordHasDefault() { + RunningService service = createService(Collections.emptyMap()); + RabbitService rabbitService = new RabbitService(service); + assertThat(rabbitService.getPassword()).isEqualTo("guest"); + } + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + RabbitService rabbitService = new RabbitService(service); + assertThat(rabbitService.getPort()).isEqualTo(15672); + } + + @Test + void matches() { + assertThat(RabbitService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(RabbitService.matches(createService(ImageReference.parse("redis:7.1"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("rabbitmq:3.11"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(5672, 15672).env(env).build(); + } + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..1373362056d --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.redis; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Standalone; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for {@link RedisDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class RedisDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + RedisDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("redis-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + RedisConnectionDetails connectionDetails = run(RedisConnectionDetails.class); + Standalone standalone = connectionDetails.getStandalone(); + assertThat(connectionDetails.getUsername()).isNull(); + assertThat(connectionDetails.getPassword()).isNull(); + assertThat(connectionDetails.getCluster()).isNull(); + assertThat(connectionDetails.getSentinel()).isNull(); + assertThat(standalone).isNotNull(); + assertThat(standalone.getDatabase()).isZero(); + assertThat(standalone.getPort()).isGreaterThan(0); + assertThat(standalone.getHost()).isNotNull(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..5bb17cfced8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.redis; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link RedisDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class RedisDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + +@Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + RedisService redisService = new RedisService(service); + assertThat(redisService.getPort()).isEqualTo(16379); + } + + @Test + void matches() { + assertThat(RedisService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(RedisService.matches(createService(ImageReference.parse("postgres:15.2"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("redis:7.0"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(6379, 16379).env(env).build(); + } + + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java new file mode 100644 index 00000000000..fc210b9af03 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringApplicationShutdownHandlers; +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.testsupport.process.DisabledIfProcessUnavailable; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.function.ThrowingSupplier; + +/** + * Abstract base class for integration tests. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + */ +@DisabledIfProcessUnavailable({ "docker", "compose" }) +public abstract class AbstractDockerComposeIntegrationTests { + + private final Resource composeResource; + + @AfterAll + static void shutdown() { + SpringApplicationShutdownHandlers shutdownHandlers = SpringApplication.getShutdownHandlers(); + ((Runnable) shutdownHandlers).run(); + } + + protected AbstractDockerComposeIntegrationTests(String composeResource) { + this.composeResource = new ClassPathResource(composeResource, getClass()); + } + + protected final T run(Class type) { + SpringApplication application = new SpringApplication(Config.class); + Map properties = new LinkedHashMap<>(); + properties.put("spring.docker.compose.skip.in-tests", "false"); + properties.put("spring.docker.compose.file", + ThrowingSupplier.of(this.composeResource::getFile).get().getAbsolutePath()); + application.setDefaultProperties(properties); + return application.run().getBean(type); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractIntegrationTests.java new file mode 100644 index 00000000000..d2e081d88a7 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.test; + +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +/** + * Abstract base class for integration tests. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + */ +@DisabledIfDockerUnavailable +public abstract class AbstractIntegrationTests { + + //// @formatter:off + + /* + + @TempDir + static Path tempDir; + + private static List shutdownHandler; + + @BeforeAll + static void beforeAll() { + shutdownHandler = new ArrayList<>(); + } + + @AfterAll + static void afterAll() { + for (Runnable runnable : shutdownHandler) { + runnable.run(); + } + } + + @BeforeEach + void setUp() throws IOException { + createComposeYaml(); + } + + protected abstract InputStream getComposeContent(); + + protected final T runProvider(Class serviceConnectionClass) { + return runProvider(new MockEnvironment(), serviceConnectionClass); + } + + protected final T runProvider(MockEnvironment environment, + Class serviceConnectionClass) { + environment.setProperty("spring.dev-services.docker-compose.stop-mode", "down"); + DockerComposeListener dockerComposeListener = createProvider(environment); + GenericApplicationContext context = new GenericApplicationContext(); + context.setEnvironment(environment); + dockerComposeListener + .onApplicationEvent(new ApplicationPreparedEvent(new SpringApplication(), new String[0], context)); + context.refresh(); + T serviceConnection = context.getBean(serviceConnectionClass); + assertThat(serviceConnection.getOrigin()).isInstanceOf(DockerComposeOrigin.class); + return serviceConnection; + } + + private DockerComposeListener createProvider(Environment environment) { + return new DockerComposeListener(getClass().getClassLoader(), environment, null, null, null, tempDir, + new SpringApplicationShutdownHandlers() { + + @Override + public void add(Runnable action) { + shutdownHandler.add(action); + } + + @Override + public void remove(Runnable action) { + } + + }); + } + + private void createComposeYaml() throws IOException { + try (InputStream stream = getComposeContent()) { + byte[] content = stream.readAllBytes(); + Files.write(tempDir.resolve("compose.yaml"), content); + } + } + + */ + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/XZipkinDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/XZipkinDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 00000000000..8dca340be48 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/XZipkinDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.zipkin; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link ZipkinDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +@Disabled +class XZipkinDockerComposeConnectionDetailsFactoryTests { + + @Test + void test() { + fail("Not yet implemented"); + } + + // @formatter:off + + /* + + + @Test + void getPort() { + RunningService service = createService(Collections.emptyMap()); + ZipkinService zipkinService = new ZipkinService(service); + assertThat(zipkinService.getPort()).isEqualTo(19411); + } + + @Test + void matches() { + assertThat(ZipkinService.matches(createService(Collections.emptyMap()))).isTrue(); + assertThat(ZipkinService.matches(createService(ImageReference.parse("postgres:15.2"), Collections.emptyMap()))) + .isFalse(); + } + + private RunningService createService(Map env) { + return createService(ImageReference.parse("openzipkin/zipkin:2.24"), env); + } + + private RunningService createService(ImageReference image, Map env) { + return RunningServiceBuilder.create("service-1", image).addTcpPort(9411, 19411).env(env).build(); + } + + + */ + + // @formatter:on + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..6eeffc25029 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/zipkin/ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.zipkin; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ZipkinDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + ZipkinDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("zipkin-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + ZipkinConnectionDetails connectionDetails = run(ZipkinConnectionDetails.class); + assertThat(connectionDetails.getSpanEndpoint()).startsWith("http://").endsWith("/api/v2/spans"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-config.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-config.json new file mode 100644 index 00000000000..0f3c71e176e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-config.json @@ -0,0 +1,29 @@ +{ + "name": "redis-docker", + "services": { + "redis": { + "command": null, + "entrypoint": null, + "image": "redis:7.0", + "networks": { + "default": null + }, + "ports": [ + { + "mode": "ingress", + "target": 6379, + "protocol": "tcp" + } + ] + } + }, + "networks": { + "default": { + "name": "redis-docker_default", + "ipam": { + + }, + "external": false + } + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-ps.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-ps.json new file mode 100644 index 00000000000..d89b1342b07 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-ps.json @@ -0,0 +1,16 @@ +{ + "Command": "/command", + "CreatedAt": "2023-02-21 13:35:10 +0100 CET", + "ID": "f5af31dae7f6", + "Image": "redis:7.0", + "Labels": "com.docker.compose.project.config_files=/compose.yaml,com.docker.compose.project.working_dir=/,com.docker.compose.container-number=1,com.docker.compose.image=sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82,com.docker.compose.oneoff=False,com.docker.compose.project=redis-docker,com.docker.compose.config-hash=cfdc8e119d85a53c7d47edb37a3b160a8c83ba48b0428ebc07713befec991dd0,com.docker.compose.depends_on=,com.docker.compose.service=redis,com.docker.compose.version=2.16.0", + "LocalVolumes": "1", + "Mounts": "9edc7fa2fe6c9e…", + "Name": "redis-docker-redis-1", + "Networks": "redis-docker_default", + "Ports": "0.0.0.0:32770-\\u003e6379/tcp, :::32770-\\u003e6379/tcp", + "RunningFor": "2 days ago", + "Size": "0B", + "State": "running", + "Status": "Up 3 seconds" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-version.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-version.json new file mode 100644 index 00000000000..2bf83a12dde --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-compose-version.json @@ -0,0 +1,3 @@ +{ + "version": "123" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-context.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-context.json new file mode 100644 index 00000000000..8e13bdc4105 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-context.json @@ -0,0 +1,8 @@ +{ + "Current": true, + "Description": "Current DOCKER_HOST based configuration", + "DockerEndpoint": "unix:///var/run/docker.sock", + "Error": "", + "KubernetesEndpoint": "", + "Name": "default" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-bridge-network.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-bridge-network.json new file mode 100644 index 00000000000..151c4c74376 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-bridge-network.json @@ -0,0 +1,250 @@ +{ + "Id": "f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc", + "Created": "2023-02-21T12:35:10.468917704Z", + "Path": "docker-entrypoint.sh", + "Args": [ + "redis-server" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 38657, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-02-23T12:55:27.585705588Z", + "FinishedAt": "2023-02-23T12:46:42.013469854Z" + }, + "Image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "ResolvConfPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/hostname", + "HostsPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/hosts", + "LogPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc-json.log", + "Name": "/redis-docker-redis-1", + "RestartCount": 0, + "Driver": "btrfs", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": { + + } + }, + "NetworkMode": "redis-docker_default", + "PortBindings": { + "6379/tcp": [ + { + "HostIp": "", + "HostPort": "" + } + ] + }, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": null, + "Name": "btrfs" + }, + "Mounts": [ + { + "Type": "volume", + "Name": "9edc7fa2fe6c9e8f67fd31a8649a4b5d7edbc9c1604462e04a5f35d6bfda87c3", + "Source": "/var/lib/docker/volumes/9edc7fa2fe6c9e8f67fd31a8649a4b5d7edbc9c1604462e04a5f35d6bfda87c3/_data", + "Destination": "/data", + "Driver": "local", + "Mode": "", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "f5af31dae7f6", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "6379/tcp": { + + } + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "GOSU_VERSION=1.16", + "REDIS_VERSION=7.0.8", + "REDIS_DOWNLOAD_URL=https://download.redis.io/releases/redis-7.0.8.tar.gz", + "REDIS_DOWNLOAD_SHA=06a339e491306783dcf55b97f15a5dbcbdc01ccbde6dc23027c475cab735e914" + ], + "Cmd": [ + "redis-server" + ], + "Image": "redis:7.0", + "Volumes": { + "/data": { + + } + }, + "WorkingDir": "/data", + "Entrypoint": [ + "docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "cfdc8e119d85a53c7d47edb37a3b160a8c83ba48b0428ebc07713befec991dd0", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "redis-docker", + "com.docker.compose.project.config_files": "/compose.yaml", + "com.docker.compose.project.working_dir": "/", + "com.docker.compose.service": "redis", + "com.docker.compose.version": "2.16.0" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "3df878d8ed31b2686e41437f141bebba8afcf3bdf8c47ea07c34c2e0b365ec88", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": { + "6379/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "32770" + }, + { + "HostIp": "::", + "HostPort": "32770" + } + ] + }, + "SandboxKey": "/var/run/docker/netns/3df878d8ed31", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "redis-docker_default": { + "IPAMConfig": null, + "Links": null, + "Aliases": [ + "redis-docker-redis-1", + "redis", + "f5af31dae7f6" + ], + "NetworkID": "9cb2b8b6fb20703841b9337b48e65ed2a71e2da2e995e4782066d146c44fc205", + "EndpointID": "e155c61c1608b20ba7a0bd34790fc342ec576310f75ef4399e96bf3a67e8b3f6", + "Gateway": "192.168.32.1", + "IPAddress": "192.168.32.2", + "IPPrefixLen": 20, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:c0:a8:20:02", + "DriverOpts": null + } + } + } +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-host-network.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-host-network.json new file mode 100644 index 00000000000..7d179da1570 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect-host-network.json @@ -0,0 +1,237 @@ +{ + "Id": "111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1", + "Created": "2023-02-23T14:19:06.668158561Z", + "Path": "docker-entrypoint.sh", + "Args": [ + "redis-server" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 46377, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-02-23T14:19:07.001096801Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "ResolvConfPath": "/var/lib/docker/containers/111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1/hostname", + "HostsPath": "/var/lib/docker/containers/111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1/hosts", + "LogPath": "/var/lib/docker/containers/111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1/111b22dba993f3282257cbafc87c77763cb4f8a8e534804ef1feae9c8ef282a1-json.log", + "Name": "/redis-docker-redis-1", + "RestartCount": 0, + "Driver": "btrfs", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": { + + } + }, + "NetworkMode": "host", + "PortBindings": { + "6379/tcp": [ + { + "HostIp": "", + "HostPort": "" + } + ] + }, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": null, + "Name": "btrfs" + }, + "Mounts": [ + { + "Type": "volume", + "Name": "0ff245e74dc368da772d4d1139b2aafd423ca1ce1fbe502b4635d7d15f0faf8c", + "Source": "/var/lib/docker/volumes/0ff245e74dc368da772d4d1139b2aafd423ca1ce1fbe502b4635d7d15f0faf8c/_data", + "Destination": "/data", + "Driver": "local", + "Mode": "", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "fedora", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "6379/tcp": { + + } + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "GOSU_VERSION=1.16", + "REDIS_VERSION=7.0.8", + "REDIS_DOWNLOAD_URL=https://download.redis.io/releases/redis-7.0.8.tar.gz", + "REDIS_DOWNLOAD_SHA=06a339e491306783dcf55b97f15a5dbcbdc01ccbde6dc23027c475cab735e914" + ], + "Cmd": [ + "redis-server" + ], + "Image": "redis:7.0", + "Volumes": { + "/data": { + + } + }, + "WorkingDir": "/data", + "Entrypoint": [ + "docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "204d00fc2f8ffd749769e3f6c160b2a2366e76cf8980bb2984bc65674748b3ca", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "redis-docker", + "com.docker.compose.project.config_files": "/compose.yaml", + "com.docker.compose.project.working_dir": "/", + "com.docker.compose.service": "redis", + "com.docker.compose.version": "2.16.0" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "6ec5c12e14078b424707534b8b64d0953ce9da21eaebd422daefff2d6a08f14d", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": { + + }, + "SandboxKey": "/var/run/docker/netns/default", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "host": { + "IPAMConfig": null, + "Links": null, + "Aliases": null, + "NetworkID": "c8fa8aac531ce4630465de4baf8d0310a2ff3243b3986e5251611c1a4ee6e1b3", + "EndpointID": "cfdc6016b0dd724f7714ae116c5fa33127401f5e6853f07c4c1db5e967871136", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "", + "DriverOpts": null + } + } + } +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect.json b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect.json new file mode 100644 index 00000000000..57f144dbc11 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/core/docker-inspect.json @@ -0,0 +1,248 @@ +{ + "Id": "f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc", + "Created": "2023-02-21T12:35:10.468917704Z", + "Path": "docker-entrypoint.sh", + "Args": [ + "redis-server" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 38657, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-02-23T12:55:27.585705588Z", + "FinishedAt": "2023-02-23T12:46:42.013469854Z" + }, + "Image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "ResolvConfPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/hostname", + "HostsPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/hosts", + "LogPath": "/var/lib/docker/containers/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc/f5af31dae7f665bd194ec7261bdc84e5df9c64753abb4a6cec6c33f7cf64c3fc-json.log", + "Name": "/redis-docker-redis-1", + "RestartCount": 0, + "Driver": "btrfs", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": { + + } + }, + "NetworkMode": "redis-docker_default", + "PortBindings": { + "6379/tcp": [ + { + "HostIp": "", + "HostPort": "" + } + ] + }, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "private", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": null, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": null, + "Name": "btrfs" + }, + "Mounts": [ + { + "Type": "volume", + "Name": "9edc7fa2fe6c9e8f67fd31a8649a4b5d7edbc9c1604462e04a5f35d6bfda87c3", + "Source": "/var/lib/docker/volumes/9edc7fa2fe6c9e8f67fd31a8649a4b5d7edbc9c1604462e04a5f35d6bfda87c3/_data", + "Destination": "/data", + "Driver": "local", + "Mode": "", + "RW": true, + "Propagation": "" + } + ], + "Config": { + "Hostname": "f5af31dae7f6", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "6379/tcp": { + + } + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "GOSU_VERSION=1.16", + "REDIS_VERSION=7.0.8" + ], + "Cmd": [ + "redis-server" + ], + "Image": "redis:7.0", + "Volumes": { + "/data": { + + } + }, + "WorkingDir": "/data", + "Entrypoint": [ + "docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "cfdc8e119d85a53c7d47edb37a3b160a8c83ba48b0428ebc07713befec991dd0", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:e79ba23ed43baa22054741136bf45bdb041824f41c5e16c0033ea044ca164b82", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "redis-docker", + "com.docker.compose.project.config_files": "compose.yaml", + "com.docker.compose.project.working_dir": "/", + "com.docker.compose.service": "redis", + "com.docker.compose.version": "2.16.0" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "3df878d8ed31b2686e41437f141bebba8afcf3bdf8c47ea07c34c2e0b365ec88", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": { + "6379/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "32770" + }, + { + "HostIp": "::", + "HostPort": "32770" + } + ] + }, + "SandboxKey": "/var/run/docker/netns/3df878d8ed31", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "redis-docker_default": { + "IPAMConfig": null, + "Links": null, + "Aliases": [ + "redis-docker-redis-1", + "redis", + "f5af31dae7f6" + ], + "NetworkID": "9cb2b8b6fb20703841b9337b48e65ed2a71e2da2e995e4782066d146c44fc205", + "EndpointID": "e155c61c1608b20ba7a0bd34790fc342ec576310f75ef4399e96bf3a67e8b3f6", + "Gateway": "192.168.32.1", + "IPAddress": "192.168.32.2", + "IPPrefixLen": 20, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:c0:a8:20:02", + "DriverOpts": null + } + } + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-compose.yaml new file mode 100644 index 00000000000..b5d214f5445 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-compose.yaml @@ -0,0 +1,11 @@ +services: + elasticsearch: + image: 'elasticsearch:8.6.1' + environment: + - 'ELASTIC_PASSWORD=secret' + - 'ES_JAVA_OPTS=-Xmx512m' + - 'xpack.security.enabled=false' + - 'discovery.type=single-node' + ports: + - '9200' + - '9300' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-compose.yaml new file mode 100644 index 00000000000..7902b194f44 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-compose.yaml @@ -0,0 +1,11 @@ +services: + database: + image: 'mariadb:10.10' + ports: + - '3306' + environment: + - 'MARIADB_ROOT_PASSWORD=verysecret' + - 'MARIADB_USER=myuser' + - 'MARIADB_PASSWORD=secret' + - 'MARIADB_DATABASE=mydatabase' + diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-compose.yaml new file mode 100644 index 00000000000..279af979588 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-compose.yaml @@ -0,0 +1,9 @@ +services: + mongo: + image: 'mongo:6.0' + ports: + - '27017' + environment: + MONGO_INITDB_ROOT_USERNAME: 'root' + MONGO_INITDB_ROOT_PASSWORD: 'secret' + MONGO_INITDB_DATABASE: 'mydatabase' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-compose.yaml new file mode 100644 index 00000000000..6aa9f4da092 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-compose.yaml @@ -0,0 +1,10 @@ +services: + database: + image: 'mysql:8.0' + ports: + - '3306' + environment: + - 'MYSQL_ROOT_PASSWORD=verysecret' + - 'MYSQL_USER=myuser' + - 'MYSQL_PASSWORD=secret' + - 'MYSQL_DATABASE=mydatabase' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-compose.yaml new file mode 100644 index 00000000000..5f781d980f3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-compose.yaml @@ -0,0 +1,9 @@ +services: + database: + image: 'postgres:15.2' + ports: + - '5432' + environment: + - 'POSTGRES_USER=myuser' + - 'POSTGRES_DB=mydatabase' + - 'POSTGRES_PASSWORD=secret' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-compose.yaml new file mode 100644 index 00000000000..4808c8432e5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: 'rabbitmq:3.11' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-compose.yaml new file mode 100644 index 00000000000..411e2569699 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-compose.yaml @@ -0,0 +1,5 @@ +services: + redis: + image: 'redis:7.0' + ports: + - '6379' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/zipkin/zipkin-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/zipkin/zipkin-compose.yaml new file mode 100644 index 00000000000..d5ebe65d4d5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/zipkin/zipkin-compose.yaml @@ -0,0 +1,5 @@ +services: + zipkin: + image: 'openzipkin/zipkin:2.24' + ports: + - '9411' diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 3d045435c4a..9306848a3cb 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -59,6 +59,7 @@ dependencies { configurationProperties(project(path: ":spring-boot-project:spring-boot-actuator", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-actuator-autoconfigure", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-autoconfigure", configuration: "configurationPropertiesMetadata")) + configurationProperties(project(path: ":spring-boot-project:spring-boot-docker-compose", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-devtools", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-test-autoconfigure", configuration: "configurationPropertiesMetadata")) @@ -67,6 +68,7 @@ dependencies { implementation(project(path: ":spring-boot-project:spring-boot-actuator")) implementation(project(path: ":spring-boot-project:spring-boot-actuator-autoconfigure")) implementation(project(path: ":spring-boot-project:spring-boot-autoconfigure")) + implementation(project(path: ":spring-boot-project:spring-boot-docker-compose")) implementation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-cli")) implementation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) implementation(project(path: ":spring-boot-project:spring-boot-test")) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc index 74adc6cc415..925a8134843 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc @@ -45,4 +45,6 @@ include::application-properties/actuator.adoc[] include::application-properties/devtools.adoc[] +include::application-properties/docker-compose.adoc[] + include::application-properties/testing.adoc[] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java index 805b3bf1625..72576db04f7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java @@ -163,7 +163,7 @@ class SpringApplicationShutdownHook implements Runnable { /** * The handler actions for this shutdown hook. */ - private class Handlers implements SpringApplicationShutdownHandlers { + private class Handlers implements SpringApplicationShutdownHandlers, Runnable { private final Set actions = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -189,6 +189,12 @@ class SpringApplicationShutdownHook implements Runnable { return this.actions; } + @Override + public void run() { + SpringApplicationShutdownHook.this.run(); + SpringApplicationShutdownHook.this.reset(); + } + } /** diff --git a/src/nohttp/suppressions.xml b/src/nohttp/suppressions.xml index 69575ea3f59..93db2829745 100644 --- a/src/nohttp/suppressions.xml +++ b/src/nohttp/suppressions.xml @@ -9,4 +9,5 @@ +