Add Docker Compose support

Add `spring-boot-docker-compose` module with service connection
support.

Closes gh-34747

Co-authored-by: Phillip Webb <pwebb@vmware.com>
Co-authored-by: "Andy Wilkinson <wilkinsona@vmware.com>
pull/35031/head
Mortitz Halbritter 2 years ago committed by Phillip Webb
parent 4ae24e404e
commit 842e17eced

@ -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");
}

@ -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"

@ -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",

@ -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")
}

@ -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<Integer> 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<Integer> getAll(String protocol);
}

@ -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<ContainerPort, Integer> mappings = new LinkedHashMap<>();
private Map<Integer, Integer> portMappings = new LinkedHashMap<>();
DefaultConnectionPorts(DockerCliInspectResponse inspectResponse) {
this.mappings = !isHostNetworkMode(inspectResponse)
? buildMappingsForNetworkSettings(inspectResponse.networkSettings())
: buildMappingsForHostNetworking(inspectResponse.config());
Map<Integer, Integer> 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<ContainerPort, Integer> buildMappingsForNetworkSettings(NetworkSettings networkSettings) {
if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) {
return Collections.emptyMap();
}
Map<ContainerPort, Integer> 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<ContainerPort, Integer> buildMappingsForHostNetworking(Config config) {
if (CollectionUtils.isEmpty(config.exposedPorts())) {
return Collections.emptyMap();
}
Map<ContainerPort, Integer> 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<Integer> getAll() {
return getAll(null);
}
@Override
public List<Integer> getAll(String protocol) {
List<Integer> hostPorts = new ArrayList<>();
this.mappings.forEach((containerPort, hostPort) -> {
if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) {
hostPorts.add(hostPort);
}
});
return Collections.unmodifiableList(hostPorts);
}
Map<ContainerPort, Integer> 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);
}
}
}
}

@ -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<RunningService> getRunningServices() {
List<DockerCliComposePsResponse> runningPsResponses = runComposePs().stream().filter(this::isRunning).toList();
if (runningPsResponses.isEmpty()) {
return Collections.emptyList();
}
DockerComposeFile dockerComposeFile = this.cli.getDockerComposeFile();
List<RunningService> result = new ArrayList<>();
Map<String, DockerCliInspectResponse> 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<String, DockerCliInspectResponse> inspect(List<DockerCliComposePsResponse> runningPsResponses) {
List<String> ids = runningPsResponses.stream().map(DockerCliComposePsResponse::id).toList();
List<DockerCliInspectResponse> inspectResponses = this.cli.run(new DockerCliCommand.Inspect(ids));
return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity()));
}
private List<DockerCliComposePsResponse> runComposePs() {
return this.cli.run(new DockerCliCommand.ComposePs());
}
private boolean isRunning(DockerCliComposePsResponse psResponse) {
return !"exited".equals(psResponse.state());
}
}

@ -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<String, String> 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<String, String> env() {
return this.env.asMap();
}
@Override
public Map<String, String> labels() {
return this.labels;
}
@Override
public String toString() {
return this.name;
}
}

