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
parent
4ae24e404e
commit
842e17eced
@ -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…
Reference in New Issue