Browse Source
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
160 changed files with 11454 additions and 2 deletions
@ -0,0 +1,32 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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