@ -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<String> dockerCommand;
private final List<String> dockerComposeCommand;
private final DockerComposeFile composeFile;
private final Set<String> 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<String> 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<String> 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<String> 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 <R> the response type
* @param dockerCommand the command to run
* @return the response
*/
<R> R run(DockerCliCommand<R> dockerCommand) {
List<String> command = createCommand(dockerCommand.getType());
command.addAll(dockerCommand.getCommand());
String json = this.processRunner.run(command.toArray(new String[0]));
return dockerCommand.deserialize(json);
}
private <R> List<String> createCommand(Type type) {
return switch (type) {
case DOCKER -> new ArrayList<>(this.dockerCommand);
case DOCKER_COMPOSE -> {
List<String> 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;
}
}

@ -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 <R> the response type
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
*/
abstract sealed class DockerCliCommand<R> {
private final Type type;
private final Class<?> responseType;
private final boolean listResponse;
private final List<String> 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<String> 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<String> command, Collection<String> args) {
List<String> result = new ArrayList<>(command);
result.addAll(args);
return result.toArray(new String[0]);
}
/**
* The {@code docker context} command.
*/
static final class Context extends DockerCliCommand<List<DockerCliContextResponse>> {
Context() {
super(Type.DOCKER, DockerCliContextResponse.class, true, "context", "ls", "--format={{ json . }}");
}
}
/**
* The {@code docker inspect} command.
*/
static final class Inspect extends DockerCliCommand<List<DockerCliInspectResponse>> {
Inspect(Collection<String> 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<DockerCliComposeConfigResponse> {
ComposeConfig() {
super(Type.DOCKER_COMPOSE, DockerCliComposeConfigResponse.class, false, "config", "--format=json");
}
}
/**
* The {@code docker compose ps} command.
*/
static final class ComposePs extends DockerCliCommand<List<DockerCliComposePsResponse>> {
ComposePs() {
super(Type.DOCKER_COMPOSE, DockerCliComposePsResponse.class, true, "ps", "--format=json");
}
}
/**
* The {@code docker compose up} command.
*/
static final class ComposeUp extends DockerCliCommand<Void> {
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<Void> {
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<Void> {
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<Void> {
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
}
}

@ -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<String, DockerCliComposeConfigResponse.Service> services) {
/**
* Docker compose service.
*
* @param image the image
*/
record Service(String image) {
}
}

@ -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) {
}

@ -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) {
}

@ -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) {
}

@ -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<String, String> labels, Map<String, ExposedPort> exposedPorts, List<String> 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<String, List<HostPort>> ports) {
}
/**
* Port mapping details.
*
* @param hostIp the host IP
* @param hostPort the host port
*/
record HostPort(String hostIp, String hostPort) {
}
}

@ -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<RunningService> 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<String> activeProfiles) {
DockerCli cli = new DockerCli(null, file, activeProfiles);
return new DefaultDockerCompose(cli, hostname);
}
}

@ -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<String> 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:
* <ul>
* <li>{@code compose.yaml}</li>
* <li>{@code compose.yml}</li>
* <li>{@code docker-compose.yaml}</li>
* <li>{@code docker-compose.yml}</li>
* </ul>
* @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);
}
}

@ -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");
}
}

@ -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<String, String> 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<String> env) {
this.map = parse(env);
}
private Map<String, String> parse(List<String> env) {
if (CollectionUtils.isEmpty(env)) {
return Collections.emptyMap();
}
Map<String, String> 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<String, String> asMap() {
return this.map;
}
private record Entry(String key, String value) {
}
}

@ -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);
}
}

@ -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<List<DockerCliContextResponse>> 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<String, String> systemEnv,
Supplier<List<DockerCliContextResponse>> 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<String, String> systemEnv) {
return systemEnv.apply("SERVICES_HOST");
}
private static String fromDockerHostEnv(Function<String, String> systemEnv) {
return fromEndpoint(systemEnv.apply("DOCKER_HOST"));
}
private static String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) {
DockerCliContextResponse current = getCurrentContext(contextsSupplier.get());
return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null;
}
private static DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> 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;
}
}
}

@ -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 <T> the item type
* @param json the source JSON
* @param itemType the item type
* @return a list of items
*/
static <T> List<T> deserializeToList(String json, Class<T> 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 <T> the result type
* @param json the source JSON
* @param type the result type
* @return the deserialized result
*/
static <T> T deserialize(String json, Class<T> type) {
return deserialize(json, objectMapper.getTypeFactory().constructType(type));
}
private static <T> T deserialize(String json, JavaType type) {
try {
return objectMapper.readValue(json.trim(), type);
}
catch (IOException ex) {
throw new DockerOutputParseException(json, ex);
}
}
}

@ -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;
}
}

@ -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);
}
}

@ -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);
}
}

@ -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 [<registry>/][<project>/]<image>[:<tag>|@<digest>]}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
* @see <a href="https://docs.docker.com/compose/compose-file/#image">docker
* documentation</a>
*/
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;
}
}

@ -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;
}
}

@ -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;
}
}
}
}

@ -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);
}
}

@ -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<String, String> env();
/**
* Return the labels attached to the service.
* @return the service labels
*/
Map<String, String> labels();
}

@ -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;

@ -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<ApplicationListener<?>> eventListeners;
private final DockerComposeSkipCheck skipCheck;
private final ServiceReadinessChecks serviceReadinessChecks;
DockerComposeLifecycleManager(ApplicationContext applicationContext, Binder binder,
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
Set<ApplicationListener<?>> eventListeners) {
this(null, applicationContext, binder, shutdownHandlers, properties, eventListeners,
new DockerComposeSkipCheck(), null);
}
DockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder,
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
Set<ApplicationListener<?>> 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<String> 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<RunningService> 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<String> 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);
}
}

@ -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<ApplicationPreparedEvent> {
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<ApplicationListener<?>> eventListeners = event.getSpringApplication().getListeners();
createDockerComposeLifecycleManager(applicationContext, binder, properties, eventListeners).startup();
}
protected DockerComposeLifecycleManager createDockerComposeLifecycleManager(
ConfigurableApplicationContext applicationContext, Binder binder, DockerComposeProperties properties,
Set<ApplicationListener<?>> eventListeners) {
return new DockerComposeLifecycleManager(applicationContext, binder, this.shutdownHandlers, properties,
eventListeners);
}
}

@ -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<String> active = new LinkedHashSet<>();
public Set<String> getActive() {
return this.active;
}
public void setActive(Set<String> 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;
}
}
}

@ -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<RunningService> runningServices;
DockerComposeServicesReadyEvent(ApplicationContext source, List<RunningService> 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<RunningService> getRunningServices() {
return this.runningServices;
}
}

@ -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<String> REQUIRED_CLASSES = Set.of("org.junit.jupiter.api.Test", "org.junit.Test");
private static final Set<String> SKIPPED_STACK_ELEMENTS;
static {
Set<String> 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;
}
}

@ -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;
}
}

@ -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<DockerCompose, Duration> action;
ShutdownCommand(BiConsumer<DockerCompose, Duration> action) {
this.action = action;
}
void applyTo(DockerCompose dockerCompose, Duration timeout) {
this.action.accept(dockerCompose, timeout);
}
}

@ -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<DockerCompose> action;
StartupCommand(Consumer<DockerCompose> action) {
this.action = action;
}
void applyTo(DockerCompose dockerCompose) {
this.action.accept(dockerCompose);
}
}

@ -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;

@ -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;
}
}
}

@ -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<ServiceNotReadyException> exceptions) {
super(buildMessage(timeout, exceptions));
this.timeout = timeout;
exceptions.forEach(this::addSuppressed);
}
private static String buildMessage(Duration timeout, List<ServiceNotReadyException> exceptions) {
List<String> 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;
}
}

@ -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;
}
}

@ -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:
* <ul>
* <li>{@link ClassLoader}</li>
* <li>{@link Environment}</li>
* <li>{@link Binder}</li>
* </ul>
*
* @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;
}

@ -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<Duration> sleep;
private final ReadinessProperties properties;
private final List<ServiceReadinessCheck> 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<Duration> sleep, SpringFactoriesLoader loader, ClassLoader classLoader,
Environment environment, Binder binder,
Function<ReadinessProperties.Tcp, ServiceReadinessCheck> 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<RunningService> runningServices) {
Duration timeout = this.properties.getTimeout();
Instant start = this.clock.instant();
while (true) {
List<ServiceNotReadyException> 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<ServiceNotReadyException> check(List<RunningService> runningServices) {
List<ServiceNotReadyException> 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();
}
}
}

@ -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) {
}
}
}

@ -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;

@ -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 <D> the connection details type
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
*/
public abstract class DockerComposeConnectionDetailsFactory<D extends ConnectionDetails>
implements ConnectionDetailsFactory<DockerComposeConnectionSource, D> {
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;
}
}
}

@ -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;
}
}

@ -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<DockerComposeServicesReadyEvent> {
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<RunningService> 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 <T> void register(BeanDefinitionRegistry registry, RunningService runningService,
Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {
String beanName = getBeanName(runningService, connectionDetailsType, connectionDetails);
Class<T> beanType = (Class<T>) connectionDetails.getClass();
Supplier<T> beanSupplier = () -> (T) connectionDetails;
registry.registerBeanDefinition(beanName, new RootBeanDefinition(beanType, beanSupplier));
}
private String getBeanName(RunningService runningService, Class<?> connectionDetailsType,
ConnectionDetails connectionDetails) {
List<String> 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()));
}
}

@ -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<ElasticsearchConnectionDetails> {
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<Node> 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<Node> getNodes() {
return this.nodes;
}
}
}

@ -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<String, String> 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;
}
}

@ -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;

@ -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 : "";
}
}

@ -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;

@ -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<String, String> env) {
this.username = extractUsername(env);
this.password = extractPassword(env);
this.database = extractDatabase(env);
}
private String extractUsername(Map<String, String> env) {
String user = env.get("MARIADB_USER");
return (user != null) ? user : env.getOrDefault("MYSQL_USER", "root");
}
private String extractPassword(Map<String, String> 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<String, String> 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;
}
}

@ -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<JdbcConnectionDetails> {
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;
}
}
}

@ -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<R2dbcConnectionDetails> {
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;
}
}
}

@ -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;

@ -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<MongoConnectionDetails> {
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;
}
}
}

@ -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<String, String> 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;
}
}

@ -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;

@ -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<String, String> env) {
this.username = env.getOrDefault("MYSQL_USER", "root");
this.password = extractPassword(env);
this.database = extractDatabase(env);
}
private String extractPassword(Map<String, String> 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<String, String> 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;
}
}

@ -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<JdbcConnectionDetails> {
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;
}
}
}

@ -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<R2dbcConnectionDetails> {
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;
}
}
}

@ -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;

@ -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;

@ -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<String, String> env) {
this.username = env.getOrDefault("POSTGRES_USER", "postgres");
this.password = extractPassword(env);
this.database = env.getOrDefault("POSTGRES_DB", this.username);
}
private String extractPassword(Map<String, String> 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;
}
}

@ -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<JdbcConnectionDetails> {
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;
}
}
}

@ -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<R2dbcConnectionDetails> {
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;
}
}
}

@ -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;

@ -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<String, String> parseParameters(String parameters) {
Map<String, String> 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);
}
}

@ -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;

@ -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<RabbitConnectionDetails> {
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<Address> 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<Address> getAddresses() {
return this.addresses;
}
}
}

@ -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<String, String> 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;
}
}

@ -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;

@ -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<RedisConnectionDetails> {
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;
}
}
}

@ -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;

@ -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<ZipkinConnectionDetails> {
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";
}
}
}

@ -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;

@ -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

@ -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'");
}
}
}

@ -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<String, ExposedPort> 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<RunningService> 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<String, ExposedPort> 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<RunningService> runningServices = compose.getRunningServices();
assertThat(runningServices).hasSize(1);
RunningService runningService = runningServices.get(0);
assertThat(runningService.host()).isEqualTo("192.168.1.1");
}
}

@ -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<String, String> labels = Map.of("spring", "boot");
Map<String, ExposedPort> exposedPorts = Map.of("8080/tcp", new ExposedPort());
List<String> env = List.of("a=b");
Config config = new Config(image, labels, exposedPorts, env);
Map<String, List<HostPort>> 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");
}
}

@ -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();
}
}

@ -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);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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<String, String> 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<String> 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 <K, V> LinkedHashMap<K, V> linkedMapOf(Object... values) {
LinkedHashMap<K, V> result = new LinkedHashMap<>();
for (int i = 0; i < values.length; i = i + 2) {
result.put((K) values[i], (V) values[i + 1]);
}
return result;
}
}

@ -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<DockerCliContextResponse> context = cli.run(new DockerCliCommand.Context());
assertThat(context).isNotEmpty();
}
}

@ -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);
}
}

@ -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);
}
}

@ -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));
}
}

@ -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<String, String> NO_SYSTEM_ENV = (key) -> null;
private static final Supplier<List<DockerCliContextResponse>> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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<DockerCliContextResponse> 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");
}
}

@ -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<TestResponse> 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<TestResponse> response = DockerJson.deserializeToList(json, TestResponse.class);
assertThat(response).containsExactly(new TestResponse(1), new TestResponse(2));
}
record TestResponse(int value) {
}
}

@ -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);
}
}

@ -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();
});
}
}

@ -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<String> activeProfiles;
private GenericApplicationContext applicationContext;
private TestSpringApplicationShutdownHandlers shutdownHandlers;
private ServiceReadinessChecks serviceReadinessChecks;
private List<RunningService> runningServices;
private DockerComposeProperties properties;
private LinkedHashSet<ApplicationListener<?>> 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<String, String> 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<Runnable> 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<DockerComposeServicesReadyEvent> {
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<ApplicationListener<?>> 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<String> activeProfiles) {
DockerComposeLifecycleManagerTests.this.activeProfiles = activeProfiles;
return DockerComposeLifecycleManagerTests.this.dockerCompose;
}
}
}

@ -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<ApplicationListener<?>> 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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save